Compare commits
1 Commits
load_diffs
...
test-branc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79037ff600 |
@@ -16,7 +16,9 @@ rustflags = ["-D", "warnings"]
|
||||
debug = "limited"
|
||||
|
||||
# Use Mold on Linux, because it's faster than GNU ld and LLD.
|
||||
# We dont use wild in CI as its not production ready.
|
||||
#
|
||||
# We no longer set this in the default `config.toml` so that developers can opt in to Wild, which
|
||||
# is faster than Mold, in their own ~/.cargo/config.toml.
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
@@ -8,14 +8,6 @@ perf-test = ["test", "--profile", "release-fast", "--lib", "--bins", "--tests",
|
||||
# Keep similar flags here to share some ccache
|
||||
perf-compare = ["run", "--profile", "release-fast", "-p", "perf", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]", "--", "compare"]
|
||||
|
||||
# [target.x86_64-unknown-linux-gnu]
|
||||
# linker = "clang"
|
||||
# rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = [
|
||||
"--cfg",
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,4 +1,4 @@
|
||||
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Request
|
||||
|
||||
6
.github/actions/run_tests/action.yml
vendored
6
.github/actions/run_tests/action.yml
vendored
@@ -4,8 +4,10 @@ description: "Runs the tests"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: Install Rust
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
cargo install cargo-nextest --locked
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
|
||||
3
.github/actions/run_tests_windows/action.yml
vendored
3
.github/actions/run_tests_windows/action.yml
vendored
@@ -11,8 +11,9 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install test runner
|
||||
shell: powershell
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
uses: taiki-e/install-action@nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
|
||||
30
.github/workflows/after_release.yml
vendored
30
.github/workflows/after_release.yml
vendored
@@ -56,14 +56,14 @@ jobs:
|
||||
- id: set-package-name
|
||||
name: after_release::publish_winget::set_package_name
|
||||
run: |
|
||||
if ("${{ github.event.release.prerelease }}" -eq "true") {
|
||||
$PACKAGE_NAME = "ZedIndustries.Zed.Preview"
|
||||
} else {
|
||||
$PACKAGE_NAME = "ZedIndustries.Zed"
|
||||
}
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
PACKAGE_NAME=ZedIndustries.Zed.Preview
|
||||
else
|
||||
PACKAGE_NAME=ZedIndustries.Zed
|
||||
fi
|
||||
|
||||
echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT
|
||||
shell: pwsh
|
||||
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: after_release::publish_winget::winget_releaser
|
||||
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
|
||||
with:
|
||||
@@ -86,19 +86,3 @@ jobs:
|
||||
SENTRY_ORG: zed-dev
|
||||
SENTRY_PROJECT: zed
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
notify_on_failure:
|
||||
needs:
|
||||
- rebuild_releases_page
|
||||
- post_to_discord
|
||||
- publish_winget
|
||||
- create_sentry_release
|
||||
if: failure()
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: release::notify_on_failure::notify_slack
|
||||
run: |-
|
||||
curl -X POST -H 'Content-type: application/json'\
|
||||
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }}
|
||||
|
||||
2
.github/workflows/bump_patch_version.yml
vendored
2
.github/workflows/bump_patch_version.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
which cargo-set-version > /dev/null || cargo install cargo-edit -f --no-default-features --features "set-version"
|
||||
which cargo-set-version > /dev/null || cargo install cargo-edit
|
||||
output="$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')"
|
||||
export GIT_COMMITTER_NAME="Zed Bot"
|
||||
export GIT_COMMITTER_EMAIL="hi@zed.dev"
|
||||
|
||||
5
.github/workflows/cherry_pick.yml
vendored
5
.github/workflows/cherry_pick.yml
vendored
@@ -1,7 +1,6 @@
|
||||
# Generated from xtask::workflows::cherry_pick
|
||||
# Rebuild with `cargo xtask workflows`.
|
||||
name: cherry_pick
|
||||
run-name: 'cherry_pick to ${{ inputs.channel }} #${{ inputs.pr_number }}'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -17,10 +16,6 @@ on:
|
||||
description: channel
|
||||
required: true
|
||||
type: string
|
||||
pr_number:
|
||||
description: pr_number
|
||||
required: true
|
||||
type: string
|
||||
jobs:
|
||||
run_cherry_pick:
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
|
||||
@@ -15,15 +15,14 @@ jobs:
|
||||
stale-issue-message: >
|
||||
Hi there! 👋
|
||||
|
||||
We're working to clean up our issue tracker by closing older bugs that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and it will be kept open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, it will close automatically in 14 days.
|
||||
We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and we will keep it open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, we'll close it in 7 days.
|
||||
|
||||
Thanks for your help!
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
|
||||
days-before-stale: 60
|
||||
days-before-close: 14
|
||||
only-issue-types: "Bug,Crash"
|
||||
days-before-stale: 120
|
||||
days-before-close: 7
|
||||
any-of-issue-labels: "bug,panic / crash"
|
||||
operations-per-run: 1000
|
||||
ascending: true
|
||||
enable-statistics: true
|
||||
stale-issue-label: "stale"
|
||||
exempt-issue-labels: "never stale"
|
||||
|
||||
3
.github/workflows/compare_perf.yml
vendored
3
.github/workflows/compare_perf.yml
vendored
@@ -39,7 +39,8 @@ jobs:
|
||||
run: ./script/download-wasi-sdk
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: compare_perf::run_perf::install_hyperfine
|
||||
uses: taiki-e/install-action@hyperfine
|
||||
run: cargo install hyperfine
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::git_checkout
|
||||
run: git fetch origin ${{ inputs.base }} && git checkout ${{ inputs.base }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
4
.github/workflows/deploy_collab.yml
vendored
4
.github/workflows/deploy_collab.yml
vendored
@@ -43,7 +43,9 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install cargo nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
cargo install cargo-nextest --locked
|
||||
|
||||
- name: Limit target directory size
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@@ -29,6 +29,9 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than 300
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -75,7 +78,8 @@ jobs:
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than 250
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -108,6 +112,9 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::cargo_install_nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||
shell: pwsh
|
||||
@@ -477,20 +484,6 @@ jobs:
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
notify_on_failure:
|
||||
needs:
|
||||
- upload_release_assets
|
||||
- auto_release_preview
|
||||
if: failure()
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: release::notify_on_failure::notify_slack
|
||||
run: |-
|
||||
curl -X POST -H 'Content-type: application/json'\
|
||||
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
21
.github/workflows/release_nightly.yml
vendored
21
.github/workflows/release_nightly.yml
vendored
@@ -47,6 +47,9 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::cargo_install_nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||
shell: pwsh
|
||||
@@ -490,21 +493,3 @@ jobs:
|
||||
SENTRY_PROJECT: zed
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
timeout-minutes: 60
|
||||
notify_on_failure:
|
||||
needs:
|
||||
- bundle_linux_aarch64
|
||||
- bundle_linux_x86_64
|
||||
- bundle_mac_aarch64
|
||||
- bundle_mac_x86_64
|
||||
- bundle_windows_aarch64
|
||||
- bundle_windows_x86_64
|
||||
if: failure()
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: release::notify_on_failure::notify_slack
|
||||
run: |-
|
||||
curl -X POST -H 'Content-type: application/json'\
|
||||
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }}
|
||||
|
||||
8
.github/workflows/run_agent_evals.yml
vendored
8
.github/workflows/run_agent_evals.yml
vendored
@@ -6,9 +6,6 @@ env:
|
||||
CARGO_INCREMENTAL: '0'
|
||||
RUST_BACKTRACE: '1'
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
|
||||
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_EVAL_TELEMETRY: '1'
|
||||
MODEL_NAME: ${{ inputs.model_name }}
|
||||
@@ -51,11 +48,6 @@ jobs:
|
||||
- name: run_agent_evals::agent_evals::run_eval
|
||||
run: cargo run --package=eval -- --repetitions=8 --concurrency=1 --model "${MODEL_NAME}"
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
|
||||
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
run: |
|
||||
|
||||
68
.github/workflows/run_cron_unit_evals.yml
vendored
68
.github/workflows/run_cron_unit_evals.yml
vendored
@@ -1,68 +0,0 @@
|
||||
# Generated from xtask::workflows::run_cron_unit_evals
|
||||
# Rebuild with `cargo xtask workflows`.
|
||||
name: run_cron_unit_evals
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: '0'
|
||||
RUST_BACKTRACE: '1'
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
on:
|
||||
schedule:
|
||||
- cron: 47 1 * * 2
|
||||
workflow_dispatch: {}
|
||||
jobs:
|
||||
cron_unit_evals:
|
||||
runs-on: namespace-profile-16x32-ubuntu-2204
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
- name: steps::setup_cargo_config
|
||||
run: |
|
||||
mkdir -p ./../.cargo
|
||||
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cache_rust_dependencies_namespace
|
||||
uses: namespacelabs/nscloud-cache-action@v1
|
||||
with:
|
||||
cache: rust
|
||||
- name: steps::setup_linux
|
||||
run: ./script/linux
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::install_mold
|
||||
run: ./script/install-mold
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::download_wasi_sdk
|
||||
run: ./script/download-wasi-sdk
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than 250
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/run-unit-evals
|
||||
run: ./script/run-unit-evals
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
|
||||
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: run_agent_evals::cron_unit_evals::send_failure_to_slack
|
||||
if: ${{ failure() }}
|
||||
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: C04UDRNNJFQ
|
||||
text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
9
.github/workflows/run_tests.yml
vendored
9
.github/workflows/run_tests.yml
vendored
@@ -113,6 +113,9 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::cargo_install_nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||
shell: pwsh
|
||||
@@ -161,7 +164,8 @@ jobs:
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than 250
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -196,6 +200,9 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than 300
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
39
.github/workflows/run_unit_evals.yml
vendored
39
.github/workflows/run_unit_evals.yml
vendored
@@ -1,26 +1,17 @@
|
||||
# Generated from xtask::workflows::run_unit_evals
|
||||
# Generated from xtask::workflows::run_agent_evals
|
||||
# Rebuild with `cargo xtask workflows`.
|
||||
name: run_unit_evals
|
||||
name: run_agent_evals
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: '0'
|
||||
RUST_BACKTRACE: '1'
|
||||
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
|
||||
ZED_EVAL_TELEMETRY: '1'
|
||||
MODEL_NAME: ${{ inputs.model_name }}
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
model_name:
|
||||
description: model_name
|
||||
required: true
|
||||
type: string
|
||||
commit_sha:
|
||||
description: commit_sha
|
||||
required: true
|
||||
type: string
|
||||
schedule:
|
||||
- cron: 47 1 * * 2
|
||||
workflow_dispatch: {}
|
||||
jobs:
|
||||
run_unit_evals:
|
||||
unit_evals:
|
||||
runs-on: namespace-profile-16x32-ubuntu-2204
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
@@ -46,7 +37,8 @@ jobs:
|
||||
run: ./script/download-wasi-sdk
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than 250
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -55,15 +47,20 @@ jobs:
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
|
||||
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
|
||||
UNIT_EVAL_COMMIT: ${{ inputs.commit_sha }}
|
||||
- name: run_agent_evals::unit_evals::send_failure_to_slack
|
||||
if: ${{ failure() }}
|
||||
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
|
||||
with:
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}
|
||||
payload: |
|
||||
channel: C04UDRNNJFQ
|
||||
text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.run_id }}
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
161
Cargo.lock
generated
161
Cargo.lock
generated
@@ -322,7 +322,6 @@ dependencies = [
|
||||
"assistant_slash_command",
|
||||
"assistant_slash_commands",
|
||||
"assistant_text_thread",
|
||||
"async-fs",
|
||||
"audio",
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
@@ -344,7 +343,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"html_to_markdown",
|
||||
"http_client",
|
||||
"image",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"jsonschema",
|
||||
@@ -1463,7 +1461,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"untrusted 0.7.1",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -2617,23 +2614,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "calloop"
|
||||
version = "0.14.3"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"log",
|
||||
"polling",
|
||||
"rustix 1.1.2",
|
||||
"rustix 0.38.44",
|
||||
"slab",
|
||||
"tracing",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop-wayland-source"
|
||||
version = "0.4.1"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
|
||||
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
|
||||
dependencies = [
|
||||
"calloop",
|
||||
"rustix 1.1.2",
|
||||
"rustix 0.38.44",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
]
|
||||
@@ -5311,7 +5311,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"supermaven",
|
||||
"sweep_ai",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -5861,7 +5860,6 @@ dependencies = [
|
||||
"lsp",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"proto",
|
||||
"semantic_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -6250,7 +6248,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"nanorand",
|
||||
"spin 0.9.8",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6361,9 +6359,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||
|
||||
[[package]]
|
||||
name = "fork"
|
||||
version = "0.4.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30268f1eefccc9d72f43692e8b89e659aeb52e84016c3b32b6e7e9f1c8f38f94"
|
||||
checksum = "05dc8b302e04a1c27f4fe694439ef0f29779ca4edc205b7b58f00db04e29656d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -7289,7 +7287,6 @@ dependencies = [
|
||||
"calloop",
|
||||
"calloop-wayland-source",
|
||||
"cbindgen",
|
||||
"circular-buffer",
|
||||
"cocoa 0.26.0",
|
||||
"cocoa-foundation 0.2.0",
|
||||
"collections",
|
||||
@@ -7345,7 +7342,6 @@ dependencies = [
|
||||
"slotmap",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"spin 0.10.0",
|
||||
"stacksafe",
|
||||
"strum 0.27.2",
|
||||
"sum_tree",
|
||||
@@ -8660,25 +8656,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-protocol"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c047f6b5e551563af2ddb13dafed833f0ec5a5b0f9621d5ad740a9ff1e1095"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bytes 1.10.1",
|
||||
"chrono",
|
||||
"futures 0.3.31",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-websocket-client"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4197fa926a6b0bddfed7377d9fed3d00a0dec44a1501e020097bd26604699cae"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -8687,7 +8681,6 @@ dependencies = [
|
||||
"jupyter-protocol",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
@@ -8877,7 +8870,6 @@ dependencies = [
|
||||
"icons",
|
||||
"image",
|
||||
"log",
|
||||
"open_ai",
|
||||
"open_router",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
@@ -9080,7 +9072,7 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
dependencies = [
|
||||
"spin 0.9.8",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10022,19 +10014,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniprofiler_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"log",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
@@ -10239,9 +10218,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nbformat"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89c7229d604d847227002715e1235cd84e81919285d904ccb290a42ecc409348"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -10527,10 +10505,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.6"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
|
||||
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"libm",
|
||||
"num-integer",
|
||||
@@ -11033,7 +11012,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum 0.27.2",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13073,23 +13051,6 @@ dependencies = [
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project_benchmarks"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"client",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
"node_runtime",
|
||||
"project",
|
||||
"settings",
|
||||
"watch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project_panel"
|
||||
version = "0.1.0"
|
||||
@@ -13117,7 +13078,6 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -14036,7 +13996,6 @@ dependencies = [
|
||||
"paths",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rayon",
|
||||
"release_channel",
|
||||
@@ -14282,7 +14241,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.2.16",
|
||||
"libc",
|
||||
"untrusted 0.9.0",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -14411,9 +14370,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.9"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
|
||||
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest",
|
||||
@@ -14463,26 +14422,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "runtimelib"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "481b48894073a0096f28cbe9860af01fc1b861e55b3bc96afafc645ee3de62dc"
|
||||
version = "0.25.0"
|
||||
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-dispatcher",
|
||||
"async-std",
|
||||
"aws-lc-rs",
|
||||
"base64 0.22.1",
|
||||
"bytes 1.10.1",
|
||||
"chrono",
|
||||
"data-encoding",
|
||||
"dirs 6.0.0",
|
||||
"dirs 5.0.1",
|
||||
"futures 0.3.31",
|
||||
"glob",
|
||||
"jupyter-protocol",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand 3.1.1",
|
||||
"smol",
|
||||
"thiserror 2.0.17",
|
||||
"uuid",
|
||||
"zeromq",
|
||||
]
|
||||
@@ -14750,7 +14708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted 0.9.0",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14762,7 +14720,7 @@ dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted 0.9.0",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14992,7 +14950,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted 0.9.0",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15895,15 +15853,6 @@ dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spirv"
|
||||
version = "0.3.0+sdk-1.3.268.0"
|
||||
@@ -16589,33 +16538,6 @@ dependencies = [
|
||||
"zeno",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sweep_ai"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"brotli",
|
||||
"client",
|
||||
"collections",
|
||||
"edit_prediction",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"indoc",
|
||||
"language",
|
||||
"project",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tree-sitter-rust",
|
||||
"util",
|
||||
"workspace",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia"
|
||||
version = "0.5.5"
|
||||
@@ -18612,12 +18534,6 @@ version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@@ -21230,7 +21146,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.214.0"
|
||||
version = "0.213.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
@@ -21243,11 +21159,11 @@ dependencies = [
|
||||
"audio",
|
||||
"auto_update",
|
||||
"auto_update_ui",
|
||||
"backtrace",
|
||||
"bincode 1.3.3",
|
||||
"breadcrumbs",
|
||||
"call",
|
||||
"channel",
|
||||
"chrono",
|
||||
"clap",
|
||||
"cli",
|
||||
"client",
|
||||
@@ -21305,8 +21221,8 @@ dependencies = [
|
||||
"menu",
|
||||
"migrator",
|
||||
"mimalloc",
|
||||
"miniprofiler_ui",
|
||||
"nc",
|
||||
"nix 0.29.0",
|
||||
"node_runtime",
|
||||
"notifications",
|
||||
"onboarding",
|
||||
@@ -21342,13 +21258,13 @@ dependencies = [
|
||||
"snippets_ui",
|
||||
"supermaven",
|
||||
"svg_preview",
|
||||
"sweep_ai",
|
||||
"sysinfo 0.37.2",
|
||||
"system_specs",
|
||||
"tab_switcher",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
"theme_extension",
|
||||
@@ -21777,7 +21693,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"strsim",
|
||||
"thiserror 2.0.17",
|
||||
"util",
|
||||
"uuid",
|
||||
|
||||
39
Cargo.toml
39
Cargo.toml
@@ -110,7 +110,6 @@ members = [
|
||||
"crates/menu",
|
||||
"crates/migrator",
|
||||
"crates/mistral",
|
||||
"crates/miniprofiler_ui",
|
||||
"crates/multi_buffer",
|
||||
"crates/nc",
|
||||
"crates/net",
|
||||
@@ -127,7 +126,6 @@ members = [
|
||||
"crates/picker",
|
||||
"crates/prettier",
|
||||
"crates/project",
|
||||
"crates/project_benchmarks",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/prompt_store",
|
||||
@@ -165,7 +163,6 @@ members = [
|
||||
"crates/sum_tree",
|
||||
"crates/supermaven",
|
||||
"crates/supermaven_api",
|
||||
"crates/sweep_ai",
|
||||
"crates/codestral",
|
||||
"crates/svg_preview",
|
||||
"crates/system_specs",
|
||||
@@ -344,7 +341,6 @@ menu = { path = "crates/menu" }
|
||||
migrator = { path = "crates/migrator" }
|
||||
mistral = { path = "crates/mistral" }
|
||||
multi_buffer = { path = "crates/multi_buffer" }
|
||||
miniprofiler_ui = { path = "crates/miniprofiler_ui" }
|
||||
nc = { path = "crates/nc" }
|
||||
net = { path = "crates/net" }
|
||||
node_runtime = { path = "crates/node_runtime" }
|
||||
@@ -399,7 +395,6 @@ streaming_diff = { path = "crates/streaming_diff" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
supermaven_api = { path = "crates/supermaven_api" }
|
||||
sweep_ai = { path = "crates/sweep_ai" }
|
||||
codestral = { path = "crates/codestral" }
|
||||
system_specs = { path = "crates/system_specs" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
@@ -480,7 +475,6 @@ bitflags = "2.6.0"
|
||||
blade-graphics = { version = "0.7.0" }
|
||||
blade-macros = { version = "0.3.0" }
|
||||
blade-util = { version = "0.3.0" }
|
||||
brotli = "8.0.2"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
@@ -488,7 +482,7 @@ cfg-if = "1.0.3"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ciborium = "0.2"
|
||||
circular-buffer = "1.0"
|
||||
clap = { version = "4.4", features = ["derive", "wrap_help"] }
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
cocoa = "=0.26.0"
|
||||
cocoa-foundation = "=0.2.0"
|
||||
convert_case = "0.8.0"
|
||||
@@ -510,7 +504,7 @@ emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
exec = "0.3.1"
|
||||
fancy-regex = "0.14.0"
|
||||
fork = "0.4.0"
|
||||
fork = "0.2.0"
|
||||
futures = "0.3"
|
||||
futures-batch = "0.6.1"
|
||||
futures-lite = "1.13"
|
||||
@@ -537,8 +531,8 @@ itertools = "0.14.0"
|
||||
json_dotpath = "1.1"
|
||||
jsonschema = "0.30.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = "0.10.0"
|
||||
jupyter-websocket-client = "0.15.0"
|
||||
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
@@ -551,7 +545,7 @@ minidumper = "0.8"
|
||||
moka = { version = "0.12.10", features = ["sync"] }
|
||||
naga = { version = "25.0", features = ["wgsl-in"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = "0.15.0"
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
num-traits = "0.2"
|
||||
@@ -622,8 +616,8 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662
|
||||
"stream",
|
||||
], package = "zed-reqwest", version = "0.12.15-zed" }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { version = "0.30.0", default-features = false, features = [
|
||||
"async-dispatcher-runtime", "aws-lc-rs"
|
||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
rustc-hash = "2.1.0"
|
||||
@@ -634,7 +628,6 @@ scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
||||
serde_derive = "1.0.221"
|
||||
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
|
||||
serde_json_lenient = { version = "0.2", features = [
|
||||
"preserve_order",
|
||||
@@ -728,7 +721,6 @@ yawc = "0.2.5"
|
||||
zeroize = "1.8"
|
||||
zstd = "0.11"
|
||||
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.61"
|
||||
features = [
|
||||
@@ -784,7 +776,6 @@ features = [
|
||||
notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
|
||||
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
|
||||
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
||||
calloop = { path = "/home/davidsk/tmp/calloop" }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
@@ -798,19 +789,6 @@ codegen-units = 16
|
||||
codegen-units = 16
|
||||
|
||||
[profile.dev.package]
|
||||
# proc-macros start
|
||||
gpui_macros = { opt-level = 3 }
|
||||
derive_refineable = { opt-level = 3 }
|
||||
settings_macros = { opt-level = 3 }
|
||||
sqlez_macros = { opt-level = 3, codegen-units = 1 }
|
||||
ui_macros = { opt-level = 3 }
|
||||
util_macros = { opt-level = 3 }
|
||||
serde_derive = { opt-level = 3 }
|
||||
quote = { opt-level = 3 }
|
||||
syn = { opt-level = 3 }
|
||||
proc-macro2 = { opt-level = 3 }
|
||||
# proc-macros end
|
||||
|
||||
taffy = { opt-level = 3 }
|
||||
cranelift-codegen = { opt-level = 3 }
|
||||
cranelift-codegen-meta = { opt-level = 3 }
|
||||
@@ -852,6 +830,7 @@ semantic_version = { codegen-units = 1 }
|
||||
session = { codegen-units = 1 }
|
||||
snippet = { codegen-units = 1 }
|
||||
snippets_ui = { codegen-units = 1 }
|
||||
sqlez_macros = { codegen-units = 1 }
|
||||
story = { codegen-units = 1 }
|
||||
supermaven_api = { codegen-units = 1 }
|
||||
telemetry_events = { codegen-units = 1 }
|
||||
@@ -861,7 +840,7 @@ ui_input = { codegen-units = 1 }
|
||||
zed_actions = { codegen-units = 1 }
|
||||
|
||||
[profile.release]
|
||||
debug = "full"
|
||||
debug = "limited"
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.91.1-bookworm as builder
|
||||
FROM rust:1.90-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ design
|
||||
|
||||
docs
|
||||
= @probably-neb
|
||||
= @miguelraz
|
||||
|
||||
extension
|
||||
= @kubkon
|
||||
@@ -99,9 +98,6 @@ settings_ui
|
||||
= @danilo-leal
|
||||
= @probably-neb
|
||||
|
||||
support
|
||||
= @miguelraz
|
||||
|
||||
tasks
|
||||
= @SomeoneToIgnore
|
||||
= @Veykril
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.00156 10.3996C9.32705 10.3996 10.4016 9.32509 10.4016 7.99961C10.4016 6.67413 9.32705 5.59961 8.00156 5.59961C6.67608 5.59961 5.60156 6.67413 5.60156 7.99961C5.60156 9.32509 6.67608 10.3996 8.00156 10.3996Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.4 5.6V8.6C10.4 9.07739 10.5896 9.53523 10.9272 9.8728C11.2648 10.2104 11.7226 10.4 12.2 10.4C12.6774 10.4 13.1352 10.2104 13.4728 9.8728C13.8104 9.53523 14 9.07739 14 8.6V8C14 6.64839 13.5436 5.33636 12.7048 4.27651C11.8661 3.21665 10.694 2.47105 9.37852 2.16051C8.06306 1.84997 6.68129 1.99269 5.45707 2.56554C4.23285 3.13838 3.23791 4.1078 2.63344 5.31672C2.02898 6.52565 1.85041 7.90325 2.12667 9.22633C2.40292 10.5494 3.11782 11.7405 4.15552 12.6065C5.19323 13.4726 6.49295 13.9629 7.84411 13.998C9.19527 14.0331 10.5187 13.611 11.6 12.8" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.3 KiB |
@@ -313,7 +313,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewTextThread",
|
||||
"cmd-alt-n": "agent::NewExternalAgentThread"
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -421,6 +421,12 @@
|
||||
"ctrl-[": "editor::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == helix_select && !menu",
|
||||
"bindings": {
|
||||
"escape": "vim::SwitchToHelixNormalMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
|
||||
"bindings": {
|
||||
|
||||
@@ -616,13 +616,9 @@
|
||||
"search": {
|
||||
// Whether to show the project search button in the status bar.
|
||||
"button": true,
|
||||
// Whether to only match on whole words.
|
||||
"whole_word": false,
|
||||
// Whether to match case sensitively.
|
||||
"case_sensitive": false,
|
||||
// Whether to include gitignored files in search results.
|
||||
"include_ignored": false,
|
||||
// Whether to interpret the search query as a regular expression.
|
||||
"regex": false,
|
||||
// Whether to center the cursor on each search match when navigating.
|
||||
"center_on_match": false
|
||||
@@ -742,31 +738,14 @@
|
||||
// "never"
|
||||
"show": "always"
|
||||
},
|
||||
// Sort order for entries in the project panel.
|
||||
// This setting can take three values:
|
||||
//
|
||||
// 1. Show directories first, then files:
|
||||
// "directories_first"
|
||||
// 2. Mix directories and files together:
|
||||
// "mixed"
|
||||
// 3. Show files first, then directories:
|
||||
// "files_first"
|
||||
"sort_mode": "directories_first",
|
||||
// Whether to enable drag-and-drop operations in the project panel.
|
||||
"drag_and_drop": true,
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
"hide_root": false,
|
||||
// Whether to hide the hidden entries in the project panel.
|
||||
"hide_hidden": false,
|
||||
// Settings for automatically opening files.
|
||||
"auto_open": {
|
||||
// Whether to automatically open newly created files in the editor.
|
||||
"on_create": true,
|
||||
// Whether to automatically open files after pasting or duplicating them.
|
||||
"on_paste": true,
|
||||
// Whether to automatically open files dropped from external sources.
|
||||
"on_drop": true
|
||||
}
|
||||
// Whether to automatically open files when pasting them in the project panel.
|
||||
"open_file_on_paste": true
|
||||
},
|
||||
"outline_panel": {
|
||||
// Whether to show the outline panel button in the status bar
|
||||
@@ -1560,8 +1539,6 @@
|
||||
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
|
||||
// Existing terminals will not pick up this change until they are recreated.
|
||||
"max_scroll_history_lines": 10000,
|
||||
// The multiplier for scrolling speed in the terminal.
|
||||
"scroll_multiplier": 1.0,
|
||||
// The minimum APCA perceptual contrast between foreground and background colors.
|
||||
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
|
||||
// especially for dark mode. Values range from 0 to 106.
|
||||
|
||||
@@ -1866,14 +1866,10 @@ impl AcpThread {
|
||||
.checkpoint
|
||||
.as_ref()
|
||||
.map(|c| c.git_checkpoint.clone());
|
||||
|
||||
// Cancel any in-progress generation before restoring
|
||||
let cancel_task = self.cancel(cx);
|
||||
let rewind = self.rewind(id.clone(), cx);
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
cancel_task.await;
|
||||
rewind.await?;
|
||||
if let Some(checkpoint) = checkpoint {
|
||||
git_store
|
||||
@@ -1898,25 +1894,9 @@ impl AcpThread {
|
||||
cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some((ix, _)) = this.user_message_mut(&id) {
|
||||
// Collect all terminals from entries that will be removed
|
||||
let terminals_to_remove: Vec<acp::TerminalId> = this.entries[ix..]
|
||||
.iter()
|
||||
.flat_map(|entry| entry.terminals())
|
||||
.filter_map(|terminal| terminal.read(cx).id().clone().into())
|
||||
.collect();
|
||||
|
||||
let range = ix..this.entries.len();
|
||||
this.entries.truncate(ix);
|
||||
cx.emit(AcpThreadEvent::EntriesRemoved(range));
|
||||
|
||||
// Kill and remove the terminals
|
||||
for terminal_id in terminals_to_remove {
|
||||
if let Some(terminal) = this.terminals.remove(&terminal_id) {
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.kill(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
this.action_log().update(cx, |action_log, cx| {
|
||||
action_log.reject_all_edits(Some(telemetry), cx)
|
||||
@@ -3823,314 +3803,4 @@ mod tests {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Tests that restoring a checkpoint properly cleans up terminals that were
|
||||
/// created after that checkpoint, and cancels any in-progress generation.
|
||||
///
|
||||
/// Reproduces issue #35142: When a checkpoint is restored, any terminal processes
|
||||
/// that were started after that checkpoint should be terminated, and any in-progress
|
||||
/// AI generation should be canceled.
|
||||
#[gpui::test]
|
||||
async fn test_restore_checkpoint_kills_terminal(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let connection = Rc::new(FakeAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send first user message to create a checkpoint
|
||||
cx.update(|cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(vec!["first message".into()], cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Send second message (creates another checkpoint) - we'll restore to this one
|
||||
cx.update(|cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send(vec!["second message".into()], cx)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create 2 terminals BEFORE the checkpoint that have completed running
|
||||
let terminal_id_1 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
|
||||
let mock_terminal_1 = cx.new(|cx| {
|
||||
let builder = ::terminal::TerminalBuilder::new_display_only(
|
||||
::terminal::terminal_settings::CursorShape::default(),
|
||||
::terminal::terminal_settings::AlternateScroll::On,
|
||||
None,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
builder.subscribe(cx)
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Created {
|
||||
terminal_id: terminal_id_1.clone(),
|
||||
label: "echo 'first'".to_string(),
|
||||
cwd: Some(PathBuf::from("/test")),
|
||||
output_byte_limit: None,
|
||||
terminal: mock_terminal_1.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Output {
|
||||
terminal_id: terminal_id_1.clone(),
|
||||
data: b"first\n".to_vec(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Exit {
|
||||
terminal_id: terminal_id_1.clone(),
|
||||
status: acp::TerminalExitStatus {
|
||||
exit_code: Some(0),
|
||||
signal: None,
|
||||
meta: None,
|
||||
},
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
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(),
|
||||
::terminal::terminal_settings::AlternateScroll::On,
|
||||
None,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
builder.subscribe(cx)
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Created {
|
||||
terminal_id: terminal_id_2.clone(),
|
||||
label: "echo 'second'".to_string(),
|
||||
cwd: Some(PathBuf::from("/test")),
|
||||
output_byte_limit: None,
|
||||
terminal: mock_terminal_2.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Output {
|
||||
terminal_id: terminal_id_2.clone(),
|
||||
data: b"second\n".to_vec(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Exit {
|
||||
terminal_id: terminal_id_2.clone(),
|
||||
status: acp::TerminalExitStatus {
|
||||
exit_code: Some(0),
|
||||
signal: None,
|
||||
meta: None,
|
||||
},
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Get the second message ID to restore to
|
||||
let second_message_id = thread.read_with(cx, |thread, _| {
|
||||
// At this point we have:
|
||||
// - Index 0: First user message (with checkpoint)
|
||||
// - Index 1: Second user message (with checkpoint)
|
||||
// No assistant responses because FakeAgentConnection just returns EndTurn
|
||||
let AgentThreadEntry::UserMessage(message) = &thread.entries[1] else {
|
||||
panic!("expected user message at index 1");
|
||||
};
|
||||
message.id.clone().unwrap()
|
||||
});
|
||||
|
||||
// Create a terminal AFTER the checkpoint we'll restore to.
|
||||
// This simulates the AI agent starting a long-running terminal command.
|
||||
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
|
||||
let mock_terminal = cx.new(|cx| {
|
||||
let builder = ::terminal::TerminalBuilder::new_display_only(
|
||||
::terminal::terminal_settings::CursorShape::default(),
|
||||
::terminal::terminal_settings::AlternateScroll::On,
|
||||
None,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
builder.subscribe(cx)
|
||||
});
|
||||
|
||||
// Register the terminal as created
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Created {
|
||||
terminal_id: terminal_id.clone(),
|
||||
label: "sleep 1000".to_string(),
|
||||
cwd: Some(PathBuf::from("/test")),
|
||||
output_byte_limit: None,
|
||||
terminal: mock_terminal.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Simulate the terminal producing output (still running)
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Output {
|
||||
terminal_id: terminal_id.clone(),
|
||||
data: b"terminal is running...\n".to_vec(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// Create a tool call entry that references this terminal
|
||||
// This represents the agent requesting a terminal command
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread
|
||||
.handle_session_update(
|
||||
acp::SessionUpdate::ToolCall(acp::ToolCall {
|
||||
id: acp::ToolCallId("terminal-tool-1".into()),
|
||||
title: "Running command".into(),
|
||||
kind: acp::ToolKind::Execute,
|
||||
status: acp::ToolCallStatus::InProgress,
|
||||
content: vec![acp::ToolCallContent::Terminal {
|
||||
terminal_id: terminal_id.clone(),
|
||||
}],
|
||||
locations: vec![],
|
||||
raw_input: Some(
|
||||
serde_json::json!({"command": "sleep 1000", "cd": "/test"}),
|
||||
),
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Verify terminal exists and is in the thread
|
||||
let terminal_exists_before =
|
||||
thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id));
|
||||
assert!(
|
||||
terminal_exists_before,
|
||||
"Terminal should exist before checkpoint restore"
|
||||
);
|
||||
|
||||
// Verify the terminal's underlying task is still running (not completed)
|
||||
let terminal_running_before = thread.read_with(cx, |thread, _cx| {
|
||||
let terminal_entity = thread.terminals.get(&terminal_id).unwrap();
|
||||
terminal_entity.read_with(cx, |term, _cx| {
|
||||
term.output().is_none() // output is None means it's still running
|
||||
})
|
||||
});
|
||||
assert!(
|
||||
terminal_running_before,
|
||||
"Terminal should be running before checkpoint restore"
|
||||
);
|
||||
|
||||
// Verify we have the expected entries before restore
|
||||
let entry_count_before = thread.read_with(cx, |thread, _| thread.entries.len());
|
||||
assert!(
|
||||
entry_count_before > 1,
|
||||
"Should have multiple entries before restore"
|
||||
);
|
||||
|
||||
// Restore the checkpoint to the second message.
|
||||
// This should:
|
||||
// 1. Cancel any in-progress generation (via the cancel() call)
|
||||
// 2. Remove the terminal that was created after that point
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.restore_checkpoint(second_message_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that no send_task is in progress after restore
|
||||
// (cancel() clears the send_task)
|
||||
let has_send_task_after = thread.read_with(cx, |thread, _| thread.send_task.is_some());
|
||||
assert!(
|
||||
!has_send_task_after,
|
||||
"Should not have a send_task after restore (cancel should have cleared it)"
|
||||
);
|
||||
|
||||
// Verify the entries were truncated (restoring to index 1 truncates at 1, keeping only index 0)
|
||||
let entry_count = thread.read_with(cx, |thread, _| thread.entries.len());
|
||||
assert_eq!(
|
||||
entry_count, 1,
|
||||
"Should have 1 entry after restore (only the first user message)"
|
||||
);
|
||||
|
||||
// Verify the 2 completed terminals from before the checkpoint still exist
|
||||
let terminal_1_exists = thread.read_with(cx, |thread, _| {
|
||||
thread.terminals.contains_key(&terminal_id_1)
|
||||
});
|
||||
assert!(
|
||||
terminal_1_exists,
|
||||
"Terminal 1 (from before checkpoint) should still exist"
|
||||
);
|
||||
|
||||
let terminal_2_exists = thread.read_with(cx, |thread, _| {
|
||||
thread.terminals.contains_key(&terminal_id_2)
|
||||
});
|
||||
assert!(
|
||||
terminal_2_exists,
|
||||
"Terminal 2 (from before checkpoint) should still exist"
|
||||
);
|
||||
|
||||
// Verify they're still in completed state
|
||||
let terminal_1_completed = thread.read_with(cx, |thread, _cx| {
|
||||
let terminal_entity = thread.terminals.get(&terminal_id_1).unwrap();
|
||||
terminal_entity.read_with(cx, |term, _cx| term.output().is_some())
|
||||
});
|
||||
assert!(terminal_1_completed, "Terminal 1 should still be completed");
|
||||
|
||||
let terminal_2_completed = thread.read_with(cx, |thread, _cx| {
|
||||
let terminal_entity = thread.terminals.get(&terminal_id_2).unwrap();
|
||||
terminal_entity.read_with(cx, |term, _cx| term.output().is_some())
|
||||
});
|
||||
assert!(terminal_2_completed, "Terminal 2 should still be completed");
|
||||
|
||||
// Verify the running terminal (created after checkpoint) was removed
|
||||
let terminal_3_exists =
|
||||
thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id));
|
||||
assert!(
|
||||
!terminal_3_exists,
|
||||
"Terminal 3 (created after checkpoint) should have been removed"
|
||||
);
|
||||
|
||||
// Verify total count is 2 (the two from before the checkpoint)
|
||||
let terminal_count = thread.read_with(cx, |thread, _| thread.terminals.len());
|
||||
assert_eq!(
|
||||
terminal_count, 2,
|
||||
"Should have exactly 2 terminals (the completed ones from before checkpoint)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,9 @@ impl LanguageModels {
|
||||
for model in provider.provided_models(cx) {
|
||||
let model_info = Self::map_language_model_to_info(&model, &provider);
|
||||
let model_id = model_info.id.clone();
|
||||
provider_models.push(model_info);
|
||||
if !recommended_models.contains(&(model.provider_id(), model.id())) {
|
||||
provider_models.push(model_info);
|
||||
}
|
||||
models.insert(model_id, model);
|
||||
}
|
||||
if !provider_models.is_empty() {
|
||||
|
||||
@@ -15,14 +15,12 @@ const SEPARATOR_MARKER: &str = "=======";
|
||||
const REPLACE_MARKER: &str = ">>>>>>> REPLACE";
|
||||
const SONNET_PARAMETER_INVOKE_1: &str = "</parameter>\n</invoke>";
|
||||
const SONNET_PARAMETER_INVOKE_2: &str = "</parameter></invoke>";
|
||||
const SONNET_PARAMETER_INVOKE_3: &str = "</parameter>";
|
||||
const END_TAGS: [&str; 6] = [
|
||||
const END_TAGS: [&str; 5] = [
|
||||
OLD_TEXT_END_TAG,
|
||||
NEW_TEXT_END_TAG,
|
||||
EDITS_END_TAG,
|
||||
SONNET_PARAMETER_INVOKE_1, // Remove these after switching to streaming tool call
|
||||
SONNET_PARAMETER_INVOKE_1, // Remove this after switching to streaming tool call
|
||||
SONNET_PARAMETER_INVOKE_2,
|
||||
SONNET_PARAMETER_INVOKE_3,
|
||||
];
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -569,29 +567,21 @@ mod tests {
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<old_text>some text</old_text><new_text>updated text</parameter></invoke>
|
||||
<old_text>more text</old_text><new_text>upd</parameter></new_text>
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![
|
||||
Edit {
|
||||
old_text: "some text".to_string(),
|
||||
new_text: "updated text".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "more text".to_string(),
|
||||
new_text: "upd".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
]
|
||||
vec![Edit {
|
||||
old_text: "some text".to_string(),
|
||||
new_text: "updated text".to_string(),
|
||||
line_hint: None,
|
||||
},]
|
||||
);
|
||||
assert_eq!(
|
||||
parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 4,
|
||||
mismatched_tags: 2
|
||||
tags: 2,
|
||||
mismatched_tags: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,25 +44,6 @@ pub async fn get_buffer_content_or_outline(
|
||||
.collect::<Vec<_>>()
|
||||
})?;
|
||||
|
||||
// If no outline exists, fall back to first 1KB so the agent has some context
|
||||
if outline_items.is_empty() {
|
||||
let text = buffer.read_with(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
let len = snapshot.len().min(1024);
|
||||
let content = snapshot.text_for_range(0..len).collect::<String>();
|
||||
if let Some(path) = path {
|
||||
format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}")
|
||||
} else {
|
||||
format!("# First 1KB of file (file too large to show full content, and no outline available)\n\n{content}")
|
||||
}
|
||||
})?;
|
||||
|
||||
return Ok(BufferContent {
|
||||
text,
|
||||
is_outline: false,
|
||||
});
|
||||
}
|
||||
|
||||
let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
|
||||
|
||||
let text = if let Some(path) = path {
|
||||
@@ -159,62 +140,3 @@ fn render_entries(
|
||||
|
||||
entries_rendered
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use project::Project;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_large_file_fallback_to_subset(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
|
||||
let content = "A".repeat(100 * 1024); // 100KB
|
||||
let content_len = content.len();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.create_buffer(true, cx))
|
||||
.await
|
||||
.expect("failed to create buffer");
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(content, cx));
|
||||
|
||||
let result = cx
|
||||
.spawn(|cx| async move { get_buffer_content_or_outline(buffer, None, &cx).await })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Should contain some of the actual file content
|
||||
assert!(
|
||||
result.text.contains("AAAAAAAAAA"),
|
||||
"Result did not contain content subset"
|
||||
);
|
||||
|
||||
// Should be marked as not an outline (it's truncated content)
|
||||
assert!(
|
||||
!result.is_outline,
|
||||
"Large file without outline should not be marked as outline"
|
||||
);
|
||||
|
||||
// Should be reasonably sized (much smaller than original)
|
||||
assert!(
|
||||
result.text.len() < 50 * 1024,
|
||||
"Result size {} should be smaller than 50KB",
|
||||
result.text.len()
|
||||
);
|
||||
|
||||
// Should be significantly smaller than the original content
|
||||
assert!(
|
||||
result.text.len() < content_len / 10,
|
||||
"Result should be much smaller than original content"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,8 +607,6 @@ pub struct Thread {
|
||||
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||
pub(crate) project: Entity<Project>,
|
||||
pub(crate) action_log: Entity<ActionLog>,
|
||||
/// Tracks the last time files were read by the agent, to detect external modifications
|
||||
pub(crate) file_read_times: HashMap<PathBuf, fs::MTime>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
@@ -667,7 +665,6 @@ impl Thread {
|
||||
prompt_capabilities_rx,
|
||||
project,
|
||||
action_log,
|
||||
file_read_times: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,7 +860,6 @@ impl Thread {
|
||||
updated_at: db_thread.updated_at,
|
||||
prompt_capabilities_tx,
|
||||
prompt_capabilities_rx,
|
||||
file_read_times: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1003,7 +999,6 @@ impl Thread {
|
||||
self.add_tool(NowTool);
|
||||
self.add_tool(OpenTool::new(self.project.clone()));
|
||||
self.add_tool(ReadFileTool::new(
|
||||
cx.weak_entity(),
|
||||
self.project.clone(),
|
||||
self.action_log.clone(),
|
||||
));
|
||||
|
||||
@@ -309,40 +309,6 @@ impl AgentTool for EditFileTool {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
// Check if the file has been modified since the agent last read it
|
||||
if let Some(abs_path) = abs_path.as_ref() {
|
||||
let (last_read_mtime, current_mtime, is_dirty) = self.thread.update(cx, |thread, cx| {
|
||||
let last_read = thread.file_read_times.get(abs_path).copied();
|
||||
let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime());
|
||||
let dirty = buffer.read(cx).is_dirty();
|
||||
(last_read, current, dirty)
|
||||
})?;
|
||||
|
||||
// Check for unsaved changes first - these indicate modifications we don't know about
|
||||
if is_dirty {
|
||||
anyhow::bail!(
|
||||
"This file cannot be written to because it has unsaved changes. \
|
||||
Please end the current conversation immediately by telling the user you want to write to this file (mention its path explicitly) but you can't write to it because it has unsaved changes. \
|
||||
Ask the user to save that buffer's changes and to inform you when it's ok to proceed."
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the file was modified on disk since we last read it
|
||||
if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) {
|
||||
// MTime can be unreliable for comparisons, so our newtype intentionally
|
||||
// doesn't support comparing them. If the mtime at all different
|
||||
// (which could be because of a modification or because e.g. system clock changed),
|
||||
// we pessimistically assume it was modified.
|
||||
if current != last_read {
|
||||
anyhow::bail!(
|
||||
"The file {} has been modified since you last read it. \
|
||||
Please read the file again to get the current state before editing it.",
|
||||
input.path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
||||
event_stream.update_diff(diff.clone());
|
||||
let _finalize_diff = util::defer({
|
||||
@@ -455,17 +421,6 @@ impl AgentTool for EditFileTool {
|
||||
log.buffer_edited(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
// Update the recorded read time after a successful edit so consecutive edits work
|
||||
if let Some(abs_path) = abs_path.as_ref() {
|
||||
if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| {
|
||||
buffer.file().and_then(|file| file.disk_state().mtime())
|
||||
})? {
|
||||
self.thread.update(cx, |thread, _| {
|
||||
thread.file_read_times.insert(abs_path.to_path_buf(), new_mtime);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let (new_text, unified_diff) = cx
|
||||
.background_spawn({
|
||||
@@ -1793,426 +1748,10 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_read_times_tracking(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
// Initially, file_read_times should be empty
|
||||
let is_empty = thread.read_with(cx, |thread, _| thread.file_read_times.is_empty());
|
||||
assert!(is_empty, "file_read_times should start empty");
|
||||
|
||||
// Create read tool
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
|
||||
// Read the file to record the read time
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that file_read_times now contains an entry for the file
|
||||
let has_entry = thread.read_with(cx, |thread, _| {
|
||||
thread.file_read_times.len() == 1
|
||||
&& thread
|
||||
.file_read_times
|
||||
.keys()
|
||||
.any(|path| path.ends_with("test.txt"))
|
||||
});
|
||||
assert!(
|
||||
has_entry,
|
||||
"file_read_times should contain an entry after reading the file"
|
||||
);
|
||||
|
||||
// Read the file again - should update the entry
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Should still have exactly one entry
|
||||
let has_one_entry = thread.read_with(cx, |thread, _| thread.file_read_times.len() == 1);
|
||||
assert!(
|
||||
has_one_entry,
|
||||
"file_read_times should still have one entry after re-reading"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_consecutive_edits_work(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let languages = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
let edit_tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Read the file first
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// First edit should work
|
||||
let edit_result = {
|
||||
let edit_task = cx.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "First edit".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
model.send_last_completion_stream_text_chunk(
|
||||
"<old_text>original content</old_text><new_text>modified content</new_text>"
|
||||
.to_string(),
|
||||
);
|
||||
model.end_last_completion_stream();
|
||||
|
||||
edit_task.await
|
||||
};
|
||||
assert!(
|
||||
edit_result.is_ok(),
|
||||
"First edit should succeed, got error: {:?}",
|
||||
edit_result.as_ref().err()
|
||||
);
|
||||
|
||||
// Second edit should also work because the edit updated the recorded read time
|
||||
let edit_result = {
|
||||
let edit_task = cx.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "Second edit".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
model.send_last_completion_stream_text_chunk(
|
||||
"<old_text>modified content</old_text><new_text>further modified content</new_text>".to_string(),
|
||||
);
|
||||
model.end_last_completion_stream();
|
||||
|
||||
edit_task.await
|
||||
};
|
||||
assert!(
|
||||
edit_result.is_ok(),
|
||||
"Second consecutive edit should succeed, got error: {:?}",
|
||||
edit_result.as_ref().err()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_external_modification_detected(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let languages = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
let edit_tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Read the file first
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Simulate external modification - advance time and save file
|
||||
cx.background_executor
|
||||
.advance_clock(std::time::Duration::from_secs(2));
|
||||
fs.save(
|
||||
path!("/root/test.txt").as_ref(),
|
||||
&"externally modified content".into(),
|
||||
language::LineEnding::Unix,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Reload the buffer to pick up the new mtime
|
||||
let project_path = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.find_project_path("root/test.txt", cx)
|
||||
})
|
||||
.expect("Should find project path");
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
buffer
|
||||
.update(cx, |buffer, cx| buffer.reload(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Try to edit - should fail because file was modified externally
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit after external change".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Edit should fail after external modification"
|
||||
);
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("has been modified since you last read it"),
|
||||
"Error should mention file modification, got: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dirty_buffer_detected(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let languages = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
let edit_tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Read the file first
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Open the buffer and make it dirty by editing without saving
|
||||
let project_path = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.find_project_path("root/test.txt", cx)
|
||||
})
|
||||
.expect("Should find project path");
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make an in-memory edit to the buffer (making it dirty)
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let end_point = buffer.max_point();
|
||||
buffer.edit([(end_point..end_point, " added text")], None, cx);
|
||||
});
|
||||
|
||||
// Verify buffer is dirty
|
||||
let is_dirty = buffer.read_with(cx, |buffer, _| buffer.is_dirty());
|
||||
assert!(is_dirty, "Buffer should be dirty after in-memory edit");
|
||||
|
||||
// Try to edit - should fail because buffer has unsaved changes
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit with dirty buffer".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_err(), "Edit should fail when buffer is dirty");
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("cannot be written to because it has unsaved changes"),
|
||||
"Error should mention unsaved changes, got: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, ToolCallUpdateFields};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use gpui::{App, Entity, SharedString, Task, WeakEntity};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use indoc::formatdoc;
|
||||
use language::Point;
|
||||
use language_model::{LanguageModelImage, LanguageModelToolResultContent};
|
||||
@@ -12,7 +12,7 @@ use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
|
||||
use crate::{AgentTool, Thread, ToolCallEventStream, outline};
|
||||
use crate::{AgentTool, ToolCallEventStream, outline};
|
||||
|
||||
/// Reads the content of the given file in the project.
|
||||
///
|
||||
@@ -42,19 +42,13 @@ pub struct ReadFileToolInput {
|
||||
}
|
||||
|
||||
pub struct ReadFileTool {
|
||||
thread: WeakEntity<Thread>,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
}
|
||||
|
||||
impl ReadFileTool {
|
||||
pub fn new(
|
||||
thread: WeakEntity<Thread>,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
) -> Self {
|
||||
pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
project,
|
||||
action_log,
|
||||
}
|
||||
@@ -201,17 +195,6 @@ impl AgentTool for ReadFileTool {
|
||||
anyhow::bail!("{file_path} not found");
|
||||
}
|
||||
|
||||
// Record the file read time and mtime
|
||||
if let Some(mtime) = buffer.read_with(cx, |buffer, _| {
|
||||
buffer.file().and_then(|file| file.disk_state().mtime())
|
||||
})? {
|
||||
self.thread
|
||||
.update(cx, |thread, _| {
|
||||
thread.file_read_times.insert(abs_path.to_path_buf(), mtime);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
let mut anchor = None;
|
||||
|
||||
// Check if specific line ranges are provided
|
||||
@@ -302,15 +285,11 @@ impl AgentTool for ReadFileTool {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{ContextServerRegistry, Templates, Thread};
|
||||
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use project::{FakeFs, Project};
|
||||
use prompt_store::ProjectContext;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -321,20 +300,7 @@ mod test {
|
||||
fs.insert_tree(path!("/root"), json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let (event_stream, _) = ToolCallEventStream::test();
|
||||
|
||||
let result = cx
|
||||
@@ -367,20 +333,7 @@ mod test {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = ReadFileToolInput {
|
||||
@@ -410,20 +363,7 @@ mod test {
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(Arc::new(rust_lang()));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = ReadFileToolInput {
|
||||
@@ -495,20 +435,7 @@ mod test {
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = ReadFileToolInput {
|
||||
@@ -536,20 +463,7 @@ mod test {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
|
||||
// start_line of 0 should be treated as 1
|
||||
let result = cx
|
||||
@@ -693,20 +607,7 @@ mod test {
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
|
||||
// Reading a file outside the project worktree should fail
|
||||
let result = cx
|
||||
@@ -920,24 +821,7 @@ mod test {
|
||||
.await;
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
));
|
||||
let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone()));
|
||||
|
||||
// Test reading allowed files in worktree1
|
||||
let result = cx
|
||||
|
||||
@@ -50,14 +50,13 @@ impl crate::AgentServer for CustomAgentServer {
|
||||
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
let name = self.name();
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
if let Some(settings) = settings
|
||||
settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.custom
|
||||
.get_mut(&name)
|
||||
{
|
||||
settings.default_mode = mode_id.map(|m| m.to_string())
|
||||
}
|
||||
.unwrap()
|
||||
.default_mode = mode_id.map(|m| m.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -98,8 +98,6 @@ util.workspace = true
|
||||
watch.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
image.workspace = true
|
||||
async-fs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
acp_thread = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -109,8 +109,6 @@ impl ContextPickerCompletionProvider {
|
||||
icon_path: Some(mode.icon().path().into()),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
// This ensures that when a user accepts this completion, the
|
||||
// completion menu will still be shown after "@category " is
|
||||
@@ -148,8 +146,6 @@ impl ContextPickerCompletionProvider {
|
||||
documentation: None,
|
||||
insert_text_mode: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
icon_path: Some(icon_for_completion),
|
||||
confirm: Some(confirm_completion_callback(
|
||||
thread_entry.title().clone(),
|
||||
@@ -181,8 +177,6 @@ impl ContextPickerCompletionProvider {
|
||||
documentation: None,
|
||||
insert_text_mode: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
icon_path: Some(icon_path),
|
||||
confirm: Some(confirm_completion_callback(
|
||||
rule.title,
|
||||
@@ -239,8 +233,6 @@ impl ContextPickerCompletionProvider {
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(completion_icon_path),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
confirm: Some(confirm_completion_callback(
|
||||
file_name,
|
||||
@@ -292,8 +284,6 @@ impl ContextPickerCompletionProvider {
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(icon_path),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
confirm: Some(confirm_completion_callback(
|
||||
symbol.name.into(),
|
||||
@@ -326,8 +316,6 @@ impl ContextPickerCompletionProvider {
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(icon_path),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
confirm: Some(confirm_completion_callback(
|
||||
url_to_fetch.to_string().into(),
|
||||
@@ -396,8 +384,6 @@ impl ContextPickerCompletionProvider {
|
||||
icon_path: Some(action.icon().path().into()),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
// This ensures that when a user accepts this completion, the
|
||||
// completion menu will still be shown after "@category " is
|
||||
@@ -708,18 +694,14 @@ fn build_symbol_label(symbol_name: &str, file_name: &str, line: u32, cx: &App) -
|
||||
}
|
||||
|
||||
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
|
||||
let path = cx
|
||||
.theme()
|
||||
.syntax()
|
||||
.highlight_id("variable")
|
||||
.map(HighlightId);
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
let mut label = CodeLabelBuilder::default();
|
||||
|
||||
label.push_str(file_name, None);
|
||||
label.push_str(" ", None);
|
||||
|
||||
if let Some(directory) = directory {
|
||||
label.push_str(directory, path);
|
||||
label.push_str(directory, comment_id);
|
||||
}
|
||||
|
||||
label.build()
|
||||
@@ -788,8 +770,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
)),
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: None,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
confirm: Some(Arc::new({
|
||||
let editor = editor.clone();
|
||||
|
||||
@@ -15,7 +15,6 @@ use editor::{
|
||||
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
|
||||
MultiBuffer, ToOffset,
|
||||
actions::Paste,
|
||||
code_context_menus::CodeContextMenu,
|
||||
display_map::{Crease, CreaseId, FoldId},
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
@@ -28,7 +27,6 @@ use gpui::{
|
||||
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
|
||||
Subscription, Task, TextStyle, WeakEntity, pulsating_between,
|
||||
};
|
||||
use itertools::Either;
|
||||
use language::{Buffer, Language, language_settings::InlayHintKind};
|
||||
use language_model::LanguageModelImage;
|
||||
use postage::stream::Stream as _;
|
||||
@@ -274,15 +272,6 @@ impl MessageEditor {
|
||||
self.editor.read(cx).is_empty(cx)
|
||||
}
|
||||
|
||||
pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
|
||||
self.editor
|
||||
.read(cx)
|
||||
.context_menu()
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
|
||||
}
|
||||
|
||||
pub fn mentions(&self) -> HashSet<MentionUri> {
|
||||
self.mention_set
|
||||
.mentions
|
||||
@@ -847,45 +836,6 @@ impl MessageEditor {
|
||||
cx.emit(MessageEditorEvent::Send)
|
||||
}
|
||||
|
||||
pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let editor = self.editor.clone();
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
let menu_is_open =
|
||||
editor.context_menu().borrow().as_ref().is_some_and(|menu| {
|
||||
matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
|
||||
});
|
||||
|
||||
let has_at_sign = {
|
||||
let snapshot = editor.display_snapshot(cx);
|
||||
let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
|
||||
let offset = cursor.to_offset(&snapshot);
|
||||
if offset > 0 {
|
||||
snapshot
|
||||
.buffer_snapshot()
|
||||
.reversed_chars_at(offset)
|
||||
.next()
|
||||
.map(|sign| sign == '@')
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if menu_is_open && has_at_sign {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.insert("@", window, cx);
|
||||
editor.show_completions(&editor::actions::ShowCompletions, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.send(cx);
|
||||
}
|
||||
@@ -913,114 +863,74 @@ impl MessageEditor {
|
||||
if !self.prompt_capabilities.borrow().image {
|
||||
return;
|
||||
}
|
||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||
return;
|
||||
};
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
use itertools::Itertools;
|
||||
let (mut images, paths) = clipboard
|
||||
.into_entries()
|
||||
.filter_map(|entry| match entry {
|
||||
ClipboardEntry::Image(image) => Some(Either::Left(image)),
|
||||
ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
|
||||
_ => None,
|
||||
})
|
||||
.partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
|
||||
|
||||
if !paths.is_empty() {
|
||||
images.extend(
|
||||
cx.background_spawn(async move {
|
||||
let mut images = vec![];
|
||||
for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
|
||||
let Ok(content) = async_fs::read(path).await else {
|
||||
continue;
|
||||
};
|
||||
let Ok(format) = image::guess_format(&content) else {
|
||||
continue;
|
||||
};
|
||||
images.push(gpui::Image::from_bytes(
|
||||
match format {
|
||||
image::ImageFormat::Png => gpui::ImageFormat::Png,
|
||||
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
|
||||
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
|
||||
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
|
||||
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
|
||||
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
|
||||
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
|
||||
_ => continue,
|
||||
},
|
||||
content,
|
||||
));
|
||||
let images = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| {
|
||||
item.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::Image(image) = entry {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
images
|
||||
})
|
||||
.await,
|
||||
);
|
||||
}
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if images.is_empty() {
|
||||
return;
|
||||
}
|
||||
if images.is_empty() {
|
||||
return;
|
||||
}
|
||||
cx.stop_propagation();
|
||||
|
||||
let replacement_text = MentionUri::PastedImage.as_link().to_string();
|
||||
let Ok(editor) = this.update(cx, |this, cx| {
|
||||
cx.stop_propagation();
|
||||
this.editor.clone()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
for image in images {
|
||||
let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
|
||||
editor.update_in(cx, |message_editor, window, cx| {
|
||||
let snapshot = message_editor.snapshot(window, cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.buffer_snapshot().as_singleton().unwrap();
|
||||
let replacement_text = MentionUri::PastedImage.as_link().to_string();
|
||||
for image in images {
|
||||
let (excerpt_id, text_anchor, multibuffer_anchor) =
|
||||
self.editor.update(cx, |message_editor, cx| {
|
||||
let snapshot = message_editor.snapshot(window, cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.buffer_snapshot().as_singleton().unwrap();
|
||||
|
||||
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
|
||||
let multibuffer_anchor = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_in_excerpt(*excerpt_id, text_anchor);
|
||||
message_editor.edit(
|
||||
[(
|
||||
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
|
||||
format!("{replacement_text} "),
|
||||
)],
|
||||
cx,
|
||||
);
|
||||
(*excerpt_id, text_anchor, multibuffer_anchor)
|
||||
})
|
||||
else {
|
||||
break;
|
||||
};
|
||||
|
||||
let content_len = replacement_text.len();
|
||||
let Some(start_anchor) = multibuffer_anchor else {
|
||||
continue;
|
||||
};
|
||||
let Ok(end_anchor) = editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let image = Arc::new(image);
|
||||
let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
|
||||
insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
MentionUri::PastedImage.name().into(),
|
||||
IconName::Image.path().into(),
|
||||
Some(Task::ready(Ok(image.clone())).shared()),
|
||||
editor.clone(),
|
||||
window,
|
||||
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
|
||||
let multibuffer_anchor = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_in_excerpt(*excerpt_id, text_anchor);
|
||||
message_editor.edit(
|
||||
[(
|
||||
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
|
||||
format!("{replacement_text} "),
|
||||
)],
|
||||
cx,
|
||||
)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let task = cx
|
||||
.spawn(async move |cx| {
|
||||
);
|
||||
(*excerpt_id, text_anchor, multibuffer_anchor)
|
||||
});
|
||||
|
||||
let content_len = replacement_text.len();
|
||||
let Some(start_anchor) = multibuffer_anchor else {
|
||||
continue;
|
||||
};
|
||||
let end_anchor = self.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
|
||||
});
|
||||
let image = Arc::new(image);
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
MentionUri::PastedImage.name().into(),
|
||||
IconName::Image.path().into(),
|
||||
Some(Task::ready(Ok(image.clone())).shared()),
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let task = cx
|
||||
.spawn_in(window, {
|
||||
async move |_, cx| {
|
||||
let format = image.format;
|
||||
let image = cx
|
||||
.update(|_, cx| LanguageModelImage::from_image(image, cx))
|
||||
@@ -1035,16 +945,15 @@ impl MessageEditor {
|
||||
} else {
|
||||
Err("Failed to convert image".into())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.mention_set
|
||||
.mentions
|
||||
.insert(crease_id, (MentionUri::PastedImage, task.clone()))
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
.shared();
|
||||
|
||||
self.mention_set
|
||||
.mentions
|
||||
.insert(crease_id, (MentionUri::PastedImage, task.clone()));
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if task.await.notify_async_err(cx).is_none() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
@@ -1054,9 +963,9 @@ impl MessageEditor {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_dragged_files(
|
||||
@@ -2713,14 +2622,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_large_file_mention_fallback(cx: &mut TestAppContext) {
|
||||
async fn test_large_file_mention_uses_outline(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
// Create a large file that exceeds AUTO_OUTLINE_SIZE
|
||||
// Using plain text without a configured language, so no outline is available
|
||||
const LINE: &str = "This is a line of text in the file\n";
|
||||
const LINE: &str = "fn example_function() { /* some code */ }\n";
|
||||
let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
|
||||
assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
|
||||
|
||||
@@ -2731,8 +2639,8 @@ mod tests {
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"large_file.txt": large_content.clone(),
|
||||
"small_file.txt": small_content,
|
||||
"large_file.rs": large_content.clone(),
|
||||
"small_file.rs": small_content,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@@ -2778,7 +2686,7 @@ mod tests {
|
||||
let large_file_abs_path = project.read_with(cx, |project, cx| {
|
||||
let worktree = project.worktrees(cx).next().unwrap();
|
||||
let worktree_root = worktree.read(cx).abs_path();
|
||||
worktree_root.join("large_file.txt")
|
||||
worktree_root.join("large_file.rs")
|
||||
});
|
||||
let large_file_task = message_editor.update(cx, |editor, cx| {
|
||||
editor.confirm_mention_for_file(large_file_abs_path, cx)
|
||||
@@ -2787,20 +2695,11 @@ mod tests {
|
||||
let large_file_mention = large_file_task.await.unwrap();
|
||||
match large_file_mention {
|
||||
Mention::Text { content, .. } => {
|
||||
// Should contain some of the content but not all of it
|
||||
assert!(
|
||||
content.contains(LINE),
|
||||
"Should contain some of the file content"
|
||||
);
|
||||
assert!(
|
||||
!content.contains(&LINE.repeat(100)),
|
||||
"Should not contain the full file"
|
||||
);
|
||||
// Should be much smaller than original
|
||||
assert!(
|
||||
content.len() < large_content.len() / 10,
|
||||
"Should be significantly truncated"
|
||||
);
|
||||
// Should contain outline header for large files
|
||||
assert!(content.contains("File outline for"));
|
||||
assert!(content.contains("file too large to show full content"));
|
||||
// Should not contain the full repeated content
|
||||
assert!(!content.contains(&LINE.repeat(100)));
|
||||
}
|
||||
_ => panic!("Expected Text mention for large file"),
|
||||
}
|
||||
@@ -2810,7 +2709,7 @@ mod tests {
|
||||
let small_file_abs_path = project.read_with(cx, |project, cx| {
|
||||
let worktree = project.worktrees(cx).next().unwrap();
|
||||
let worktree_root = worktree.read(cx).abs_path();
|
||||
worktree_root.join("small_file.txt")
|
||||
worktree_root.join("small_file.rs")
|
||||
});
|
||||
let small_file_task = message_editor.update(cx, |editor, cx| {
|
||||
editor.confirm_mention_for_file(small_file_abs_path, cx)
|
||||
@@ -2819,8 +2718,10 @@ mod tests {
|
||||
let small_file_mention = small_file_task.await.unwrap();
|
||||
match small_file_mention {
|
||||
Mention::Text { content, .. } => {
|
||||
// Should contain the full actual content
|
||||
// Should contain the actual content
|
||||
assert_eq!(content, small_content);
|
||||
// Should not contain outline header
|
||||
assert!(!content.contains("File outline for"));
|
||||
}
|
||||
_ => panic!("Expected Text mention for small file"),
|
||||
}
|
||||
|
||||
@@ -251,17 +251,17 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.start_slot::<Icon>(model_info.icon.map(|icon| {
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pl_0p5()
|
||||
.gap_1p5()
|
||||
.when_some(model_info.icon, |this, icon| {
|
||||
this.child(
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small)
|
||||
)
|
||||
})
|
||||
.w(px(240.))
|
||||
.child(Label::new(model_info.name.clone()).truncate()),
|
||||
)
|
||||
.end_slot(div().pr_3().when(is_selected, |this| {
|
||||
|
||||
@@ -51,7 +51,7 @@ use ui::{
|
||||
PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
||||
use workspace::{CollaboratorId, NewTerminal, Workspace};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::{Chat, ToggleModelSelector};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
|
||||
@@ -69,8 +69,8 @@ use crate::ui::{
|
||||
};
|
||||
use crate::{
|
||||
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
|
||||
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory,
|
||||
RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
|
||||
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, OpenHistory, RejectAll,
|
||||
RejectOnce, ToggleBurnMode, ToggleProfileSelector,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -278,7 +278,6 @@ pub struct AcpThreadView {
|
||||
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
|
||||
thread_retry_status: Option<RetryStatus>,
|
||||
thread_error: Option<ThreadError>,
|
||||
thread_error_markdown: Option<Entity<Markdown>>,
|
||||
thread_feedback: ThreadFeedbackState,
|
||||
list_state: ListState,
|
||||
auth_task: Option<Task<()>>,
|
||||
@@ -416,7 +415,6 @@ impl AcpThreadView {
|
||||
list_state: list_state,
|
||||
thread_retry_status: None,
|
||||
thread_error: None,
|
||||
thread_error_markdown: None,
|
||||
thread_feedback: Default::default(),
|
||||
auth_task: None,
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
@@ -800,7 +798,6 @@ impl AcpThreadView {
|
||||
|
||||
if should_retry {
|
||||
self.thread_error = None;
|
||||
self.thread_error_markdown = None;
|
||||
self.reset(window, cx);
|
||||
}
|
||||
}
|
||||
@@ -1330,7 +1327,6 @@ impl AcpThreadView {
|
||||
|
||||
fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
|
||||
self.thread_error = None;
|
||||
self.thread_error_markdown = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -3144,7 +3140,7 @@ impl AcpThreadView {
|
||||
.text_ui_sm(cx)
|
||||
.h_full()
|
||||
.children(terminal_view.map(|terminal_view| {
|
||||
let element = if terminal_view
|
||||
if terminal_view
|
||||
.read(cx)
|
||||
.content_mode(window, cx)
|
||||
.is_scrollable()
|
||||
@@ -3152,15 +3148,7 @@ impl AcpThreadView {
|
||||
div().h_72().child(terminal_view).into_any_element()
|
||||
} else {
|
||||
terminal_view.into_any_element()
|
||||
};
|
||||
|
||||
div()
|
||||
.on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
|
||||
window.dispatch_action(NewThread.boxed_clone(), cx);
|
||||
cx.stop_propagation();
|
||||
}))
|
||||
.child(element)
|
||||
.into_any_element()
|
||||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
@@ -4200,8 +4188,6 @@ impl AcpThreadView {
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(self.render_add_context_button(cx))
|
||||
.child(self.render_follow_toggle(cx))
|
||||
.children(self.render_burn_mode_toggle(cx)),
|
||||
)
|
||||
@@ -4516,29 +4502,6 @@ impl AcpThreadView {
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_add_context_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let message_editor = self.message_editor.clone();
|
||||
let menu_visible = message_editor.read(cx).is_completions_menu_visible(cx);
|
||||
|
||||
IconButton::new("add-context", IconName::AtSign)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.when(!menu_visible, |this| {
|
||||
this.tooltip(move |_window, cx| {
|
||||
Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
|
||||
})
|
||||
})
|
||||
.on_click(cx.listener(move |_this, _, window, cx| {
|
||||
let message_editor_clone = message_editor.clone();
|
||||
|
||||
window.defer(cx, move |window, cx| {
|
||||
message_editor_clone.update(cx, |message_editor, cx| {
|
||||
message_editor.trigger_completion_menu(window, cx);
|
||||
});
|
||||
});
|
||||
}))
|
||||
}
|
||||
|
||||
fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
|
||||
let workspace = self.workspace.clone();
|
||||
MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
|
||||
@@ -5356,9 +5319,9 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
fn render_thread_error(&self, cx: &mut Context<Self>) -> Option<Div> {
|
||||
let content = match self.thread_error.as_ref()? {
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), window, cx),
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
|
||||
ThreadError::Refusal => self.render_refusal_error(cx),
|
||||
ThreadError::AuthenticationRequired(error) => {
|
||||
self.render_authentication_required_error(error.clone(), cx)
|
||||
@@ -5443,12 +5406,7 @@ impl AcpThreadView {
|
||||
.dismiss_action(self.dismiss_error_button(cx))
|
||||
}
|
||||
|
||||
fn render_any_thread_error(
|
||||
&mut self,
|
||||
error: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) -> Callout {
|
||||
fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
|
||||
let can_resume = self
|
||||
.thread()
|
||||
.map_or(false, |thread| thread.read(cx).can_resume(cx));
|
||||
@@ -5461,24 +5419,11 @@ impl AcpThreadView {
|
||||
supports_burn_mode && thread.completion_mode() == CompletionMode::Normal
|
||||
});
|
||||
|
||||
let markdown = if let Some(markdown) = &self.thread_error_markdown {
|
||||
markdown.clone()
|
||||
} else {
|
||||
let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
|
||||
self.thread_error_markdown = Some(markdown.clone());
|
||||
markdown
|
||||
};
|
||||
|
||||
let markdown_style = default_markdown_style(false, true, window, cx);
|
||||
let description = self
|
||||
.render_markdown(markdown, markdown_style)
|
||||
.into_any_element();
|
||||
|
||||
Callout::new()
|
||||
.severity(Severity::Error)
|
||||
.title("Error")
|
||||
.icon(IconName::XCircle)
|
||||
.title("An Error Happened")
|
||||
.description_slot(description)
|
||||
.description(error.clone())
|
||||
.actions_slot(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
@@ -5497,9 +5442,11 @@ impl AcpThreadView {
|
||||
})
|
||||
.when(can_resume, |this| {
|
||||
this.child(
|
||||
IconButton::new("retry", IconName::RotateCw)
|
||||
Button::new("retry", "Retry")
|
||||
.icon(IconName::RotateCw)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Retry Generation"))
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.resume_chat(cx);
|
||||
})),
|
||||
@@ -5641,6 +5588,7 @@ impl AcpThreadView {
|
||||
|
||||
IconButton::new("copy", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Copy Error Message"))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
||||
@@ -5650,6 +5598,7 @@ impl AcpThreadView {
|
||||
fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss Error"))
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _, cx| {
|
||||
@@ -5867,7 +5816,7 @@ impl Render for AcpThreadView {
|
||||
None
|
||||
}
|
||||
})
|
||||
.children(self.render_thread_error(window, cx))
|
||||
.children(self.render_thread_error(cx))
|
||||
.when_some(
|
||||
self.new_server_version_available.as_ref().filter(|_| {
|
||||
!has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
|
||||
@@ -5933,6 +5882,7 @@ fn default_markdown_style(
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: colors.element_selection_background,
|
||||
code_block_overflow_x_scroll: true,
|
||||
table_overflow_x_scroll: true,
|
||||
heading_level_styles: Some(HeadingLevelStyles {
|
||||
h1: Some(TextStyleRefinement {
|
||||
font_size: Some(rems(1.15).into()),
|
||||
@@ -6000,7 +5950,6 @@ fn default_markdown_style(
|
||||
},
|
||||
link: TextStyleRefinement {
|
||||
background_color: Some(colors.editor_foreground.opacity(0.025)),
|
||||
color: Some(colors.text_accent),
|
||||
underline: Some(UnderlineStyle {
|
||||
color: Some(colors.text_accent.opacity(0.5)),
|
||||
thickness: px(1.),
|
||||
|
||||
@@ -8,7 +8,6 @@ use std::{ops::Range, sync::Arc};
|
||||
|
||||
use agent::ContextServerRegistry;
|
||||
use anyhow::Result;
|
||||
use client::zed_urls;
|
||||
use cloud_llm_client::{Plan, PlanV1, PlanV2};
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
@@ -27,20 +26,18 @@ use language_model::{
|
||||
use language_models::AllLanguageModelSettings;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
agent_server_store::{
|
||||
AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME,
|
||||
},
|
||||
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure,
|
||||
Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize,
|
||||
PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
|
||||
Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor,
|
||||
ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, PopoverMenu, Switch,
|
||||
SwitchColor, Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Workspace, create_and_open_local_file};
|
||||
use zed_actions::{ExtensionCategoryFilter, OpenBrowser};
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
|
||||
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
|
||||
pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
|
||||
@@ -418,7 +415,6 @@ impl AgentConfiguration {
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
|
||||
let popover_menu = PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
Button::new("add-provider", "Add Provider")
|
||||
@@ -429,6 +425,7 @@ impl AgentConfiguration {
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small),
|
||||
)
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.menu({
|
||||
let workspace = self.workspace.clone();
|
||||
move |window, cx| {
|
||||
@@ -450,11 +447,6 @@ impl AgentConfiguration {
|
||||
})
|
||||
}))
|
||||
}
|
||||
})
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(2.0),
|
||||
});
|
||||
|
||||
v_flex()
|
||||
@@ -549,6 +541,7 @@ impl AgentConfiguration {
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small),
|
||||
)
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.menu({
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
@@ -571,11 +564,6 @@ impl AgentConfiguration {
|
||||
})
|
||||
}))
|
||||
}
|
||||
})
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(2.0),
|
||||
});
|
||||
|
||||
v_flex()
|
||||
@@ -955,7 +943,7 @@ impl AgentConfiguration {
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let user_defined_agents: Vec<_> = user_defined_agents
|
||||
let user_defined_agents = user_defined_agents
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
|
||||
@@ -963,93 +951,27 @@ impl AgentConfiguration {
|
||||
} else {
|
||||
AgentIcon::Name(IconName::Ai)
|
||||
};
|
||||
(name, icon)
|
||||
self.render_agent_server(icon, name, true)
|
||||
.into_any_element()
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let add_agent_popover = PopoverMenu::new("add-agent-server-popover")
|
||||
.trigger(
|
||||
Button::new("add-agent", "Add Agent")
|
||||
.style(ButtonStyle::Outlined)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small),
|
||||
)
|
||||
.menu({
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
menu.entry("Install from Extensions", None, {
|
||||
|window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
ExtensionCategoryFilter::AgentServers,
|
||||
),
|
||||
id: None,
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
let add_agens_button = Button::new("add-agent", "Add Agent")
|
||||
.style(ButtonStyle::Outlined)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(move |_, window, cx| {
|
||||
if let Some(workspace) = window.root().flatten() {
|
||||
let workspace = workspace.downgrade();
|
||||
window
|
||||
.spawn(cx, async |cx| {
|
||||
open_new_agent_servers_entry_in_settings_editor(workspace, cx).await
|
||||
})
|
||||
.entry("Add Custom Agent", None, {
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = window.root().flatten() {
|
||||
let workspace = workspace.downgrade();
|
||||
window
|
||||
.spawn(cx, async |cx| {
|
||||
open_new_agent_servers_entry_in_settings_editor(
|
||||
workspace, cx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.separator()
|
||||
.header("Learn More")
|
||||
.item(
|
||||
ContextMenuEntry::new("Agent Servers Docs")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::End)
|
||||
.handler({
|
||||
move |window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(OpenBrowser {
|
||||
url: zed_urls::agent_server_docs(cx),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("ACP Docs")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::End)
|
||||
.handler({
|
||||
move |window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(OpenBrowser {
|
||||
url: "https://agentclientprotocol.com/".into(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(2.0),
|
||||
});
|
||||
|
||||
v_flex()
|
||||
@@ -1060,7 +982,7 @@ impl AgentConfiguration {
|
||||
.child(self.render_section_title(
|
||||
"External Agents",
|
||||
"All agents connected through the Agent Client Protocol.",
|
||||
add_agent_popover.into_any_element(),
|
||||
add_agens_button.into_any_element(),
|
||||
))
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -1071,29 +993,26 @@ impl AgentConfiguration {
|
||||
AgentIcon::Name(IconName::AiClaude),
|
||||
"Claude Code",
|
||||
false,
|
||||
cx,
|
||||
))
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
.child(self.render_agent_server(
|
||||
AgentIcon::Name(IconName::AiOpenAi),
|
||||
"Codex CLI",
|
||||
false,
|
||||
cx,
|
||||
))
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
.child(self.render_agent_server(
|
||||
AgentIcon::Name(IconName::AiGemini),
|
||||
"Gemini CLI",
|
||||
false,
|
||||
cx,
|
||||
))
|
||||
.map(|mut parent| {
|
||||
for (name, icon) in user_defined_agents {
|
||||
for agent in user_defined_agents {
|
||||
parent = parent
|
||||
.child(
|
||||
Divider::horizontal().color(DividerColor::BorderFaded),
|
||||
)
|
||||
.child(self.render_agent_server(icon, name, true, cx));
|
||||
.child(agent);
|
||||
}
|
||||
parent
|
||||
}),
|
||||
@@ -1106,7 +1025,6 @@ impl AgentConfiguration {
|
||||
icon: AgentIcon,
|
||||
name: impl Into<SharedString>,
|
||||
external: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let name = name.into();
|
||||
let icon = match icon {
|
||||
@@ -1121,53 +1039,28 @@ impl AgentConfiguration {
|
||||
let tooltip_id = SharedString::new(format!("agent-source-{}", name));
|
||||
let tooltip_message = format!("The {} agent was installed from an extension.", name);
|
||||
|
||||
let agent_server_name = ExternalAgentServerName(name.clone());
|
||||
|
||||
let uninstall_btn_id = SharedString::from(format!("uninstall-{}", name));
|
||||
let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Uninstall Agent Extension"))
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
let agent_name = agent_server_name.clone();
|
||||
|
||||
if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| {
|
||||
store.get_extension_id_for_agent(&agent_name)
|
||||
}) {
|
||||
ExtensionStore::global(cx)
|
||||
.update(cx, |store, cx| store.uninstall_extension(ext_id, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}));
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.gap_1p5()
|
||||
.child(icon)
|
||||
.child(Label::new(name))
|
||||
.when(external, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.id(tooltip_id)
|
||||
.flex_none()
|
||||
.tooltip(Tooltip::text(tooltip_message))
|
||||
.child(
|
||||
Icon::new(IconName::ZedSrcExtension)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(icon)
|
||||
.child(Label::new(name))
|
||||
.when(external, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.id(tooltip_id)
|
||||
.flex_none()
|
||||
.tooltip(Tooltip::text(tooltip_message))
|
||||
.child(
|
||||
Icon::new(IconName::ZedSrcExtension)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Success)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Success)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.when(external, |this| this.child(uninstall_button))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ use anyhow::{Context as _, Result};
|
||||
use context_server::{ContextServerCommand, ContextServerId};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle,
|
||||
Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
|
||||
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
|
||||
TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
|
||||
};
|
||||
use language::{Language, LanguageRegistry};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
@@ -23,8 +23,7 @@ use project::{
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip,
|
||||
WithScrollbar, prelude::*,
|
||||
CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{ModalView, Workspace};
|
||||
@@ -253,7 +252,6 @@ pub struct ConfigureContextServerModal {
|
||||
source: ConfigurationSource,
|
||||
state: State,
|
||||
original_server_id: Option<ContextServerId>,
|
||||
scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
impl ConfigureContextServerModal {
|
||||
@@ -363,7 +361,6 @@ impl ConfigureContextServerModal {
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -683,7 +680,6 @@ impl ConfigureContextServerModal {
|
||||
|
||||
impl Render for ConfigureContextServerModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
div()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
@@ -703,29 +699,14 @@ impl Render for ConfigureContextServerModal {
|
||||
Modal::new("configure-context-server", None)
|
||||
.header(self.render_modal_header())
|
||||
.section(
|
||||
Section::new().child(
|
||||
div()
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.id("modal-content")
|
||||
.max_h(vh(0.7, window))
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&scroll_handle)
|
||||
.child(self.render_modal_description(window, cx))
|
||||
.child(self.render_modal_content(cx))
|
||||
.child(match &self.state {
|
||||
State::Idle => div(),
|
||||
State::Waiting => {
|
||||
Self::render_waiting_for_context_server()
|
||||
}
|
||||
State::Error(error) => {
|
||||
Self::render_modal_error(error.clone())
|
||||
}
|
||||
}),
|
||||
)
|
||||
.vertical_scrollbar_for(scroll_handle, window, cx),
|
||||
),
|
||||
Section::new()
|
||||
.child(self.render_modal_description(window, cx))
|
||||
.child(self.render_modal_content(cx))
|
||||
.child(match &self.state {
|
||||
State::Idle => div(),
|
||||
State::Waiting => Self::render_waiting_for_context_server(),
|
||||
State::Error(error) => Self::render_modal_error(error.clone()),
|
||||
}),
|
||||
)
|
||||
.footer(self.render_modal_footer(cx)),
|
||||
)
|
||||
|
||||
@@ -1892,9 +1892,6 @@ impl AgentPanel {
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.new_thread_menu_handle.clone())
|
||||
.menu({
|
||||
let selected_agent = self.selected_agent.clone();
|
||||
let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let is_via_collab = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
@@ -1908,6 +1905,7 @@ impl AgentPanel {
|
||||
let active_thread = active_thread.clone();
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, cx| {
|
||||
menu.context(focus_handle.clone())
|
||||
.header("Zed Agent")
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
|
||||
@@ -1931,11 +1929,9 @@ impl AgentPanel {
|
||||
}
|
||||
})
|
||||
.item(
|
||||
ContextMenuEntry::new("Zed Agent")
|
||||
.when(is_agent_selected(AgentType::NativeAgent) | is_agent_selected(AgentType::TextThread) , |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
.icon(IconName::ZedAgent)
|
||||
ContextMenuEntry::new("New Thread")
|
||||
.action(NewThread.boxed_clone())
|
||||
.icon(IconName::Thread)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
@@ -1959,10 +1955,10 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("Text Thread")
|
||||
.action(NewTextThread.boxed_clone())
|
||||
ContextMenuEntry::new("New Text Thread")
|
||||
.icon(IconName::TextThread)
|
||||
.icon_color(Color::Muted)
|
||||
.action(NewTextThread.boxed_clone())
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
@@ -1987,10 +1983,7 @@ impl AgentPanel {
|
||||
.separator()
|
||||
.header("External Agents")
|
||||
.item(
|
||||
ContextMenuEntry::new("Claude Code")
|
||||
.when(is_agent_selected(AgentType::ClaudeCode), |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
ContextMenuEntry::new("New Claude Code")
|
||||
.icon(IconName::AiClaude)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -2016,10 +2009,7 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("Codex CLI")
|
||||
.when(is_agent_selected(AgentType::Codex), |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
ContextMenuEntry::new("New Codex CLI")
|
||||
.icon(IconName::AiOpenAi)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -2045,10 +2035,7 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("Gemini CLI")
|
||||
.when(is_agent_selected(AgentType::Gemini), |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
ContextMenuEntry::new("New Gemini CLI")
|
||||
.icon(IconName::AiGemini)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_via_collab)
|
||||
@@ -2074,8 +2061,8 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.map(|mut menu| {
|
||||
let agent_server_store = agent_server_store.read(cx);
|
||||
let agent_names = agent_server_store
|
||||
let agent_server_store_read = agent_server_store.read(cx);
|
||||
let agent_names = agent_server_store_read
|
||||
.external_agents()
|
||||
.filter(|name| {
|
||||
name.0 != GEMINI_NAME
|
||||
@@ -2084,38 +2071,21 @@ impl AgentPanel {
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let custom_settings = cx
|
||||
.global::<SettingsStore>()
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.clone();
|
||||
|
||||
for agent_name in agent_names {
|
||||
let icon_path = agent_server_store.agent_icon(&agent_name);
|
||||
|
||||
let mut entry = ContextMenuEntry::new(agent_name.clone());
|
||||
|
||||
let command = custom_settings
|
||||
.get(&agent_name.0)
|
||||
.map(|settings| settings.command.clone())
|
||||
.unwrap_or(placeholder_command());
|
||||
|
||||
let icon_path = agent_server_store_read.agent_icon(&agent_name);
|
||||
let mut entry =
|
||||
ContextMenuEntry::new(format!("New {}", agent_name));
|
||||
if let Some(icon_path) = icon_path {
|
||||
entry = entry.custom_icon_svg(icon_path);
|
||||
} else {
|
||||
entry = entry.icon(IconName::Terminal);
|
||||
}
|
||||
entry = entry
|
||||
.when(
|
||||
is_agent_selected(AgentType::Custom {
|
||||
name: agent_name.0.clone(),
|
||||
command: command.clone(),
|
||||
}),
|
||||
|this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_via_collab)
|
||||
.handler({
|
||||
@@ -2155,7 +2125,6 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
menu = menu.item(entry);
|
||||
}
|
||||
|
||||
@@ -2188,7 +2157,7 @@ impl AgentPanel {
|
||||
.id("selected_agent_icon")
|
||||
.when_some(selected_agent_custom_icon, |this, icon_path| {
|
||||
let label = selected_agent_label.clone();
|
||||
this.px_1()
|
||||
this.px(DynamicSpacing::Base02.rems(cx))
|
||||
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
|
||||
@@ -2197,7 +2166,7 @@ impl AgentPanel {
|
||||
.when(!has_custom_icon, |this| {
|
||||
this.when_some(self.selected_agent.icon(), |this, icon| {
|
||||
let label = selected_agent_label.clone();
|
||||
this.px_1()
|
||||
this.px(DynamicSpacing::Base02.rems(cx))
|
||||
.child(Icon::new(icon).color(Color::Muted))
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
|
||||
|
||||
@@ -30,10 +30,7 @@ use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, Entity, SharedString, actions};
|
||||
use language::{
|
||||
LanguageRegistry,
|
||||
language_settings::{AllLanguageSettings, EditPredictionProvider},
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
@@ -289,25 +286,7 @@ pub fn init(
|
||||
|
||||
fn update_command_palette_filter(cx: &mut App) {
|
||||
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
||||
let agent_enabled = AgentSettings::get_global(cx).enabled;
|
||||
let edit_prediction_provider = AllLanguageSettings::get_global(cx)
|
||||
.edit_predictions
|
||||
.provider;
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _| {
|
||||
use editor::actions::{
|
||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
};
|
||||
let edit_prediction_actions = [
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||
TypeId::of::<ShowEditPrediction>(),
|
||||
TypeId::of::<NextEditPrediction>(),
|
||||
TypeId::of::<PreviousEditPrediction>(),
|
||||
TypeId::of::<ToggleEditPrediction>(),
|
||||
];
|
||||
|
||||
if disable_ai {
|
||||
filter.hide_namespace("agent");
|
||||
filter.hide_namespace("assistant");
|
||||
@@ -316,47 +295,42 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
filter.hide_namespace("zed_predict_onboarding");
|
||||
filter.hide_namespace("edit_prediction");
|
||||
|
||||
use editor::actions::{
|
||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
};
|
||||
let edit_prediction_actions = [
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||
TypeId::of::<ShowEditPrediction>(),
|
||||
TypeId::of::<NextEditPrediction>(),
|
||||
TypeId::of::<PreviousEditPrediction>(),
|
||||
TypeId::of::<ToggleEditPrediction>(),
|
||||
];
|
||||
filter.hide_action_types(&edit_prediction_actions);
|
||||
filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||
} else {
|
||||
if agent_enabled {
|
||||
filter.show_namespace("agent");
|
||||
} else {
|
||||
filter.hide_namespace("agent");
|
||||
}
|
||||
|
||||
filter.show_namespace("agent");
|
||||
filter.show_namespace("assistant");
|
||||
|
||||
match edit_prediction_provider {
|
||||
EditPredictionProvider::None => {
|
||||
filter.hide_namespace("edit_prediction");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
filter.hide_action_types(&edit_prediction_actions);
|
||||
}
|
||||
EditPredictionProvider::Copilot => {
|
||||
filter.show_namespace("edit_prediction");
|
||||
filter.show_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
}
|
||||
EditPredictionProvider::Supermaven => {
|
||||
filter.show_namespace("edit_prediction");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.show_namespace("supermaven");
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
}
|
||||
EditPredictionProvider::Zed
|
||||
| EditPredictionProvider::Codestral
|
||||
| EditPredictionProvider::Experimental(_) => {
|
||||
filter.show_namespace("edit_prediction");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
}
|
||||
}
|
||||
|
||||
filter.show_namespace("copilot");
|
||||
filter.show_namespace("zed_predict_onboarding");
|
||||
|
||||
filter.show_namespace("edit_prediction");
|
||||
|
||||
use editor::actions::{
|
||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
};
|
||||
let edit_prediction_actions = [
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||
TypeId::of::<ShowEditPrediction>(),
|
||||
TypeId::of::<NextEditPrediction>(),
|
||||
TypeId::of::<PreviousEditPrediction>(),
|
||||
TypeId::of::<ToggleEditPrediction>(),
|
||||
];
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
|
||||
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||
}
|
||||
});
|
||||
@@ -446,137 +420,3 @@ fn register_slash_commands(cx: &mut App) {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use editor::actions::AcceptEditPrediction;
|
||||
use gpui::{BorrowAppContext, TestAppContext, px};
|
||||
use project::DisableAiSettings;
|
||||
use settings::{
|
||||
DefaultAgentView, DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
|
||||
// Init settings
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
command_palette_hooks::init(cx);
|
||||
AgentSettings::register(cx);
|
||||
DisableAiSettings::register(cx);
|
||||
AllLanguageSettings::register(cx);
|
||||
});
|
||||
|
||||
let agent_settings = AgentSettings {
|
||||
enabled: true,
|
||||
button: true,
|
||||
dock: DockPosition::Right,
|
||||
default_width: px(300.),
|
||||
default_height: px(600.),
|
||||
default_model: None,
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: vec![],
|
||||
default_profile: AgentProfileId::default(),
|
||||
default_view: DefaultAgentView::Thread,
|
||||
profiles: Default::default(),
|
||||
always_allow_tool_actions: false,
|
||||
notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
|
||||
play_sound_when_agent_done: false,
|
||||
single_file_review: false,
|
||||
model_parameters: vec![],
|
||||
preferred_completion_mode: CompletionMode::Normal,
|
||||
enable_feedback: false,
|
||||
expand_edit_card: true,
|
||||
expand_terminal_card: true,
|
||||
use_modifier_to_send: true,
|
||||
message_editor_min_lines: 1,
|
||||
};
|
||||
|
||||
cx.update(|cx| {
|
||||
AgentSettings::override_global(agent_settings.clone(), cx);
|
||||
DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
|
||||
|
||||
// Initial update
|
||||
update_command_palette_filter(cx);
|
||||
});
|
||||
|
||||
// Assert visible
|
||||
cx.update(|cx| {
|
||||
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
||||
assert!(
|
||||
!filter.is_hidden(&NewThread),
|
||||
"NewThread should be visible by default"
|
||||
);
|
||||
});
|
||||
|
||||
// Disable agent
|
||||
cx.update(|cx| {
|
||||
let mut new_settings = agent_settings.clone();
|
||||
new_settings.enabled = false;
|
||||
AgentSettings::override_global(new_settings, cx);
|
||||
|
||||
// Trigger update
|
||||
update_command_palette_filter(cx);
|
||||
});
|
||||
|
||||
// Assert hidden
|
||||
cx.update(|cx| {
|
||||
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
||||
assert!(
|
||||
filter.is_hidden(&NewThread),
|
||||
"NewThread should be hidden when agent is disabled"
|
||||
);
|
||||
});
|
||||
|
||||
// Test EditPredictionProvider
|
||||
// Enable EditPredictionProvider::Copilot
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project
|
||||
.all_languages
|
||||
.features
|
||||
.get_or_insert(Default::default())
|
||||
.edit_prediction_provider = Some(EditPredictionProvider::Copilot);
|
||||
});
|
||||
});
|
||||
update_command_palette_filter(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
||||
assert!(
|
||||
!filter.is_hidden(&AcceptEditPrediction),
|
||||
"EditPrediction should be visible when provider is Copilot"
|
||||
);
|
||||
});
|
||||
|
||||
// Disable EditPredictionProvider (None)
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project
|
||||
.all_languages
|
||||
.features
|
||||
.get_or_insert(Default::default())
|
||||
.edit_prediction_provider = Some(EditPredictionProvider::None);
|
||||
});
|
||||
});
|
||||
update_command_palette_filter(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
||||
assert!(
|
||||
filter.is_hidden(&AcceptEditPrediction),
|
||||
"EditPrediction should be hidden when provider is None"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1089,7 +1089,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_large_file_uses_fallback(cx: &mut TestAppContext) {
|
||||
async fn test_large_file_uses_outline(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
// Create a large file that exceeds AUTO_OUTLINE_SIZE
|
||||
@@ -1101,16 +1101,16 @@ mod tests {
|
||||
|
||||
let file_context = load_context_for("file.txt", large_content, cx).await;
|
||||
|
||||
// Should contain some of the actual file content
|
||||
assert!(
|
||||
file_context.text.contains(LINE),
|
||||
"Should contain some of the file content"
|
||||
file_context
|
||||
.text
|
||||
.contains(&format!("# File outline for {}", path!("test/file.txt"))),
|
||||
"Large files should not get an outline"
|
||||
);
|
||||
|
||||
// Should be much smaller than original
|
||||
assert!(
|
||||
file_context.text.len() < content_len / 10,
|
||||
"Should be significantly smaller than original content"
|
||||
file_context.text.len() < content_len,
|
||||
"Outline should be smaller than original content"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -278,8 +278,6 @@ impl ContextPickerCompletionProvider {
|
||||
icon_path: Some(mode.icon().path().into()),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
// This ensures that when a user accepts this completion, the
|
||||
// completion menu will still be shown after "@category " is
|
||||
@@ -388,8 +386,6 @@ impl ContextPickerCompletionProvider {
|
||||
icon_path: Some(action.icon().path().into()),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
// This ensures that when a user accepts this completion, the
|
||||
// completion menu will still be shown after "@category " is
|
||||
@@ -421,8 +417,6 @@ impl ContextPickerCompletionProvider {
|
||||
replace_range: source_range.clone(),
|
||||
new_text,
|
||||
label: CodeLabel::plain(thread_entry.title().to_string(), None),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
documentation: None,
|
||||
insert_text_mode: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
@@ -490,8 +484,6 @@ impl ContextPickerCompletionProvider {
|
||||
replace_range: source_range.clone(),
|
||||
new_text,
|
||||
label: CodeLabel::plain(rules.title.to_string(), None),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
documentation: None,
|
||||
insert_text_mode: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
@@ -532,8 +524,6 @@ impl ContextPickerCompletionProvider {
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(IconName::ToolWeb.path().into()),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
confirm: Some(confirm_completion_callback(
|
||||
IconName::ToolWeb.path().into(),
|
||||
@@ -622,8 +612,6 @@ impl ContextPickerCompletionProvider {
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(completion_icon_path),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
confirm: Some(confirm_completion_callback(
|
||||
crease_icon_path,
|
||||
@@ -701,8 +689,6 @@ impl ContextPickerCompletionProvider {
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(IconName::Code.path().into()),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
confirm: Some(confirm_completion_callback(
|
||||
IconName::Code.path().into(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{cmp::Reverse, sync::Arc};
|
||||
|
||||
use collections::IndexMap;
|
||||
use collections::{HashSet, IndexMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
|
||||
use language_model::{
|
||||
@@ -57,7 +57,7 @@ fn all_models(cx: &App) -> GroupedModels {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let all = providers
|
||||
let other = providers
|
||||
.iter()
|
||||
.flat_map(|provider| {
|
||||
provider
|
||||
@@ -70,7 +70,7 @@ fn all_models(cx: &App) -> GroupedModels {
|
||||
})
|
||||
.collect();
|
||||
|
||||
GroupedModels::new(all, recommended)
|
||||
GroupedModels::new(other, recommended)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -210,24 +210,33 @@ impl LanguageModelPickerDelegate {
|
||||
|
||||
struct GroupedModels {
|
||||
recommended: Vec<ModelInfo>,
|
||||
all: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>,
|
||||
other: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>,
|
||||
}
|
||||
|
||||
impl GroupedModels {
|
||||
pub fn new(all: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
|
||||
let mut all_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
|
||||
for model in all {
|
||||
pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
|
||||
let recommended_ids = recommended
|
||||
.iter()
|
||||
.map(|info| (info.model.provider_id(), info.model.id()))
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
|
||||
for model in other {
|
||||
if recommended_ids.contains(&(model.model.provider_id(), model.model.id())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let provider = model.model.provider_id();
|
||||
if let Some(models) = all_by_provider.get_mut(&provider) {
|
||||
if let Some(models) = other_by_provider.get_mut(&provider) {
|
||||
models.push(model);
|
||||
} else {
|
||||
all_by_provider.insert(provider, vec![model]);
|
||||
other_by_provider.insert(provider, vec![model]);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
recommended,
|
||||
all: all_by_provider,
|
||||
other: other_by_provider,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +252,7 @@ impl GroupedModels {
|
||||
);
|
||||
}
|
||||
|
||||
for models in self.all.values() {
|
||||
for models in self.other.values() {
|
||||
if models.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -258,6 +267,20 @@ impl GroupedModels {
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
fn model_infos(&self) -> Vec<ModelInfo> {
|
||||
let other = self
|
||||
.other
|
||||
.values()
|
||||
.flat_map(|model| model.iter())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
self.recommended
|
||||
.iter()
|
||||
.chain(&other)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
enum LanguageModelPickerEntry {
|
||||
@@ -402,9 +425,8 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let available_models = all_models
|
||||
.all
|
||||
.values()
|
||||
.flat_map(|models| models.iter())
|
||||
.model_infos()
|
||||
.iter()
|
||||
.filter(|m| configured_provider_ids.contains(&m.model.provider_id()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
@@ -492,15 +514,17 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.start_slot(
|
||||
Icon::new(model_info.icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pl_0p5()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(model_info.icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.w(px(240.))
|
||||
.child(Label::new(model_info.model.name().0).truncate()),
|
||||
)
|
||||
.end_slot(div().pr_3().when(is_selected, |this| {
|
||||
@@ -740,52 +764,46 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_recommended_models_also_appear_in_other(_cx: &mut TestAppContext) {
|
||||
fn test_exclude_recommended_models(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models(vec![
|
||||
("zed", "claude"), // Should also appear in "other"
|
||||
("zed", "claude"), // Should be filtered out from "other"
|
||||
("zed", "gemini"),
|
||||
("copilot", "o3"),
|
||||
]);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
|
||||
let actual_all_models = grouped_models
|
||||
.all
|
||||
let actual_other_models = grouped_models
|
||||
.other
|
||||
.values()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Recommended models should also appear in "all"
|
||||
assert_models_eq(
|
||||
actual_all_models,
|
||||
vec!["zed/claude", "zed/gemini", "copilot/o3"],
|
||||
);
|
||||
// Recommended models should not appear in "other"
|
||||
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/o3"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_models_from_different_providers(_cx: &mut TestAppContext) {
|
||||
fn test_dont_exclude_models_from_other_providers(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models(vec![
|
||||
("zed", "claude"), // Should also appear in "other"
|
||||
("zed", "claude"), // Should be filtered out from "other"
|
||||
("zed", "gemini"),
|
||||
("copilot", "claude"), // Different provider, should appear in "other"
|
||||
("copilot", "claude"), // Should not be filtered out from "other"
|
||||
]);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
|
||||
let actual_all_models = grouped_models
|
||||
.all
|
||||
let actual_other_models = grouped_models
|
||||
.other
|
||||
.values()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// All models should appear in "all" regardless of recommended status
|
||||
assert_models_eq(
|
||||
actual_all_models,
|
||||
vec!["zed/claude", "zed/gemini", "copilot/claude"],
|
||||
);
|
||||
// Recommended models should not appear in "other"
|
||||
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/claude"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,8 +127,6 @@ impl SlashCommandCompletionProvider {
|
||||
new_text,
|
||||
label: command.label(cx),
|
||||
icon_path: None,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
insert_text_mode: None,
|
||||
confirm,
|
||||
source: CompletionSource::Custom,
|
||||
@@ -234,8 +232,6 @@ impl SlashCommandCompletionProvider {
|
||||
icon_path: None,
|
||||
new_text,
|
||||
documentation: None,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
confirm,
|
||||
insert_text_mode: None,
|
||||
source: CompletionSource::Custom,
|
||||
|
||||
@@ -1679,7 +1679,7 @@ impl TextThreadEditor {
|
||||
) {
|
||||
cx.stop_propagation();
|
||||
|
||||
let mut images = if let Some(item) = cx.read_from_clipboard() {
|
||||
let images = if let Some(item) = cx.read_from_clipboard() {
|
||||
item.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::Image(image) = entry {
|
||||
@@ -1693,40 +1693,6 @@ impl TextThreadEditor {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if let Some(paths) = cx.read_from_clipboard() {
|
||||
for path in paths
|
||||
.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::ExternalPaths(paths) = entry {
|
||||
Some(paths.paths().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
{
|
||||
let Ok(content) = std::fs::read(path) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(format) = image::guess_format(&content) else {
|
||||
continue;
|
||||
};
|
||||
images.push(gpui::Image::from_bytes(
|
||||
match format {
|
||||
image::ImageFormat::Png => gpui::ImageFormat::Png,
|
||||
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
|
||||
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
|
||||
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
|
||||
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
|
||||
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
|
||||
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
|
||||
_ => continue,
|
||||
},
|
||||
content,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = if let Some(item) = cx.read_from_clipboard() {
|
||||
item.entries().first().and_then(|entry| {
|
||||
if let ClipboardEntry::String(text) = entry {
|
||||
@@ -2626,11 +2592,12 @@ impl SearchableItem for TextThreadEditor {
|
||||
&mut self,
|
||||
index: usize,
|
||||
matches: &[Self::Match],
|
||||
collapse: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.activate_match(index, matches, window, cx);
|
||||
editor.activate_match(index, matches, collapse, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -205,9 +205,13 @@ impl PasswordProxy {
|
||||
} else {
|
||||
ShellKind::Posix
|
||||
};
|
||||
let askpass_program = ASKPASS_PROGRAM.get_or_init(|| current_exec);
|
||||
let askpass_program = ASKPASS_PROGRAM
|
||||
.get_or_init(|| current_exec)
|
||||
.try_shell_safe(shell_kind)
|
||||
.context("Failed to shell-escape Askpass program path.")?
|
||||
.to_string();
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = generate_askpass_script(shell_kind, askpass_program, &askpass_socket)?;
|
||||
let askpass_script = generate_askpass_script(&askpass_program, &askpass_socket);
|
||||
let _task = executor.spawn(async move {
|
||||
maybe!(async move {
|
||||
let listener =
|
||||
@@ -330,51 +334,23 @@ pub fn set_askpass_program(path: std::path::PathBuf) {
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn generate_askpass_script(
|
||||
shell_kind: ShellKind,
|
||||
askpass_program: &std::path::Path,
|
||||
askpass_socket: &std::path::Path,
|
||||
) -> Result<String> {
|
||||
let askpass_program = shell_kind.prepend_command_prefix(
|
||||
askpass_program
|
||||
.to_str()
|
||||
.context("Askpass program is on a non-utf8 path")?,
|
||||
);
|
||||
let askpass_program = shell_kind
|
||||
.try_quote_prefix_aware(&askpass_program)
|
||||
.context("Failed to shell-escape Askpass program path")?;
|
||||
let askpass_socket = askpass_socket
|
||||
.try_shell_safe(shell_kind)
|
||||
.context("Failed to shell-escape Askpass socket path")?;
|
||||
let print_args = "printf '%s\\0' \"$@\"";
|
||||
let shebang = "#!/bin/sh";
|
||||
Ok(format!(
|
||||
fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
|
||||
format!(
|
||||
"{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
))
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn generate_askpass_script(
|
||||
shell_kind: ShellKind,
|
||||
askpass_program: &std::path::Path,
|
||||
askpass_socket: &std::path::Path,
|
||||
) -> Result<String> {
|
||||
let askpass_program = shell_kind.prepend_command_prefix(
|
||||
askpass_program
|
||||
.to_str()
|
||||
.context("Askpass program is on a non-utf8 path")?,
|
||||
);
|
||||
let askpass_program = shell_kind
|
||||
.try_quote_prefix_aware(&askpass_program)
|
||||
.context("Failed to shell-escape Askpass program path")?;
|
||||
let askpass_socket = askpass_socket
|
||||
.try_shell_safe(shell_kind)
|
||||
.context("Failed to shell-escape Askpass socket path")?;
|
||||
Ok(format!(
|
||||
fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
|
||||
format!(
|
||||
r#"
|
||||
$ErrorActionPreference = 'Stop';
|
||||
($args -join [char]0) | & {askpass_program} --askpass={askpass_socket} 2> $null
|
||||
"#,
|
||||
))
|
||||
askpass_socket = askpass_socket.display(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ use paths::remote_servers_dir;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{RegisterSetting, Settings, SettingsStore};
|
||||
use smol::fs::File;
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
use smol::{fs::File, process::Command};
|
||||
use std::mem;
|
||||
use std::{
|
||||
env::{
|
||||
@@ -23,7 +23,6 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::command::new_smol_command;
|
||||
use workspace::Workspace;
|
||||
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
@@ -122,7 +121,7 @@ impl Drop for MacOsUnmounter<'_> {
|
||||
let mount_path = mem::take(&mut self.mount_path);
|
||||
self.background_executor
|
||||
.spawn(async move {
|
||||
let unmount_output = new_smol_command("hdiutil")
|
||||
let unmount_output = Command::new("hdiutil")
|
||||
.args(["detach", "-force"])
|
||||
.arg(&mount_path)
|
||||
.output()
|
||||
@@ -351,7 +350,8 @@ impl AutoUpdater {
|
||||
|
||||
pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
if cfg!(target_os = "windows") {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use util::ResultExt;
|
||||
|
||||
cleanup_windows()
|
||||
@@ -800,7 +800,7 @@ async fn install_release_linux(
|
||||
.await
|
||||
.context("failed to create directory into which to extract update")?;
|
||||
|
||||
let output = new_smol_command("tar")
|
||||
let output = Command::new("tar")
|
||||
.arg("-xzf")
|
||||
.arg(&downloaded_tar_gz)
|
||||
.arg("-C")
|
||||
@@ -835,7 +835,7 @@ async fn install_release_linux(
|
||||
to = PathBuf::from(prefix);
|
||||
}
|
||||
|
||||
let output = new_smol_command("rsync")
|
||||
let output = Command::new("rsync")
|
||||
.args(["-av", "--delete"])
|
||||
.arg(&from)
|
||||
.arg(&to)
|
||||
@@ -867,7 +867,7 @@ async fn install_release_macos(
|
||||
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
|
||||
|
||||
mounted_app_path.push("/");
|
||||
let output = new_smol_command("hdiutil")
|
||||
let output = Command::new("hdiutil")
|
||||
.args(["attach", "-nobrowse"])
|
||||
.arg(&downloaded_dmg)
|
||||
.arg("-mountroot")
|
||||
@@ -887,7 +887,7 @@ async fn install_release_macos(
|
||||
background_executor: cx.background_executor(),
|
||||
};
|
||||
|
||||
let output = new_smol_command("rsync")
|
||||
let output = Command::new("rsync")
|
||||
.args(["-av", "--delete"])
|
||||
.arg(&mounted_app_path)
|
||||
.arg(&running_app_path)
|
||||
@@ -903,22 +903,34 @@ async fn install_release_macos(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn cleanup_windows() -> Result<()> {
|
||||
use util::ResultExt;
|
||||
|
||||
let parent = std::env::current_exe()?
|
||||
.parent()
|
||||
.context("No parent dir for Zed.exe")?
|
||||
.to_owned();
|
||||
|
||||
// keep in sync with crates/auto_update_helper/src/updater.rs
|
||||
_ = smol::fs::remove_dir(parent.join("updates")).await;
|
||||
_ = smol::fs::remove_dir(parent.join("install")).await;
|
||||
_ = smol::fs::remove_dir(parent.join("old")).await;
|
||||
smol::fs::remove_dir(parent.join("updates"))
|
||||
.await
|
||||
.context("failed to remove updates dir")
|
||||
.log_err();
|
||||
smol::fs::remove_dir(parent.join("install"))
|
||||
.await
|
||||
.context("failed to remove install dir")
|
||||
.log_err();
|
||||
smol::fs::remove_dir(parent.join("old"))
|
||||
.await
|
||||
.context("failed to remove old version dir")
|
||||
.log_err();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
|
||||
let output = new_smol_command(downloaded_installer)
|
||||
let output = Command::new(downloaded_installer)
|
||||
.arg("/verysilent")
|
||||
.arg("/update=true")
|
||||
.arg("!desktopicon")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
cell::LazyCell,
|
||||
path::Path,
|
||||
sync::LazyLock,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
@@ -13,8 +13,8 @@ use windows::Win32::{
|
||||
use crate::windows_impl::WM_JOB_UPDATED;
|
||||
|
||||
pub(crate) struct Job {
|
||||
pub apply: Box<dyn Fn(&Path) -> Result<()> + Send + Sync>,
|
||||
pub rollback: Box<dyn Fn(&Path) -> Result<()> + Send + Sync>,
|
||||
pub apply: Box<dyn Fn(&Path) -> Result<()>>,
|
||||
pub rollback: Box<dyn Fn(&Path) -> Result<()>>,
|
||||
}
|
||||
|
||||
impl Job {
|
||||
@@ -154,8 +154,10 @@ impl Job {
|
||||
}
|
||||
}
|
||||
|
||||
// app is single threaded
|
||||
#[cfg(not(test))]
|
||||
pub(crate) static JOBS: LazyLock<[Job; 22]> = LazyLock::new(|| {
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
pub(crate) const JOBS: LazyCell<[Job; 22]> = LazyCell::new(|| {
|
||||
fn p(value: &str) -> &Path {
|
||||
Path::new(value)
|
||||
}
|
||||
@@ -204,8 +206,10 @@ pub(crate) static JOBS: LazyLock<[Job; 22]> = LazyLock::new(|| {
|
||||
]
|
||||
});
|
||||
|
||||
// app is single threaded
|
||||
#[cfg(test)]
|
||||
pub(crate) static JOBS: LazyLock<[Job; 9]> = LazyLock::new(|| {
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
pub(crate) const JOBS: LazyCell<[Job; 9]> = LazyCell::new(|| {
|
||||
fn p(value: &str) -> &Path {
|
||||
Path::new(value)
|
||||
}
|
||||
|
||||
@@ -1683,9 +1683,7 @@ impl LiveKitRoom {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum LocalTrack<Stream: ?Sized> {
|
||||
#[default]
|
||||
None,
|
||||
Pending {
|
||||
publish_id: usize,
|
||||
@@ -1696,6 +1694,12 @@ enum LocalTrack<Stream: ?Sized> {
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Default for LocalTrack<T> {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum RoomStatus {
|
||||
Online,
|
||||
|
||||
@@ -293,11 +293,10 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn metrics_enabled(self: &Arc<Self>) -> bool {
|
||||
self.state.lock().settings.metrics
|
||||
}
|
||||
|
||||
pub fn diagnostics_enabled(self: &Arc<Self>) -> bool {
|
||||
self.state.lock().settings.diagnostics
|
||||
let state = self.state.lock();
|
||||
let enabled = state.settings.metrics;
|
||||
drop(state);
|
||||
enabled
|
||||
}
|
||||
|
||||
pub fn set_authenticated_user_info(
|
||||
|
||||
@@ -267,7 +267,6 @@ impl UserStore {
|
||||
Status::SignedOut => {
|
||||
current_user_tx.send(None).await.ok();
|
||||
this.update(cx, |this, cx| {
|
||||
this.clear_plan_and_usage();
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
cx.notify();
|
||||
this.clear_contacts()
|
||||
@@ -780,12 +779,6 @@ impl UserStore {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear_plan_and_usage(&mut self) {
|
||||
self.plan_info = None;
|
||||
self.model_request_usage = None;
|
||||
self.edit_prediction_usage = None;
|
||||
}
|
||||
|
||||
fn update_authenticated_user(
|
||||
&mut self,
|
||||
response: GetAuthenticatedUserResponse,
|
||||
|
||||
@@ -51,11 +51,3 @@ pub fn external_agents_docs(cx: &App) -> String {
|
||||
server_url = server_url(cx)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the URL to Zed agent servers documentation.
|
||||
pub fn agent_server_docs(cx: &App) -> String {
|
||||
format!(
|
||||
"{server_url}/docs/extensions/agent-servers",
|
||||
server_url = server_url(cx)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,9 +58,6 @@ pub const SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str =
|
||||
/// The name of the header used by the client to indicate that it supports receiving xAI models.
|
||||
pub const CLIENT_SUPPORTS_X_AI_HEADER_NAME: &str = "x-zed-client-supports-x-ai";
|
||||
|
||||
/// The maximum number of edit predictions that can be rejected per request.
|
||||
pub const MAX_EDIT_PREDICTION_REJECTIONS_PER_REQUEST: usize = 100;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UsageLimit {
|
||||
@@ -195,17 +192,6 @@ pub struct AcceptEditPredictionBody {
|
||||
pub request_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RejectEditPredictionsBody {
|
||||
pub rejections: Vec<EditPredictionRejection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EditPredictionRejection {
|
||||
pub request_id: String,
|
||||
pub was_shown: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionMode {
|
||||
|
||||
@@ -76,10 +76,6 @@ pub enum PromptFormat {
|
||||
OldTextNewText,
|
||||
/// Prompt format intended for use via zeta_cli
|
||||
OnlySnippets,
|
||||
/// One-sentence instructions used in fine-tuned models
|
||||
Minimal,
|
||||
/// One-sentence instructions + FIM-like template
|
||||
MinimalQwen,
|
||||
}
|
||||
|
||||
impl PromptFormat {
|
||||
@@ -106,8 +102,6 @@ impl std::fmt::Display for PromptFormat {
|
||||
PromptFormat::OnlySnippets => write!(f, "Only Snippets"),
|
||||
PromptFormat::NumLinesUniDiff => write!(f, "Numbered Lines / Unified Diff"),
|
||||
PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
|
||||
PromptFormat::Minimal => write!(f, "Minimal"),
|
||||
PromptFormat::MinimalQwen => write!(f, "Minimal + Qwen FIM"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@ pub mod retrieval_prompt;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use cloud_llm_client::predict_edits_v3::{
|
||||
self, DiffPathFmt, Event, Excerpt, IncludedFile, Line, Point, PromptFormat,
|
||||
ReferencedDeclaration,
|
||||
self, DiffPathFmt, Excerpt, Line, Point, PromptFormat, ReferencedDeclaration,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use ordered_float::OrderedFloat;
|
||||
@@ -32,7 +31,7 @@ const MARKED_EXCERPT_INSTRUCTIONS: &str = indoc! {"
|
||||
|
||||
Other code is provided for context, and `…` indicates when code has been skipped.
|
||||
|
||||
## Edit History
|
||||
# Edit History:
|
||||
|
||||
"};
|
||||
|
||||
@@ -50,7 +49,7 @@ const LABELED_SECTIONS_INSTRUCTIONS: &str = indoc! {r#"
|
||||
println!("{i}");
|
||||
}
|
||||
|
||||
## Edit History
|
||||
# Edit History:
|
||||
|
||||
"#};
|
||||
|
||||
@@ -87,13 +86,6 @@ const NUMBERED_LINES_INSTRUCTIONS: &str = indoc! {r#"
|
||||
|
||||
"#};
|
||||
|
||||
const STUDENT_MODEL_INSTRUCTIONS: &str = indoc! {r#"
|
||||
You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.
|
||||
|
||||
## Edit History
|
||||
|
||||
"#};
|
||||
|
||||
const UNIFIED_DIFF_REMINDER: &str = indoc! {"
|
||||
---
|
||||
|
||||
@@ -108,27 +100,18 @@ const UNIFIED_DIFF_REMINDER: &str = indoc! {"
|
||||
to uniquely identify it amongst all excerpts of code provided.
|
||||
"};
|
||||
|
||||
const MINIMAL_PROMPT_REMINDER: &str = indoc! {"
|
||||
---
|
||||
|
||||
Please analyze the edit history and the files, then provide the unified diff for your predicted edits.
|
||||
Do not include the cursor marker in your output.
|
||||
If you're editing multiple files, be sure to reflect filename in the hunk's header.
|
||||
"};
|
||||
|
||||
const XML_TAGS_INSTRUCTIONS: &str = indoc! {r#"
|
||||
# Instructions
|
||||
|
||||
You are an edit prediction agent in a code editor.
|
||||
Your job is to predict the next edit that the user will make,
|
||||
based on their last few edits and their current cursor location.
|
||||
|
||||
Analyze the history of edits made by the user in order to infer what they are currently trying to accomplish.
|
||||
Then complete the remainder of the current change if it is incomplete, or predict the next edit the user intends to make.
|
||||
Always continue along the user's current trajectory, rather than changing course.
|
||||
# Output Format
|
||||
|
||||
## Output Format
|
||||
|
||||
You should briefly explain your understanding of the user's overall goal in one sentence, then explain what the next change
|
||||
along the users current trajectory will be in another, and finally specify the next edit using the following XML-like format:
|
||||
You must briefly explain your understanding of the user's goal, in one
|
||||
or two sentences, and then specify their next edit, using the following
|
||||
XML format:
|
||||
|
||||
<edits path="my-project/src/myapp/cli.py">
|
||||
<old_text>
|
||||
@@ -154,34 +137,20 @@ const XML_TAGS_INSTRUCTIONS: &str = indoc! {r#"
|
||||
- Always close all tags properly
|
||||
- Don't include the <|user_cursor|> marker in your output.
|
||||
|
||||
## Edit History
|
||||
# Edit History:
|
||||
|
||||
"#};
|
||||
|
||||
const OLD_TEXT_NEW_TEXT_REMINDER: &str = indoc! {r#"
|
||||
---
|
||||
|
||||
Remember that the edits in the edit history have already been applied.
|
||||
Remember that the edits in the edit history have already been deployed.
|
||||
The files are currently as shown in the Code Excerpts section.
|
||||
"#};
|
||||
|
||||
pub fn build_prompt(
|
||||
request: &predict_edits_v3::PredictEditsRequest,
|
||||
) -> Result<(String, SectionLabels)> {
|
||||
let mut section_labels = Default::default();
|
||||
|
||||
match request.prompt_format {
|
||||
PromptFormat::MinimalQwen => {
|
||||
let prompt = MinimalQwenPrompt {
|
||||
events: request.events.clone(),
|
||||
cursor_point: request.cursor_point,
|
||||
cursor_path: request.excerpt_path.clone(),
|
||||
included_files: request.included_files.clone(),
|
||||
};
|
||||
return Ok((prompt.render(), section_labels));
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
let mut insertions = match request.prompt_format {
|
||||
PromptFormat::MarkedExcerpt => vec![
|
||||
(
|
||||
@@ -202,12 +171,10 @@ pub fn build_prompt(
|
||||
],
|
||||
PromptFormat::LabeledSections
|
||||
| PromptFormat::NumLinesUniDiff
|
||||
| PromptFormat::Minimal
|
||||
| PromptFormat::OldTextNewText => {
|
||||
vec![(request.cursor_point, CURSOR_MARKER)]
|
||||
}
|
||||
PromptFormat::OnlySnippets => vec![],
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
};
|
||||
|
||||
let mut prompt = match request.prompt_format {
|
||||
@@ -216,59 +183,32 @@ pub fn build_prompt(
|
||||
PromptFormat::NumLinesUniDiff => NUMBERED_LINES_INSTRUCTIONS.to_string(),
|
||||
PromptFormat::OldTextNewText => XML_TAGS_INSTRUCTIONS.to_string(),
|
||||
PromptFormat::OnlySnippets => String::new(),
|
||||
PromptFormat::Minimal => STUDENT_MODEL_INSTRUCTIONS.to_string(),
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
};
|
||||
|
||||
if request.events.is_empty() {
|
||||
prompt.push_str("(No edit history)\n\n");
|
||||
} else {
|
||||
let edit_preamble = if request.prompt_format == PromptFormat::Minimal {
|
||||
"The following are the latest edits made by the user, from earlier to later.\n\n"
|
||||
} else {
|
||||
"Here are the latest edits made by the user, from earlier to later.\n\n"
|
||||
};
|
||||
prompt.push_str(edit_preamble);
|
||||
prompt.push_str("Here are the latest edits made by the user, from earlier to later.\n\n");
|
||||
push_events(&mut prompt, &request.events);
|
||||
}
|
||||
|
||||
let excerpts_preamble = match request.prompt_format {
|
||||
PromptFormat::Minimal => indoc! {"
|
||||
## Part of the file under the cursor
|
||||
prompt.push_str(indoc! {"
|
||||
# Code Excerpts
|
||||
|
||||
(The cursor marker <|user_cursor|> indicates the current user cursor position.
|
||||
The file is in current state, edits from edit history has been applied.
|
||||
We only show part of the file around the cursor.
|
||||
You can only edit exactly this part of the file.
|
||||
We prepend line numbers (e.g., `123|<actual line>`); they are not part of the file.)
|
||||
"},
|
||||
PromptFormat::NumLinesUniDiff | PromptFormat::OldTextNewText => indoc! {"
|
||||
## Code Excerpts
|
||||
The cursor marker <|user_cursor|> indicates the current user cursor position.
|
||||
The file is in current state, edits from edit history have been applied.
|
||||
"});
|
||||
|
||||
Here is some excerpts of code that you should take into account to predict the next edit.
|
||||
if request.prompt_format == PromptFormat::NumLinesUniDiff {
|
||||
prompt.push_str(indoc! {"
|
||||
We prepend line numbers (e.g., `123|<actual line>`); they are not part of the file.
|
||||
"});
|
||||
}
|
||||
|
||||
The cursor position is marked by `<|user_cursor|>` as it stands after the last edit in the history.
|
||||
|
||||
In addition other excerpts are included to better understand what the edit will be, including the declaration
|
||||
or references of symbols around the cursor, or other similar code snippets that may need to be updated
|
||||
following patterns that appear in the edit history.
|
||||
|
||||
Consider each of them carefully in relation to the edit history, and that the user may not have navigated
|
||||
to the next place they want to edit yet.
|
||||
|
||||
Lines starting with `…` indicate omitted line ranges. These may appear inside multi-line code constructs.
|
||||
"},
|
||||
_ => indoc! {"
|
||||
## Code Excerpts
|
||||
|
||||
The cursor marker <|user_cursor|> indicates the current user cursor position.
|
||||
The file is in current state, edits from edit history have been applied.
|
||||
"},
|
||||
};
|
||||
|
||||
prompt.push_str(excerpts_preamble);
|
||||
prompt.push('\n');
|
||||
|
||||
let mut section_labels = Default::default();
|
||||
|
||||
if !request.referenced_declarations.is_empty() || !request.signatures.is_empty() {
|
||||
let syntax_based_prompt = SyntaxBasedPrompt::populate(request)?;
|
||||
section_labels = syntax_based_prompt.write(&mut insertions, &mut prompt)?;
|
||||
@@ -277,38 +217,19 @@ pub fn build_prompt(
|
||||
anyhow::bail!("PromptFormat::LabeledSections cannot be used with ContextMode::Llm");
|
||||
}
|
||||
|
||||
let include_line_numbers = matches!(
|
||||
request.prompt_format,
|
||||
PromptFormat::NumLinesUniDiff | PromptFormat::Minimal
|
||||
);
|
||||
for related_file in &request.included_files {
|
||||
if request.prompt_format == PromptFormat::Minimal {
|
||||
write_codeblock_with_filename(
|
||||
&related_file.path,
|
||||
&related_file.excerpts,
|
||||
if related_file.path == request.excerpt_path {
|
||||
&insertions
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut prompt,
|
||||
);
|
||||
} else {
|
||||
write_codeblock(
|
||||
&related_file.path,
|
||||
&related_file.excerpts,
|
||||
if related_file.path == request.excerpt_path {
|
||||
&insertions
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut prompt,
|
||||
);
|
||||
}
|
||||
write_codeblock(
|
||||
&related_file.path,
|
||||
&related_file.excerpts,
|
||||
if related_file.path == request.excerpt_path {
|
||||
&insertions
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
related_file.max_row,
|
||||
request.prompt_format == PromptFormat::NumLinesUniDiff,
|
||||
&mut prompt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,9 +240,6 @@ pub fn build_prompt(
|
||||
PromptFormat::OldTextNewText => {
|
||||
prompt.push_str(OLD_TEXT_NEW_TEXT_REMINDER);
|
||||
}
|
||||
PromptFormat::Minimal => {
|
||||
prompt.push_str(MINIMAL_PROMPT_REMINDER);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -337,27 +255,6 @@ pub fn write_codeblock<'a>(
|
||||
output: &'a mut String,
|
||||
) {
|
||||
writeln!(output, "`````{}", DiffPathFmt(path)).unwrap();
|
||||
|
||||
write_excerpts(
|
||||
excerpts,
|
||||
sorted_insertions,
|
||||
file_line_count,
|
||||
include_line_numbers,
|
||||
output,
|
||||
);
|
||||
write!(output, "`````\n\n").unwrap();
|
||||
}
|
||||
|
||||
fn write_codeblock_with_filename<'a>(
|
||||
path: &Path,
|
||||
excerpts: impl IntoIterator<Item = &'a Excerpt>,
|
||||
sorted_insertions: &[(Point, &str)],
|
||||
file_line_count: Line,
|
||||
include_line_numbers: bool,
|
||||
output: &'a mut String,
|
||||
) {
|
||||
writeln!(output, "`````filename={}", DiffPathFmt(path)).unwrap();
|
||||
|
||||
write_excerpts(
|
||||
excerpts,
|
||||
sorted_insertions,
|
||||
@@ -769,7 +666,6 @@ impl<'a> SyntaxBasedPrompt<'a> {
|
||||
PromptFormat::MarkedExcerpt
|
||||
| PromptFormat::OnlySnippets
|
||||
| PromptFormat::OldTextNewText
|
||||
| PromptFormat::Minimal
|
||||
| PromptFormat::NumLinesUniDiff => {
|
||||
if range.start.0 > 0 && !skipped_last_snippet {
|
||||
output.push_str("…\n");
|
||||
@@ -785,7 +681,6 @@ impl<'a> SyntaxBasedPrompt<'a> {
|
||||
writeln!(output, "<|section_{}|>", section_index).ok();
|
||||
}
|
||||
}
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
}
|
||||
|
||||
let push_full_snippet = |output: &mut String| {
|
||||
@@ -895,69 +790,3 @@ fn declaration_size(declaration: &ReferencedDeclaration, style: DeclarationStyle
|
||||
DeclarationStyle::Declaration => declaration.text.len(),
|
||||
}
|
||||
}
|
||||
|
||||
struct MinimalQwenPrompt {
|
||||
events: Vec<Event>,
|
||||
cursor_point: Point,
|
||||
cursor_path: Arc<Path>, // TODO: make a common struct with cursor_point
|
||||
included_files: Vec<IncludedFile>,
|
||||
}
|
||||
|
||||
impl MinimalQwenPrompt {
|
||||
const INSTRUCTIONS: &str = "You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.\n";
|
||||
|
||||
fn render(&self) -> String {
|
||||
let edit_history = self.fmt_edit_history();
|
||||
let context = self.fmt_context();
|
||||
|
||||
format!(
|
||||
"{instructions}\n\n{edit_history}\n\n{context}",
|
||||
instructions = MinimalQwenPrompt::INSTRUCTIONS,
|
||||
edit_history = edit_history,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
|
||||
fn fmt_edit_history(&self) -> String {
|
||||
if self.events.is_empty() {
|
||||
"(No edit history)\n\n".to_string()
|
||||
} else {
|
||||
let mut events_str = String::new();
|
||||
push_events(&mut events_str, &self.events);
|
||||
format!(
|
||||
"The following are the latest edits made by the user, from earlier to later.\n\n{}",
|
||||
events_str
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_context(&self) -> String {
|
||||
let mut context = String::new();
|
||||
let include_line_numbers = true;
|
||||
|
||||
for related_file in &self.included_files {
|
||||
writeln!(context, "<|file_sep|>{}", DiffPathFmt(&related_file.path)).unwrap();
|
||||
|
||||
if related_file.path == self.cursor_path {
|
||||
write!(context, "<|fim_prefix|>").unwrap();
|
||||
write_excerpts(
|
||||
&related_file.excerpts,
|
||||
&[(self.cursor_point, "<|fim_suffix|>")],
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut context,
|
||||
);
|
||||
writeln!(context, "<|fim_middle|>").unwrap();
|
||||
} else {
|
||||
write_excerpts(
|
||||
&related_file.excerpts,
|
||||
&[],
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut context,
|
||||
);
|
||||
}
|
||||
}
|
||||
context
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ pub fn build_prompt(request: predict_edits_v3::PlanContextRetrievalRequest) -> R
|
||||
let mut prompt = SEARCH_INSTRUCTIONS.to_string();
|
||||
|
||||
if !request.events.is_empty() {
|
||||
writeln!(&mut prompt, "\n## User Edits\n\n")?;
|
||||
writeln!(&mut prompt, "## User Edits\n")?;
|
||||
push_events(&mut prompt, &request.events);
|
||||
}
|
||||
|
||||
writeln!(&mut prompt, "## Cursor context\n")?;
|
||||
writeln!(&mut prompt, "## Cursor context")?;
|
||||
write_codeblock(
|
||||
&request.excerpt_path,
|
||||
&[Excerpt {
|
||||
@@ -44,7 +44,7 @@ pub struct SearchToolInput {
|
||||
}
|
||||
|
||||
/// Search for relevant code by path, syntax hierarchy, and content.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Hash)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct SearchToolQuery {
|
||||
/// 1. A glob pattern to match file paths in the codebase to search in.
|
||||
pub glob: String,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
drop table embeddings;
|
||||
@@ -1,4 +0,0 @@
|
||||
alter table billing_customers
|
||||
add column external_id text;
|
||||
|
||||
create unique index uix_billing_customers_on_external_id on billing_customers (external_id);
|
||||
@@ -5,6 +5,7 @@ pub mod buffers;
|
||||
pub mod channels;
|
||||
pub mod contacts;
|
||||
pub mod contributors;
|
||||
pub mod embeddings;
|
||||
pub mod extensions;
|
||||
pub mod notifications;
|
||||
pub mod projects;
|
||||
|
||||
94
crates/collab/src/db/queries/embeddings.rs
Normal file
94
crates/collab/src/db/queries/embeddings.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use super::*;
|
||||
use time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
impl Database {
|
||||
pub async fn get_embeddings(
|
||||
&self,
|
||||
model: &str,
|
||||
digests: &[Vec<u8>],
|
||||
) -> Result<HashMap<Vec<u8>, Vec<f32>>> {
|
||||
self.transaction(|tx| async move {
|
||||
let embeddings = {
|
||||
let mut db_embeddings = embedding::Entity::find()
|
||||
.filter(
|
||||
embedding::Column::Model.eq(model).and(
|
||||
embedding::Column::Digest
|
||||
.is_in(digests.iter().map(|digest| digest.as_slice())),
|
||||
),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut embeddings = HashMap::default();
|
||||
while let Some(db_embedding) = db_embeddings.next().await {
|
||||
let db_embedding = db_embedding?;
|
||||
embeddings.insert(db_embedding.digest, db_embedding.dimensions);
|
||||
}
|
||||
embeddings
|
||||
};
|
||||
|
||||
if !embeddings.is_empty() {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let retrieved_at = PrimitiveDateTime::new(now.date(), now.time());
|
||||
|
||||
embedding::Entity::update_many()
|
||||
.filter(
|
||||
embedding::Column::Digest
|
||||
.is_in(embeddings.keys().map(|digest| digest.as_slice())),
|
||||
)
|
||||
.col_expr(embedding::Column::RetrievedAt, Expr::value(retrieved_at))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(embeddings)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn save_embeddings(
|
||||
&self,
|
||||
model: &str,
|
||||
embeddings: &HashMap<Vec<u8>, Vec<f32>>,
|
||||
) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
embedding::Entity::insert_many(embeddings.iter().map(|(digest, dimensions)| {
|
||||
let now_offset_datetime = OffsetDateTime::now_utc();
|
||||
let retrieved_at =
|
||||
PrimitiveDateTime::new(now_offset_datetime.date(), now_offset_datetime.time());
|
||||
|
||||
embedding::ActiveModel {
|
||||
model: ActiveValue::set(model.to_string()),
|
||||
digest: ActiveValue::set(digest.clone()),
|
||||
dimensions: ActiveValue::set(dimensions.clone()),
|
||||
retrieved_at: ActiveValue::set(retrieved_at),
|
||||
}
|
||||
}))
|
||||
.on_conflict(
|
||||
OnConflict::columns([embedding::Column::Model, embedding::Column::Digest])
|
||||
.do_nothing()
|
||||
.to_owned(),
|
||||
)
|
||||
.exec_without_returning(&*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn purge_old_embeddings(&self) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
embedding::Entity::delete_many()
|
||||
.filter(
|
||||
embedding::Column::RetrievedAt
|
||||
.lte(OffsetDateTime::now_utc() - Duration::days(60)),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -1005,7 +1005,6 @@ impl Database {
|
||||
is_last_update: true,
|
||||
merge_message: db_repository_entry.merge_message,
|
||||
stash_entries: Vec::new(),
|
||||
renamed_paths: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,7 +796,6 @@ impl Database {
|
||||
is_last_update: true,
|
||||
merge_message: db_repository.merge_message,
|
||||
stash_entries: Vec::new(),
|
||||
renamed_paths: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod channel_chat_participant;
|
||||
pub mod channel_member;
|
||||
pub mod contact;
|
||||
pub mod contributor;
|
||||
pub mod embedding;
|
||||
pub mod extension;
|
||||
pub mod extension_version;
|
||||
pub mod follower;
|
||||
|
||||
18
crates/collab/src/db/tables/embedding.rs
Normal file
18
crates/collab/src/db/tables/embedding.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "embeddings")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub model: String,
|
||||
#[sea_orm(primary_key)]
|
||||
pub digest: Vec<u8>,
|
||||
pub dimensions: Vec<f32>,
|
||||
pub retrieved_at: PrimitiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -39,6 +39,25 @@ pub enum Relation {
|
||||
Contributor,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Returns the timestamp of when the user's account was created.
|
||||
///
|
||||
/// This will be the earlier of the `created_at` and `github_user_created_at` timestamps.
|
||||
pub fn account_created_at(&self) -> NaiveDateTime {
|
||||
let mut account_created_at = self.created_at;
|
||||
if let Some(github_created_at) = self.github_user_created_at {
|
||||
account_created_at = account_created_at.min(github_created_at);
|
||||
}
|
||||
|
||||
account_created_at
|
||||
}
|
||||
|
||||
/// Returns the age of the user's account.
|
||||
pub fn account_age(&self) -> chrono::Duration {
|
||||
chrono::Utc::now().naive_utc() - self.account_created_at()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::access_token::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::AccessToken.def()
|
||||
|
||||
@@ -2,6 +2,9 @@ mod buffer_tests;
|
||||
mod channel_tests;
|
||||
mod contributor_tests;
|
||||
mod db_tests;
|
||||
// we only run postgres tests on macos right now
|
||||
#[cfg(target_os = "macos")]
|
||||
mod embedding_tests;
|
||||
mod extension_tests;
|
||||
|
||||
use crate::migrations::run_database_migrations;
|
||||
|
||||
87
crates/collab/src/db/tests/embedding_tests.rs
Normal file
87
crates/collab/src/db/tests/embedding_tests.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use super::TestDb;
|
||||
use crate::db::embedding;
|
||||
use collections::HashMap;
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, sea_query::Expr};
|
||||
use std::ops::Sub;
|
||||
use time::{Duration, OffsetDateTime, PrimitiveDateTime};
|
||||
|
||||
// SQLite does not support array arguments, so we only test this against a real postgres instance
|
||||
#[gpui::test]
|
||||
async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = TestDb::postgres(cx.executor());
|
||||
let db = test_db.db();
|
||||
|
||||
let provider = "test_model";
|
||||
let digest1 = vec![1, 2, 3];
|
||||
let digest2 = vec![4, 5, 6];
|
||||
let embeddings = HashMap::from_iter([
|
||||
(digest1.clone(), vec![0.1, 0.2, 0.3]),
|
||||
(digest2.clone(), vec![0.4, 0.5, 0.6]),
|
||||
]);
|
||||
|
||||
// Save embeddings
|
||||
db.save_embeddings(provider, &embeddings).await.unwrap();
|
||||
|
||||
// Retrieve embeddings
|
||||
let retrieved_embeddings = db
|
||||
.get_embeddings(provider, &[digest1.clone(), digest2.clone()])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(retrieved_embeddings.len(), 2);
|
||||
assert!(retrieved_embeddings.contains_key(&digest1));
|
||||
assert!(retrieved_embeddings.contains_key(&digest2));
|
||||
|
||||
// Check if the retrieved embeddings are correct
|
||||
assert_eq!(retrieved_embeddings[&digest1], vec![0.1, 0.2, 0.3]);
|
||||
assert_eq!(retrieved_embeddings[&digest2], vec![0.4, 0.5, 0.6]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = TestDb::postgres(cx.executor());
|
||||
let db = test_db.db();
|
||||
|
||||
let model = "test_model";
|
||||
let digest = vec![7, 8, 9];
|
||||
let embeddings = HashMap::from_iter([(digest.clone(), vec![0.7, 0.8, 0.9])]);
|
||||
|
||||
// Save old embeddings
|
||||
db.save_embeddings(model, &embeddings).await.unwrap();
|
||||
|
||||
// Reach into the DB and change the retrieved at to be > 60 days
|
||||
db.transaction(|tx| {
|
||||
let digest = digest.clone();
|
||||
async move {
|
||||
let sixty_days_ago = OffsetDateTime::now_utc().sub(Duration::days(61));
|
||||
let retrieved_at = PrimitiveDateTime::new(sixty_days_ago.date(), sixty_days_ago.time());
|
||||
|
||||
embedding::Entity::update_many()
|
||||
.filter(
|
||||
embedding::Column::Model
|
||||
.eq(model)
|
||||
.and(embedding::Column::Digest.eq(digest)),
|
||||
)
|
||||
.col_expr(embedding::Column::RetrievedAt, Expr::value(retrieved_at))
|
||||
.exec(&*tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Purge old embeddings
|
||||
db.purge_old_embeddings().await.unwrap();
|
||||
|
||||
// Try to retrieve the purged embeddings
|
||||
let retrieved_embeddings = db
|
||||
.get_embeddings(model, std::slice::from_ref(&digest))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
retrieved_embeddings.is_empty(),
|
||||
"Old embeddings should have been purged"
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use collab::llm::db::LlmDatabase;
|
||||
use collab::migrations::run_database_migrations;
|
||||
use collab::{
|
||||
AppState, Config, Result, api::fetch_extensions_from_blob_store_periodically, db, env,
|
||||
executor::Executor,
|
||||
executor::Executor, rpc::ResultExt,
|
||||
};
|
||||
use db::Database;
|
||||
use std::{
|
||||
@@ -95,6 +95,8 @@ async fn main() -> Result<()> {
|
||||
let state = AppState::new(config, Executor::Production).await?;
|
||||
|
||||
if mode.is_collab() {
|
||||
state.db.purge_old_embeddings().await.trace_err();
|
||||
|
||||
let epoch = state
|
||||
.db
|
||||
.create_server(&state.config.zed_environment)
|
||||
|
||||
@@ -288,7 +288,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
@@ -307,35 +307,17 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = [
|
||||
client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
),
|
||||
client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "fake-analyzer",
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
),
|
||||
];
|
||||
client_b.language_registry().add(rust_lang());
|
||||
client_b.language_registry().register_fake_lsp_adapter(
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
client_b.language_registry().add(rust_lang());
|
||||
client_b.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "fake-analyzer",
|
||||
capabilities,
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
@@ -370,8 +352,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)
|
||||
});
|
||||
|
||||
let fake_language_server = fake_language_servers[0].next().await.unwrap();
|
||||
let second_fake_language_server = fake_language_servers[1].next().await.unwrap();
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
cx_a.background_executor.run_until_parked();
|
||||
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
@@ -433,11 +414,6 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
second_fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) })
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.executor().finish_waiting();
|
||||
|
||||
// Open the buffer on the host.
|
||||
@@ -546,10 +522,6 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
])))
|
||||
});
|
||||
|
||||
// Second language server also needs to handle the request (returns None)
|
||||
let mut second_completion_response = second_fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
|
||||
|
||||
// The completion now gets a new `text_edit.new_text` when resolving the completion item
|
||||
let mut resolve_completion_response = fake_language_server
|
||||
.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {
|
||||
@@ -573,7 +545,6 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
completion_response.next().await.unwrap();
|
||||
second_completion_response.next().await.unwrap();
|
||||
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
@@ -592,77 +563,6 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
|
||||
);
|
||||
});
|
||||
|
||||
// Ensure buffer is synced before proceeding with the next test
|
||||
cx_a.executor().run_until_parked();
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// Test completions from the second fake language server
|
||||
// Add another completion trigger to test the second language server
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([68..68])
|
||||
});
|
||||
editor.handle_input("; b", window, cx);
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b. }"
|
||||
);
|
||||
});
|
||||
|
||||
// Set up completion handlers for both language servers
|
||||
let mut first_lsp_completion = fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
|
||||
|
||||
let mut second_lsp_completion = second_fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
lsp::Position::new(1, 54),
|
||||
);
|
||||
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "analyzer_method(…)".into(),
|
||||
detail: Some("fn(&self) -> Result<T>".into()),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
new_text: "analyzer_method()".to_string(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(1, 54),
|
||||
lsp::Position::new(1, 54),
|
||||
),
|
||||
})),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// Await both language server responses
|
||||
first_lsp_completion.next().await.unwrap();
|
||||
second_lsp_completion.next().await.unwrap();
|
||||
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// Confirm the completion from the second language server works
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b.analyzer_method() }"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -2269,28 +2169,16 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
} else {
|
||||
"initial hint"
|
||||
};
|
||||
Ok(Some(vec![
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(0, character),
|
||||
label: lsp::InlayHintLabel::String(label.to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(1090, 1090),
|
||||
label: lsp::InlayHintLabel::String("out-of-bounds hint".to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
]))
|
||||
Ok(Some(vec![lsp::InlayHint {
|
||||
position: lsp::Position::new(0, character),
|
||||
label: lsp::InlayHintLabel::String(label.to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
}]))
|
||||
}
|
||||
})
|
||||
.next()
|
||||
|
||||
@@ -672,25 +672,20 @@ impl CollabPanel {
|
||||
{
|
||||
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
|
||||
}
|
||||
|
||||
let should_respect_collapse = query.is_empty();
|
||||
let mut collapse_depth = None;
|
||||
|
||||
for (idx, channel) in channels.into_iter().enumerate() {
|
||||
let depth = channel.parent_path.len();
|
||||
|
||||
if should_respect_collapse {
|
||||
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
|
||||
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
|
||||
collapse_depth = Some(depth);
|
||||
} else if let Some(collapsed_depth) = collapse_depth {
|
||||
if depth > collapsed_depth {
|
||||
continue;
|
||||
}
|
||||
if self.is_channel_collapsed(channel.id) {
|
||||
collapse_depth = Some(depth);
|
||||
} else if let Some(collapsed_depth) = collapse_depth {
|
||||
if depth > collapsed_depth {
|
||||
continue;
|
||||
}
|
||||
if self.is_channel_collapsed(channel.id) {
|
||||
collapse_depth = Some(depth);
|
||||
} else {
|
||||
collapse_depth = None;
|
||||
}
|
||||
} else {
|
||||
collapse_depth = None;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1029,11 +1029,13 @@ impl SearchableItem for DapLogView {
|
||||
&mut self,
|
||||
index: usize,
|
||||
matches: &[Self::Match],
|
||||
collapse: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.activate_match(index, matches, window, cx))
|
||||
self.editor.update(cx, |e, cx| {
|
||||
e.activate_match(index, matches, collapse, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn select_matches(
|
||||
|
||||
@@ -677,8 +677,6 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
),
|
||||
new_text: string_match.string.clone(),
|
||||
label: CodeLabel::plain(string_match.string.clone(), None),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
icon_path: None,
|
||||
documentation: Some(CompletionDocumentation::MultiLineMarkdown(
|
||||
variable_value.into(),
|
||||
@@ -792,8 +790,6 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
documentation: completion.detail.map(|detail| {
|
||||
CompletionDocumentation::MultiLineMarkdown(detail.into())
|
||||
}),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
confirm: None,
|
||||
source: project::CompletionSource::Dap { sort_text },
|
||||
insert_text_mode: None,
|
||||
|
||||
@@ -370,16 +370,11 @@ impl BufferDiagnosticsEditor {
|
||||
continue;
|
||||
}
|
||||
|
||||
let languages = buffer_diagnostics_editor
|
||||
.read_with(cx, |b, cx| b.project.read(cx).languages().clone())
|
||||
.ok();
|
||||
|
||||
let diagnostic_blocks = cx.update(|_window, cx| {
|
||||
DiagnosticRenderer::diagnostic_blocks_for_group(
|
||||
group,
|
||||
buffer_snapshot.remote_id(),
|
||||
Some(Arc::new(buffer_diagnostics_editor.clone())),
|
||||
languages,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -6,7 +6,7 @@ use editor::{
|
||||
hover_popover::diagnostics_markdown_style,
|
||||
};
|
||||
use gpui::{AppContext, Entity, Focusable, WeakEntity};
|
||||
use language::{BufferId, Diagnostic, DiagnosticEntryRef, LanguageRegistry};
|
||||
use language::{BufferId, Diagnostic, DiagnosticEntryRef};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use markdown::{Markdown, MarkdownElement};
|
||||
use settings::Settings;
|
||||
@@ -27,7 +27,6 @@ impl DiagnosticRenderer {
|
||||
diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
|
||||
buffer_id: BufferId,
|
||||
diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut App,
|
||||
) -> Vec<DiagnosticBlock> {
|
||||
let Some(primary_ix) = diagnostic_group
|
||||
@@ -76,14 +75,11 @@ impl DiagnosticRenderer {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
results.push(DiagnosticBlock {
|
||||
initial_range: primary.range.clone(),
|
||||
severity: primary.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| {
|
||||
Markdown::new(markdown.into(), language_registry.clone(), None, cx)
|
||||
}),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
});
|
||||
} else {
|
||||
if entry.range.start.row.abs_diff(primary.range.start.row) >= 5 {
|
||||
@@ -95,9 +91,7 @@ impl DiagnosticRenderer {
|
||||
initial_range: entry.range.clone(),
|
||||
severity: entry.diagnostic.severity,
|
||||
diagnostics_editor: diagnostics_editor.clone(),
|
||||
markdown: cx.new(|cx| {
|
||||
Markdown::new(markdown.into(), language_registry.clone(), None, cx)
|
||||
}),
|
||||
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -124,16 +118,9 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||
buffer_id: BufferId,
|
||||
snapshot: EditorSnapshot,
|
||||
editor: WeakEntity<Editor>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut App,
|
||||
) -> Vec<BlockProperties<Anchor>> {
|
||||
let blocks = Self::diagnostic_blocks_for_group(
|
||||
diagnostic_group,
|
||||
buffer_id,
|
||||
None,
|
||||
language_registry,
|
||||
cx,
|
||||
);
|
||||
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
|
||||
|
||||
blocks
|
||||
.into_iter()
|
||||
@@ -159,16 +146,9 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||
diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
|
||||
range: Range<Point>,
|
||||
buffer_id: BufferId,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<Markdown>> {
|
||||
let blocks = Self::diagnostic_blocks_for_group(
|
||||
diagnostic_group,
|
||||
buffer_id,
|
||||
None,
|
||||
language_registry,
|
||||
cx,
|
||||
);
|
||||
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
|
||||
blocks
|
||||
.into_iter()
|
||||
.find_map(|block| (block.initial_range == range).then(|| block.markdown))
|
||||
@@ -226,11 +206,6 @@ impl DiagnosticBlock {
|
||||
self.markdown.clone(),
|
||||
diagnostics_markdown_style(bcx.window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
copy_button_on_hover: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click({
|
||||
move |link, window, cx| {
|
||||
editor
|
||||
|
||||
@@ -73,7 +73,7 @@ pub fn init(cx: &mut App) {
|
||||
}
|
||||
|
||||
pub(crate) struct ProjectDiagnosticsEditor {
|
||||
pub project: Entity<Project>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
editor: Entity<Editor>,
|
||||
@@ -182,6 +182,7 @@ impl ProjectDiagnosticsEditor {
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
|
||||
log::debug!("disk based diagnostics finished for server {language_server_id}");
|
||||
this.close_diagnosticless_buffers(
|
||||
window,
|
||||
cx,
|
||||
this.editor.focus_handle(cx).contains_focused(window, cx)
|
||||
|| this.focus_handle.contains_focused(window, cx),
|
||||
@@ -246,10 +247,10 @@ impl ProjectDiagnosticsEditor {
|
||||
window.focus(&this.focus_handle);
|
||||
}
|
||||
}
|
||||
EditorEvent::Blurred => this.close_diagnosticless_buffers(cx, false),
|
||||
EditorEvent::Saved => this.close_diagnosticless_buffers(cx, true),
|
||||
EditorEvent::Blurred => this.close_diagnosticless_buffers(window, cx, false),
|
||||
EditorEvent::Saved => this.close_diagnosticless_buffers(window, cx, true),
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
this.close_diagnosticless_buffers(cx, true)
|
||||
this.close_diagnosticless_buffers(window, cx, true)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -297,7 +298,12 @@ impl ProjectDiagnosticsEditor {
|
||||
/// - have no diagnostics anymore
|
||||
/// - are saved (not dirty)
|
||||
/// - and, if `retain_selections` is true, do not have selections within them
|
||||
fn close_diagnosticless_buffers(&mut self, cx: &mut Context<Self>, retain_selections: bool) {
|
||||
fn close_diagnosticless_buffers(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
retain_selections: bool,
|
||||
) {
|
||||
let snapshot = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.display_snapshot(cx));
|
||||
@@ -441,7 +447,7 @@ impl ProjectDiagnosticsEditor {
|
||||
fn focus_out(&mut self, _: FocusOutEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
|
||||
{
|
||||
self.close_diagnosticless_buffers(cx, false);
|
||||
self.close_diagnosticless_buffers(window, cx, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,7 +461,8 @@ impl ProjectDiagnosticsEditor {
|
||||
});
|
||||
}
|
||||
});
|
||||
self.close_diagnosticless_buffers(cx, false);
|
||||
self.multibuffer
|
||||
.update(cx, |multibuffer, cx| multibuffer.clear(cx));
|
||||
self.project.update(cx, |project, cx| {
|
||||
self.paths_to_update = project
|
||||
.diagnostic_summaries(false, cx)
|
||||
@@ -491,7 +498,7 @@ impl ProjectDiagnosticsEditor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let was_empty = self.multibuffer.read(cx).is_empty();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let mut buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
|
||||
let max_severity = if self.include_warnings {
|
||||
@@ -545,15 +552,11 @@ impl ProjectDiagnosticsEditor {
|
||||
if group_severity.is_none_or(|s| s > max_severity) {
|
||||
continue;
|
||||
}
|
||||
let languages = this
|
||||
.read_with(cx, |t, cx| t.project.read(cx).languages().clone())
|
||||
.ok();
|
||||
let more = cx.update(|_, cx| {
|
||||
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
|
||||
group,
|
||||
buffer_snapshot.remote_id(),
|
||||
Some(diagnostics_toolbar_editor.clone()),
|
||||
languages,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -602,6 +605,7 @@ impl ProjectDiagnosticsEditor {
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
buffer_snapshot = cx.update(|_, cx| buffer.read(cx).snapshot())?;
|
||||
let initial_range = buffer_snapshot.anchor_after(b.initial_range.start)
|
||||
..buffer_snapshot.anchor_before(b.initial_range.end);
|
||||
let excerpt_range = ExcerptRange {
|
||||
@@ -1009,14 +1013,11 @@ async fn heuristic_syntactic_expand(
|
||||
snapshot: BufferSnapshot,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<RangeInclusive<BufferRow>> {
|
||||
let start = snapshot.clip_point(input_range.start, Bias::Right);
|
||||
let end = snapshot.clip_point(input_range.end, Bias::Left);
|
||||
let input_row_count = input_range.end.row - input_range.start.row;
|
||||
if input_row_count > max_row_count {
|
||||
return None;
|
||||
}
|
||||
|
||||
let input_range = start..end;
|
||||
// If the outline node contains the diagnostic and is small enough, just use that.
|
||||
let outline_range = snapshot.outline_range_containing(input_range.clone());
|
||||
if let Some(outline_range) = outline_range.clone() {
|
||||
|
||||
@@ -104,7 +104,6 @@ pub trait EditPredictionProvider: 'static + Sized {
|
||||
);
|
||||
fn accept(&mut self, cx: &mut Context<Self>);
|
||||
fn discard(&mut self, cx: &mut Context<Self>);
|
||||
fn did_show(&mut self, _cx: &mut Context<Self>) {}
|
||||
fn suggest(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
@@ -143,7 +142,6 @@ pub trait EditPredictionProviderHandle {
|
||||
direction: Direction,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn did_show(&self, cx: &mut App);
|
||||
fn accept(&self, cx: &mut App);
|
||||
fn discard(&self, cx: &mut App);
|
||||
fn suggest(
|
||||
@@ -235,10 +233,6 @@ where
|
||||
self.update(cx, |this, cx| this.discard(cx))
|
||||
}
|
||||
|
||||
fn did_show(&self, cx: &mut App) {
|
||||
self.update(cx, |this, cx| this.did_show(cx))
|
||||
}
|
||||
|
||||
fn suggest(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
|
||||
@@ -18,19 +18,18 @@ client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
codestral.workspace = true
|
||||
copilot.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
language.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
settings.workspace = true
|
||||
supermaven.workspace = true
|
||||
sweep_ai.workspace = true
|
||||
telemetry.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -18,15 +18,12 @@ use language::{
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use regex::Regex;
|
||||
use settings::{
|
||||
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME, Settings, SettingsStore, update_settings_file,
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use std::{
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
use supermaven::{AccountStatus, Supermaven};
|
||||
use sweep_ai::SweepFeatureFlag;
|
||||
use ui::{
|
||||
Clickable, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton,
|
||||
IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
|
||||
@@ -46,8 +43,7 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
const COPILOT_SETTINGS_PATH: &str = "/settings/copilot";
|
||||
const COPILOT_SETTINGS_URL: &str = concat!("https://github.com", "/settings/copilot");
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security";
|
||||
|
||||
struct CopilotErrorToast;
|
||||
@@ -81,7 +77,7 @@ impl Render for EditPredictionButton {
|
||||
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
|
||||
match &all_language_settings.edit_predictions.provider {
|
||||
match all_language_settings.edit_predictions.provider {
|
||||
EditPredictionProvider::None => div().hidden(),
|
||||
|
||||
EditPredictionProvider::Copilot => {
|
||||
@@ -132,21 +128,20 @@ impl Render for EditPredictionButton {
|
||||
}),
|
||||
);
|
||||
}
|
||||
let this = cx.weak_entity();
|
||||
let this = cx.entity();
|
||||
|
||||
div().child(
|
||||
PopoverMenu::new("copilot")
|
||||
.menu(move |window, cx| {
|
||||
let current_status = Copilot::global(cx)?.read(cx).status();
|
||||
match current_status {
|
||||
Some(match current_status {
|
||||
Status::Authorized => this.update(cx, |this, cx| {
|
||||
this.build_copilot_context_menu(window, cx)
|
||||
}),
|
||||
_ => this.update(cx, |this, cx| {
|
||||
this.build_copilot_start_menu(window, cx)
|
||||
}),
|
||||
}
|
||||
.ok()
|
||||
})
|
||||
})
|
||||
.anchor(Corner::BottomRight)
|
||||
.trigger_with_tooltip(
|
||||
@@ -187,7 +182,7 @@ impl Render for EditPredictionButton {
|
||||
let icon = status.to_icon();
|
||||
let tooltip_text = status.to_tooltip();
|
||||
let has_menu = status.has_menu();
|
||||
let this = cx.weak_entity();
|
||||
let this = cx.entity();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
div().child(
|
||||
@@ -214,11 +209,9 @@ impl Render for EditPredictionButton {
|
||||
)
|
||||
}))
|
||||
}
|
||||
SupermavenButtonStatus::Ready => this
|
||||
.update(cx, |this, cx| {
|
||||
this.build_supermaven_context_menu(window, cx)
|
||||
})
|
||||
.ok(),
|
||||
SupermavenButtonStatus::Ready => Some(this.update(cx, |this, cx| {
|
||||
this.build_supermaven_context_menu(window, cx)
|
||||
})),
|
||||
_ => None,
|
||||
})
|
||||
.anchor(Corner::BottomRight)
|
||||
@@ -240,16 +233,15 @@ impl Render for EditPredictionButton {
|
||||
let enabled = self.editor_enabled.unwrap_or(true);
|
||||
let has_api_key = CodestralCompletionProvider::has_api_key(cx);
|
||||
let fs = self.fs.clone();
|
||||
let this = cx.weak_entity();
|
||||
let this = cx.entity();
|
||||
|
||||
div().child(
|
||||
PopoverMenu::new("codestral")
|
||||
.menu(move |window, cx| {
|
||||
if has_api_key {
|
||||
this.update(cx, |this, cx| {
|
||||
Some(this.update(cx, |this, cx| {
|
||||
this.build_codestral_context_menu(window, cx)
|
||||
})
|
||||
.ok()
|
||||
}))
|
||||
} else {
|
||||
Some(ContextMenu::build(window, cx, |menu, _, _| {
|
||||
let fs = fs.clone();
|
||||
@@ -300,15 +292,6 @@ impl Render for EditPredictionButton {
|
||||
.with_handle(self.popover_menu_handle.clone()),
|
||||
)
|
||||
}
|
||||
EditPredictionProvider::Experimental(provider_name) => {
|
||||
if *provider_name == EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME
|
||||
&& cx.has_flag::<SweepFeatureFlag>()
|
||||
{
|
||||
div().child(Icon::new(IconName::SweepAi))
|
||||
} else {
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
||||
EditPredictionProvider::Zed => {
|
||||
let enabled = self.editor_enabled.unwrap_or(true);
|
||||
@@ -537,7 +520,7 @@ impl EditPredictionButton {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
})
|
||||
}
|
||||
EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => continue,
|
||||
EditPredictionProvider::None => continue,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -849,16 +832,6 @@ impl EditPredictionButton {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
let copilot_config = copilot::copilot_chat::CopilotChatConfiguration {
|
||||
enterprise_uri: all_language_settings
|
||||
.edit_predictions
|
||||
.copilot
|
||||
.enterprise_uri
|
||||
.clone(),
|
||||
};
|
||||
let settings_url = copilot_settings_url(copilot_config.enterprise_uri.as_deref());
|
||||
|
||||
ContextMenu::build(window, cx, |menu, window, cx| {
|
||||
let menu = self.build_language_settings_menu(menu, window, cx);
|
||||
let menu =
|
||||
@@ -867,7 +840,10 @@ impl EditPredictionButton {
|
||||
menu.separator()
|
||||
.link(
|
||||
"Go to Copilot Settings",
|
||||
OpenBrowser { url: settings_url }.boxed_clone(),
|
||||
OpenBrowser {
|
||||
url: COPILOT_SETTINGS_URL.to_string(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
.action("Sign Out", copilot::SignOut.boxed_clone())
|
||||
})
|
||||
@@ -1196,99 +1172,3 @@ fn toggle_edit_prediction_mode(fs: Arc<dyn Fs>, mode: EditPredictionsMode, cx: &
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn copilot_settings_url(enterprise_uri: Option<&str>) -> String {
|
||||
match enterprise_uri {
|
||||
Some(uri) => {
|
||||
format!("{}{}", uri.trim_end_matches('/'), COPILOT_SETTINGS_PATH)
|
||||
}
|
||||
None => COPILOT_SETTINGS_URL.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copilot_settings_url_with_enterprise_uri(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
|
||||
cx.update_global(|settings_store: &mut SettingsStore, cx| {
|
||||
settings_store
|
||||
.set_user_settings(
|
||||
r#"{"edit_predictions":{"copilot":{"enterprise_uri":"https://my-company.ghe.com"}}}"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let url = cx.update(|cx| {
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
copilot_settings_url(
|
||||
all_language_settings
|
||||
.edit_predictions
|
||||
.copilot
|
||||
.enterprise_uri
|
||||
.as_deref(),
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(url, "https://my-company.ghe.com/settings/copilot");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copilot_settings_url_with_enterprise_uri_trailing_slash(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
|
||||
cx.update_global(|settings_store: &mut SettingsStore, cx| {
|
||||
settings_store
|
||||
.set_user_settings(
|
||||
r#"{"edit_predictions":{"copilot":{"enterprise_uri":"https://my-company.ghe.com/"}}}"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let url = cx.update(|cx| {
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
copilot_settings_url(
|
||||
all_language_settings
|
||||
.edit_predictions
|
||||
.copilot
|
||||
.enterprise_uri
|
||||
.as_deref(),
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(url, "https://my-company.ghe.com/settings/copilot");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_copilot_settings_url_without_enterprise_uri(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
|
||||
let url = cx.update(|cx| {
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
copilot_settings_url(
|
||||
all_language_settings
|
||||
.edit_predictions
|
||||
.copilot
|
||||
.enterprise_uri
|
||||
.as_deref(),
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(url, "https://github.com/settings/copilot");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,8 +305,6 @@ impl CompletionBuilder {
|
||||
icon_path: None,
|
||||
insert_text_mode: None,
|
||||
confirm: None,
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use project::{CompletionDisplayOptions, CompletionSource};
|
||||
use task::DebugScenario;
|
||||
use task::TaskContext;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{
|
||||
@@ -35,13 +36,12 @@ use util::ResultExt;
|
||||
|
||||
use crate::hover_popover::{hover_markdown_style, open_markdown_url};
|
||||
use crate::{
|
||||
CodeActionProvider, CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle,
|
||||
ResolvedTasks,
|
||||
CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
|
||||
EditorStyle, ResolvedTasks,
|
||||
actions::{ConfirmCodeAction, ConfirmCompletion},
|
||||
split_words, styled_runs_for_code_label,
|
||||
};
|
||||
use crate::{CodeActionSource, EditorSettings};
|
||||
use collections::{HashSet, VecDeque};
|
||||
use settings::{Settings, SnippetSortOrder};
|
||||
|
||||
pub const MENU_GAP: Pixels = px(4.);
|
||||
@@ -220,9 +220,7 @@ pub struct CompletionsMenu {
|
||||
pub is_incomplete: bool,
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
/// String match candidate for each completion, grouped by `match_start`.
|
||||
match_candidates: Arc<[(Option<text::Anchor>, Vec<StringMatchCandidate>)]>,
|
||||
/// Entries displayed in the menu, which is a filtered and sorted subset of `match_candidates`.
|
||||
match_candidates: Arc<[StringMatchCandidate]>,
|
||||
pub entries: Rc<RefCell<Box<[StringMatch]>>>,
|
||||
pub selected_item: usize,
|
||||
filter_task: Task<()>,
|
||||
@@ -310,8 +308,6 @@ impl CompletionsMenu {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
|
||||
.into_group_map_by(|candidate| completions[candidate.id].match_start)
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let completions_menu = Self {
|
||||
@@ -359,8 +355,6 @@ impl CompletionsMenu {
|
||||
replace_range: selection.start.text_anchor..selection.end.text_anchor,
|
||||
new_text: choice.to_string(),
|
||||
label: CodeLabel::plain(choice.to_string(), None),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
confirm: None,
|
||||
@@ -369,14 +363,11 @@ impl CompletionsMenu {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let match_candidates = Arc::new([(
|
||||
None,
|
||||
choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatchCandidate::new(id, completion))
|
||||
.collect(),
|
||||
)]);
|
||||
let match_candidates = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatchCandidate::new(id, completion))
|
||||
.collect();
|
||||
let entries = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -506,7 +497,7 @@ impl CompletionsMenu {
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
self.scroll_handle
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Nearest);
|
||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||
if let Some(provider) = provider {
|
||||
let entries = self.entries.borrow();
|
||||
let entry = if self.selected_item < entries.len() {
|
||||
@@ -957,7 +948,7 @@ impl CompletionsMenu {
|
||||
}
|
||||
|
||||
let mat = &self.entries.borrow()[self.selected_item];
|
||||
let completions = self.completions.borrow();
|
||||
let completions = self.completions.borrow_mut();
|
||||
let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
|
||||
Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
|
||||
Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
|
||||
@@ -1035,74 +1026,57 @@ impl CompletionsMenu {
|
||||
|
||||
pub fn filter(
|
||||
&mut self,
|
||||
query: Arc<String>,
|
||||
query_end: text::Anchor,
|
||||
buffer: &Entity<Buffer>,
|
||||
query: Option<Arc<String>>,
|
||||
provider: Option<Rc<dyn CompletionProvider>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
self.cancel_filter.store(true, Ordering::Relaxed);
|
||||
self.cancel_filter = Arc::new(AtomicBool::new(false));
|
||||
let matches = self.do_async_filtering(query, query_end, buffer, cx);
|
||||
let id = self.id;
|
||||
self.filter_task = cx.spawn_in(window, async move |editor, cx| {
|
||||
let matches = matches.await;
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.with_completions_menu_matching_id(id, |this| {
|
||||
if let Some(this) = this {
|
||||
this.set_filter_results(matches, provider, window, cx);
|
||||
}
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
if let Some(query) = query {
|
||||
self.cancel_filter = Arc::new(AtomicBool::new(false));
|
||||
let matches = self.do_async_filtering(query, cx);
|
||||
let id = self.id;
|
||||
self.filter_task = cx.spawn_in(window, async move |editor, cx| {
|
||||
let matches = matches.await;
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.with_completions_menu_matching_id(id, |this| {
|
||||
if let Some(this) = this {
|
||||
this.set_filter_results(matches, provider, window, cx);
|
||||
}
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
} else {
|
||||
self.filter_task = Task::ready(());
|
||||
let matches = self.unfiltered_matches();
|
||||
self.set_filter_results(matches, provider, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_async_filtering(
|
||||
&self,
|
||||
query: Arc<String>,
|
||||
query_end: text::Anchor,
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &Context<Editor>,
|
||||
) -> Task<Vec<StringMatch>> {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let background_executor = cx.background_executor().clone();
|
||||
let match_candidates = self.match_candidates.clone();
|
||||
let cancel_filter = self.cancel_filter.clone();
|
||||
let default_query = query.clone();
|
||||
|
||||
let matches_task = cx.background_spawn(async move {
|
||||
let queries_and_candidates = match_candidates
|
||||
.iter()
|
||||
.map(|(query_start, candidates)| {
|
||||
let query_for_batch = match query_start {
|
||||
Some(start) => {
|
||||
Arc::new(buffer_snapshot.text_for_range(*start..query_end).collect())
|
||||
}
|
||||
None => default_query.clone(),
|
||||
};
|
||||
(query_for_batch, candidates)
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
let mut results = vec![];
|
||||
for (query, match_candidates) in queries_and_candidates {
|
||||
results.extend(
|
||||
fuzzy::match_strings(
|
||||
&match_candidates,
|
||||
&query,
|
||||
query.chars().any(|c| c.is_uppercase()),
|
||||
false,
|
||||
1000,
|
||||
&cancel_filter,
|
||||
background_executor.clone(),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
let matches_task = cx.background_spawn({
|
||||
let query = query.clone();
|
||||
let match_candidates = self.match_candidates.clone();
|
||||
let cancel_filter = self.cancel_filter.clone();
|
||||
let background_executor = cx.background_executor().clone();
|
||||
async move {
|
||||
fuzzy::match_strings(
|
||||
&match_candidates,
|
||||
&query,
|
||||
query.chars().any(|c| c.is_uppercase()),
|
||||
false,
|
||||
1000,
|
||||
&cancel_filter,
|
||||
background_executor,
|
||||
)
|
||||
.await
|
||||
}
|
||||
results
|
||||
});
|
||||
|
||||
let completions = self.completions.clone();
|
||||
@@ -1111,31 +1085,45 @@ impl CompletionsMenu {
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let mut matches = matches_task.await;
|
||||
|
||||
let completions_ref = completions.borrow();
|
||||
|
||||
if sort_completions {
|
||||
matches = Self::sort_string_matches(
|
||||
matches,
|
||||
Some(&query), // used for non-snippets only
|
||||
Some(&query),
|
||||
snippet_sort_order,
|
||||
&completions_ref,
|
||||
completions.borrow().as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
// Remove duplicate snippet prefixes (e.g., "cool code" will match
|
||||
// the text "c c" in two places; we should only show the longer one)
|
||||
let mut snippets_seen = HashSet::<(usize, usize)>::default();
|
||||
matches.retain(|result| {
|
||||
match completions_ref[result.candidate_id].snippet_deduplication_key {
|
||||
Some(key) => snippets_seen.insert(key),
|
||||
None => true,
|
||||
}
|
||||
});
|
||||
|
||||
matches
|
||||
})
|
||||
}
|
||||
|
||||
/// Like `do_async_filtering` but there is no filter query, so no need to spawn tasks.
|
||||
pub fn unfiltered_matches(&self) -> Vec<StringMatch> {
|
||||
let mut matches = self
|
||||
.match_candidates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(candidate_id, candidate)| StringMatch {
|
||||
candidate_id,
|
||||
score: Default::default(),
|
||||
positions: Default::default(),
|
||||
string: candidate.string.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
if self.sort_completions {
|
||||
matches = Self::sort_string_matches(
|
||||
matches,
|
||||
None,
|
||||
self.snippet_sort_order,
|
||||
self.completions.borrow().as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
matches
|
||||
}
|
||||
|
||||
pub fn set_filter_results(
|
||||
&mut self,
|
||||
matches: Vec<StringMatch>,
|
||||
@@ -1178,13 +1166,28 @@ impl CompletionsMenu {
|
||||
.and_then(|c| c.to_lowercase().next());
|
||||
|
||||
if snippet_sort_order == SnippetSortOrder::None {
|
||||
matches
|
||||
.retain(|string_match| !completions[string_match.candidate_id].is_snippet_kind());
|
||||
matches.retain(|string_match| {
|
||||
let completion = &completions[string_match.candidate_id];
|
||||
|
||||
let is_snippet = matches!(
|
||||
&completion.source,
|
||||
CompletionSource::Lsp { lsp_completion, .. }
|
||||
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
|
||||
);
|
||||
|
||||
!is_snippet
|
||||
});
|
||||
}
|
||||
|
||||
matches.sort_unstable_by_key(|string_match| {
|
||||
let completion = &completions[string_match.candidate_id];
|
||||
|
||||
let is_snippet = matches!(
|
||||
&completion.source,
|
||||
CompletionSource::Lsp { lsp_completion, .. }
|
||||
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
|
||||
);
|
||||
|
||||
let sort_text = match &completion.source {
|
||||
CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
|
||||
CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
|
||||
@@ -1196,17 +1199,14 @@ impl CompletionsMenu {
|
||||
let score = string_match.score;
|
||||
let sort_score = Reverse(OrderedFloat(score));
|
||||
|
||||
// Snippets do their own first-letter matching logic elsewhere.
|
||||
let is_snippet = completion.is_snippet_kind();
|
||||
let query_start_doesnt_match_split_words = !is_snippet
|
||||
&& query_start_lower
|
||||
.map(|query_char| {
|
||||
!split_words(&string_match.string).any(|word| {
|
||||
word.chars().next().and_then(|c| c.to_lowercase().next())
|
||||
== Some(query_char)
|
||||
})
|
||||
let query_start_doesnt_match_split_words = query_start_lower
|
||||
.map(|query_char| {
|
||||
!split_words(&string_match.string).any(|word| {
|
||||
word.chars().next().and_then(|c| c.to_lowercase().next())
|
||||
== Some(query_char)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if query_start_doesnt_match_split_words {
|
||||
MatchTier::OtherMatch { sort_score }
|
||||
@@ -1218,7 +1218,6 @@ impl CompletionsMenu {
|
||||
SnippetSortOrder::None => Reverse(0),
|
||||
};
|
||||
let sort_positions = string_match.positions.clone();
|
||||
// This exact matching won't work for multi-word snippets, but it's fine
|
||||
let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
|
||||
1
|
||||
} else {
|
||||
|
||||
@@ -1097,7 +1097,7 @@ impl DisplaySnapshot {
|
||||
details: &TextLayoutDetails,
|
||||
) -> u32 {
|
||||
let layout_line = self.layout_row(display_row, details);
|
||||
layout_line.closest_index_for_x(x) as u32
|
||||
layout_line.index_for_x(x) as u32
|
||||
}
|
||||
|
||||
pub fn grapheme_at(&self, mut point: DisplayPoint) -> Option<SharedString> {
|
||||
|
||||
@@ -248,8 +248,10 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
// Determine split index handling edge cases
|
||||
let split_index = if desired_bytes >= chunk.text.len() {
|
||||
chunk.text.len()
|
||||
} else if chunk.text.is_char_boundary(desired_bytes) {
|
||||
desired_bytes
|
||||
} else {
|
||||
chunk.text.ceil_char_boundary(desired_bytes)
|
||||
find_next_utf8_boundary(chunk.text, desired_bytes)
|
||||
};
|
||||
|
||||
let (prefix, suffix) = chunk.text.split_at(split_index);
|
||||
@@ -371,8 +373,10 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
.next()
|
||||
.map(|c| c.len_utf8())
|
||||
.unwrap_or(1)
|
||||
} else if inlay_chunk.is_char_boundary(next_inlay_highlight_endpoint) {
|
||||
next_inlay_highlight_endpoint
|
||||
} else {
|
||||
inlay_chunk.ceil_char_boundary(next_inlay_highlight_endpoint)
|
||||
find_next_utf8_boundary(inlay_chunk, next_inlay_highlight_endpoint)
|
||||
};
|
||||
|
||||
let (chunk, remainder) = inlay_chunk.split_at(split_index);
|
||||
@@ -1142,6 +1146,31 @@ fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a byte index that is NOT a UTF-8 boundary, find the next one.
|
||||
/// Assumes: 0 < byte_index < text.len() and !text.is_char_boundary(byte_index)
|
||||
#[inline(always)]
|
||||
fn find_next_utf8_boundary(text: &str, byte_index: usize) -> usize {
|
||||
let bytes = text.as_bytes();
|
||||
let mut idx = byte_index + 1;
|
||||
|
||||
// Scan forward until we find a boundary
|
||||
while idx < text.len() {
|
||||
if is_utf8_char_boundary(bytes[idx]) {
|
||||
return idx;
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
// Hit the end, return the full length
|
||||
text.len()
|
||||
}
|
||||
|
||||
// Private helper function taken from Rust's core::num module (which is both Apache2 and MIT licensed)
|
||||
const fn is_utf8_char_boundary(byte: u8) -> bool {
|
||||
// This is bit magic equivalent to: b < 128 || b >= 192
|
||||
(byte as i8) >= -0x40
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -74,7 +74,7 @@ use ::git::{
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
status::FileStatus,
|
||||
};
|
||||
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError};
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use blink_manager::BlinkManager;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
@@ -117,9 +117,8 @@ use language::{
|
||||
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
|
||||
BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
|
||||
DiagnosticEntryRef, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
|
||||
IndentSize, Language, LanguageRegistry, OffsetRangeExt, OutlineItem, Point, Runnable,
|
||||
RunnableRange, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
|
||||
WordsQuery,
|
||||
IndentSize, Language, OffsetRangeExt, OutlineItem, Point, Runnable, RunnableRange, Selection,
|
||||
SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
|
||||
language_settings::{
|
||||
self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings,
|
||||
language_settings,
|
||||
@@ -372,7 +371,6 @@ pub trait DiagnosticRenderer {
|
||||
buffer_id: BufferId,
|
||||
snapshot: EditorSnapshot,
|
||||
editor: WeakEntity<Editor>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut App,
|
||||
) -> Vec<BlockProperties<Anchor>>;
|
||||
|
||||
@@ -381,7 +379,6 @@ pub trait DiagnosticRenderer {
|
||||
diagnostic_group: Vec<DiagnosticEntryRef<'_, Point>>,
|
||||
range: Range<Point>,
|
||||
buffer_id: BufferId,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<markdown::Markdown>>;
|
||||
|
||||
@@ -1099,7 +1096,6 @@ pub struct Editor {
|
||||
searchable: bool,
|
||||
cursor_shape: CursorShape,
|
||||
current_line_highlight: Option<CurrentLineHighlight>,
|
||||
collapse_matches: bool,
|
||||
autoindent_mode: Option<AutoindentMode>,
|
||||
workspace: Option<(WeakEntity<Workspace>, Option<WorkspaceId>)>,
|
||||
input_enabled: bool,
|
||||
@@ -1194,7 +1190,6 @@ pub struct Editor {
|
||||
refresh_colors_task: Task<()>,
|
||||
inlay_hints: Option<LspInlayHintData>,
|
||||
folding_newlines: Task<()>,
|
||||
select_next_is_case_sensitive: Option<bool>,
|
||||
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
|
||||
}
|
||||
|
||||
@@ -1301,9 +1296,8 @@ struct SelectionHistoryEntry {
|
||||
add_selections_state: Option<AddSelectionsState>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
enum SelectionHistoryMode {
|
||||
#[default]
|
||||
Normal,
|
||||
Undoing,
|
||||
Redoing,
|
||||
@@ -1316,6 +1310,12 @@ struct HoveredCursor {
|
||||
selection_id: usize,
|
||||
}
|
||||
|
||||
impl Default for SelectionHistoryMode {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// SelectionEffects controls the side-effects of updating the selection.
|
||||
///
|
||||
@@ -2212,7 +2212,7 @@ impl Editor {
|
||||
.unwrap_or_default(),
|
||||
current_line_highlight: None,
|
||||
autoindent_mode: Some(AutoindentMode::EachLine),
|
||||
collapse_matches: false,
|
||||
|
||||
workspace: None,
|
||||
input_enabled: !is_minimap,
|
||||
use_modal_editing: full_mode,
|
||||
@@ -2333,7 +2333,6 @@ impl Editor {
|
||||
selection_drag_state: SelectionDragState::None,
|
||||
folding_newlines: Task::ready(()),
|
||||
lookup_key: None,
|
||||
select_next_is_case_sensitive: None,
|
||||
};
|
||||
|
||||
if is_minimap {
|
||||
@@ -2385,10 +2384,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
EditorEvent::Edited { .. } => {
|
||||
let vim_mode = vim_mode_setting::VimModeSetting::try_get(cx)
|
||||
.map(|vim_mode| vim_mode.0)
|
||||
.unwrap_or(false);
|
||||
if !vim_mode {
|
||||
if vim_flavor(cx).is_none() {
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let selections = editor.selections.all_adjusted_display(&display_map);
|
||||
let pop_state = editor
|
||||
@@ -2635,10 +2631,6 @@ impl Editor {
|
||||
key_context.add("end_of_input");
|
||||
}
|
||||
|
||||
if self.has_any_expanded_diff_hunks(cx) {
|
||||
key_context.add("diffs_expanded");
|
||||
}
|
||||
|
||||
key_context
|
||||
}
|
||||
|
||||
@@ -3017,21 +3009,17 @@ impl Editor {
|
||||
self.current_line_highlight = current_line_highlight;
|
||||
}
|
||||
|
||||
pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
|
||||
self.collapse_matches = collapse_matches;
|
||||
}
|
||||
|
||||
pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
|
||||
if self.collapse_matches {
|
||||
pub fn range_for_match<T: std::marker::Copy>(
|
||||
&self,
|
||||
range: &Range<T>,
|
||||
collapse: bool,
|
||||
) -> Range<T> {
|
||||
if collapse {
|
||||
return range.start..range.start;
|
||||
}
|
||||
range.clone()
|
||||
}
|
||||
|
||||
pub fn clip_at_line_ends(&mut self, cx: &mut Context<Self>) -> bool {
|
||||
self.display_map.read(cx).clip_at_line_ends
|
||||
}
|
||||
|
||||
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut Context<Self>) {
|
||||
if self.display_map.read(cx).clip_at_line_ends != clip {
|
||||
self.display_map
|
||||
@@ -3291,8 +3279,8 @@ impl Editor {
|
||||
self.refresh_document_highlights(cx);
|
||||
refresh_linked_ranges(self, window, cx);
|
||||
|
||||
// self.refresh_selected_text_highlights(false, window, cx);
|
||||
// self.refresh_matching_bracket_highlights(window, cx);
|
||||
self.refresh_selected_text_highlights(false, window, cx);
|
||||
self.refresh_matching_bracket_highlights(window, cx);
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.edit_prediction_requires_modifier_in_indent_conflict = true;
|
||||
self.inline_blame_popover.take();
|
||||
@@ -3459,21 +3447,6 @@ impl Editor {
|
||||
Subscription::join(other_subscription, this_subscription)
|
||||
}
|
||||
|
||||
fn unfold_buffers_with_selections(&mut self, cx: &mut Context<Self>) {
|
||||
if self.buffer().read(cx).is_singleton() {
|
||||
return;
|
||||
}
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let buffer_ids: HashSet<BufferId> = self
|
||||
.selections
|
||||
.disjoint_anchor_ranges()
|
||||
.flat_map(|range| snapshot.buffer_ids_for_range(range))
|
||||
.collect();
|
||||
for buffer_id in buffer_ids {
|
||||
self.unfold_buffer(buffer_id, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes selections using the provided mutation function. Changes to `self.selections` occur
|
||||
/// immediately, but when run within `transact` or `with_selection_effects_deferred` other
|
||||
/// effects of selection change occur at the end of the transaction.
|
||||
@@ -4086,24 +4059,17 @@ impl Editor {
|
||||
self.selection_mark_mode = false;
|
||||
self.selection_drag_state = SelectionDragState::None;
|
||||
|
||||
if self.dismiss_menus_and_popups(true, window, cx) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
if self.clear_expanded_diff_hunks(cx) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
if self.show_git_blame_gutter {
|
||||
self.show_git_blame_gutter = false;
|
||||
cx.notify();
|
||||
if self.dismiss_menus_and_popups(true, window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.mode.is_full()
|
||||
&& self.change_selections(Default::default(), window, cx, |s| s.try_cancel())
|
||||
{
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4222,8 +4188,6 @@ impl Editor {
|
||||
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
||||
|
||||
self.unfold_buffers_with_selections(cx);
|
||||
|
||||
let selections = self.selections.all_adjusted(&self.display_snapshot(cx));
|
||||
let mut bracket_inserted = false;
|
||||
let mut edits = Vec::new();
|
||||
@@ -5527,14 +5491,7 @@ impl Editor {
|
||||
|
||||
if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
|
||||
if filter_completions {
|
||||
menu.filter(
|
||||
query.clone().unwrap_or_default(),
|
||||
buffer_position.text_anchor,
|
||||
&buffer,
|
||||
provider.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
menu.filter(query.clone(), provider.clone(), window, cx);
|
||||
}
|
||||
// When `is_incomplete` is false, no need to re-query completions when the current query
|
||||
// is a suffix of the initial query.
|
||||
@@ -5543,7 +5500,7 @@ impl Editor {
|
||||
// If the new query is a suffix of the old query (typing more characters) and
|
||||
// the previous result was complete, the existing completions can be filtered.
|
||||
//
|
||||
// Note that snippet completions are always complete.
|
||||
// Note that this is always true for snippet completions.
|
||||
let query_matches = match (&menu.initial_query, &query) {
|
||||
(Some(initial_query), Some(query)) => query.starts_with(initial_query.as_ref()),
|
||||
(None, _) => true,
|
||||
@@ -5673,15 +5630,12 @@ impl Editor {
|
||||
};
|
||||
|
||||
let mut words = if load_word_completions {
|
||||
cx.background_spawn({
|
||||
let buffer_snapshot = buffer_snapshot.clone();
|
||||
async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
range: word_search_range,
|
||||
skip_digits,
|
||||
})
|
||||
}
|
||||
cx.background_spawn(async move {
|
||||
buffer_snapshot.words_in_range(WordsQuery {
|
||||
fuzzy_contents: None,
|
||||
range: word_search_range,
|
||||
skip_digits,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
Task::ready(BTreeMap::default())
|
||||
@@ -5691,11 +5645,8 @@ impl Editor {
|
||||
&& provider.show_snippets()
|
||||
&& let Some(project) = self.project()
|
||||
{
|
||||
let char_classifier = buffer_snapshot
|
||||
.char_classifier_at(buffer_position)
|
||||
.scope_context(Some(CharScopeContext::Completion));
|
||||
project.update(cx, |project, cx| {
|
||||
snippet_completions(project, &buffer, buffer_position, char_classifier, cx)
|
||||
snippet_completions(project, &buffer, buffer_position, cx)
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(CompletionResponse {
|
||||
@@ -5750,8 +5701,6 @@ impl Editor {
|
||||
replace_range: word_replace_range.clone(),
|
||||
new_text: word.clone(),
|
||||
label: CodeLabel::plain(word, None),
|
||||
match_start: None,
|
||||
snippet_deduplication_key: None,
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
source: CompletionSource::BufferWord {
|
||||
@@ -5800,12 +5749,11 @@ impl Editor {
|
||||
);
|
||||
|
||||
let query = if filter_completions { query } else { None };
|
||||
let matches_task = menu.do_async_filtering(
|
||||
query.unwrap_or_default(),
|
||||
buffer_position,
|
||||
&buffer,
|
||||
cx,
|
||||
);
|
||||
let matches_task = if let Some(query) = query {
|
||||
menu.do_async_filtering(query, cx)
|
||||
} else {
|
||||
Task::ready(menu.unfiltered_matches())
|
||||
};
|
||||
(menu, matches_task)
|
||||
}) else {
|
||||
return;
|
||||
@@ -5822,7 +5770,7 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
// Only valid to take prev_menu because either the new menu is immediately set
|
||||
// Only valid to take prev_menu because it the new menu is immediately set
|
||||
// below, or the menu is hidden.
|
||||
if let Some(CodeContextMenu::Completions(prev_menu)) =
|
||||
editor.context_menu.borrow_mut().take()
|
||||
@@ -7877,10 +7825,6 @@ impl Editor {
|
||||
self.edit_prediction_preview,
|
||||
EditPredictionPreview::Inactive { .. }
|
||||
) {
|
||||
if let Some(provider) = self.edit_prediction_provider.as_ref() {
|
||||
provider.provider.did_show(cx)
|
||||
}
|
||||
|
||||
self.edit_prediction_preview = EditPredictionPreview::Active {
|
||||
previous_scroll_position: None,
|
||||
since: Instant::now(),
|
||||
@@ -8060,9 +8004,6 @@ impl Editor {
|
||||
&& !self.edit_predictions_hidden_for_vim_mode;
|
||||
|
||||
if show_completions_in_buffer {
|
||||
if let Some(provider) = &self.edit_prediction_provider {
|
||||
provider.provider.did_show(cx);
|
||||
}
|
||||
if edits
|
||||
.iter()
|
||||
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
|
||||
@@ -11077,10 +11018,6 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.breakpoint_store.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) {
|
||||
let breakpoint = breakpoint.unwrap_or_else(|| Breakpoint {
|
||||
message: None,
|
||||
@@ -11140,10 +11077,6 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.breakpoint_store.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) {
|
||||
let Some(breakpoint) = breakpoint.filter(|breakpoint| breakpoint.is_disabled()) else {
|
||||
continue;
|
||||
@@ -11163,10 +11096,6 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.breakpoint_store.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) {
|
||||
let Some(breakpoint) = breakpoint.filter(|breakpoint| breakpoint.is_enabled()) else {
|
||||
continue;
|
||||
@@ -11186,10 +11115,6 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.breakpoint_store.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) {
|
||||
if let Some(breakpoint) = breakpoint {
|
||||
self.edit_breakpoint_at_anchor(
|
||||
@@ -12597,7 +12522,6 @@ impl Editor {
|
||||
{
|
||||
let max_point = buffer.max_point();
|
||||
let mut is_first = true;
|
||||
let mut prev_selection_was_entire_line = false;
|
||||
for selection in &mut selections {
|
||||
let is_entire_line =
|
||||
(selection.is_empty() && cut_no_selection_line) || self.selections.line_mode();
|
||||
@@ -12612,10 +12536,9 @@ impl Editor {
|
||||
}
|
||||
if is_first {
|
||||
is_first = false;
|
||||
} else if !prev_selection_was_entire_line {
|
||||
} else {
|
||||
text += "\n";
|
||||
}
|
||||
prev_selection_was_entire_line = is_entire_line;
|
||||
let mut len = 0;
|
||||
for chunk in buffer.text_for_range(selection.start..selection.end) {
|
||||
text.push_str(chunk);
|
||||
@@ -12698,7 +12621,6 @@ impl Editor {
|
||||
{
|
||||
let max_point = buffer.max_point();
|
||||
let mut is_first = true;
|
||||
let mut prev_selection_was_entire_line = false;
|
||||
for selection in &selections {
|
||||
let mut start = selection.start;
|
||||
let mut end = selection.end;
|
||||
@@ -12757,10 +12679,9 @@ impl Editor {
|
||||
for trimmed_range in trimmed_selections {
|
||||
if is_first {
|
||||
is_first = false;
|
||||
} else if !prev_selection_was_entire_line {
|
||||
} else {
|
||||
text += "\n";
|
||||
}
|
||||
prev_selection_was_entire_line = is_entire_line;
|
||||
let mut len = 0;
|
||||
for chunk in buffer.text_for_range(trimmed_range.start..trimmed_range.end) {
|
||||
text.push_str(chunk);
|
||||
@@ -12834,11 +12755,7 @@ impl Editor {
|
||||
let end_offset = start_offset + clipboard_selection.len;
|
||||
to_insert = &clipboard_text[start_offset..end_offset];
|
||||
entire_line = clipboard_selection.is_entire_line;
|
||||
start_offset = if entire_line {
|
||||
end_offset
|
||||
} else {
|
||||
end_offset + 1
|
||||
};
|
||||
start_offset = end_offset + 1;
|
||||
original_indent_column = Some(clipboard_selection.first_line_indent);
|
||||
} else {
|
||||
to_insert = &*clipboard_text;
|
||||
@@ -14728,7 +14645,7 @@ impl Editor {
|
||||
.collect::<String>();
|
||||
let is_empty = query.is_empty();
|
||||
let select_state = SelectNextState {
|
||||
query: self.build_query(&[query], cx)?,
|
||||
query: AhoCorasick::new(&[query])?,
|
||||
wordwise: true,
|
||||
done: is_empty,
|
||||
};
|
||||
@@ -14738,7 +14655,7 @@ impl Editor {
|
||||
}
|
||||
} else if let Some(selected_text) = selected_text {
|
||||
self.select_next_state = Some(SelectNextState {
|
||||
query: self.build_query(&[selected_text], cx)?,
|
||||
query: AhoCorasick::new(&[selected_text])?,
|
||||
wordwise: false,
|
||||
done: false,
|
||||
});
|
||||
@@ -14946,7 +14863,7 @@ impl Editor {
|
||||
.collect::<String>();
|
||||
let is_empty = query.is_empty();
|
||||
let select_state = SelectNextState {
|
||||
query: self.build_query(&[query.chars().rev().collect::<String>()], cx)?,
|
||||
query: AhoCorasick::new(&[query.chars().rev().collect::<String>()])?,
|
||||
wordwise: true,
|
||||
done: is_empty,
|
||||
};
|
||||
@@ -14956,8 +14873,7 @@ impl Editor {
|
||||
}
|
||||
} else if let Some(selected_text) = selected_text {
|
||||
self.select_prev_state = Some(SelectNextState {
|
||||
query: self
|
||||
.build_query(&[selected_text.chars().rev().collect::<String>()], cx)?,
|
||||
query: AhoCorasick::new(&[selected_text.chars().rev().collect::<String>()])?,
|
||||
wordwise: false,
|
||||
done: false,
|
||||
});
|
||||
@@ -14967,25 +14883,6 @@ impl Editor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds an `AhoCorasick` automaton from the provided patterns, while
|
||||
/// setting the case sensitivity based on the global
|
||||
/// `SelectNextCaseSensitive` setting, if set, otherwise based on the
|
||||
/// editor's settings.
|
||||
fn build_query<I, P>(&self, patterns: I, cx: &Context<Self>) -> Result<AhoCorasick, BuildError>
|
||||
where
|
||||
I: IntoIterator<Item = P>,
|
||||
P: AsRef<[u8]>,
|
||||
{
|
||||
let case_sensitive = self.select_next_is_case_sensitive.map_or_else(
|
||||
|| EditorSettings::get_global(cx).search.case_sensitive,
|
||||
|value| value,
|
||||
);
|
||||
|
||||
let mut builder = AhoCorasickBuilder::new();
|
||||
builder.ascii_case_insensitive(!case_sensitive);
|
||||
builder.build(patterns)
|
||||
}
|
||||
|
||||
pub fn find_next_match(
|
||||
&mut self,
|
||||
_: &FindNextMatch,
|
||||
@@ -16929,7 +16826,7 @@ impl Editor {
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
let range = target_range.to_point(target_buffer.read(cx));
|
||||
let range = editor.range_for_match(&range);
|
||||
let range = editor.range_for_match(&range, false);
|
||||
let range = collapse_multiline_range(range);
|
||||
|
||||
if !split
|
||||
@@ -17972,18 +17869,8 @@ impl Editor {
|
||||
.diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let language_registry = self
|
||||
.project()
|
||||
.map(|project| project.read(cx).languages().clone());
|
||||
|
||||
let blocks = renderer.render_group(
|
||||
diagnostic_group,
|
||||
buffer_id,
|
||||
snapshot,
|
||||
cx.weak_entity(),
|
||||
language_registry,
|
||||
cx,
|
||||
);
|
||||
let blocks =
|
||||
renderer.render_group(diagnostic_group, buffer_id, snapshot, cx.weak_entity(), cx);
|
||||
|
||||
let blocks = self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.insert_blocks(blocks, cx).into_iter().collect()
|
||||
@@ -18970,17 +18857,10 @@ impl Editor {
|
||||
if self.buffer().read(cx).is_singleton() || self.is_buffer_folded(buffer_id, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx);
|
||||
self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.fold_buffers([buffer_id], cx)
|
||||
});
|
||||
|
||||
let snapshot = self.display_snapshot(cx);
|
||||
self.selections.change_with(&snapshot, |selections| {
|
||||
selections.remove_selections_from_buffer(buffer_id);
|
||||
});
|
||||
|
||||
cx.emit(EditorEvent::BufferFoldToggled {
|
||||
ids: folded_excerpts.iter().map(|&(id, _)| id).collect(),
|
||||
folded: true,
|
||||
@@ -19348,16 +19228,6 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn has_any_expanded_diff_hunks(&self, cx: &App) -> bool {
|
||||
if self.buffer.read(cx).all_diff_hunks_expanded() {
|
||||
return true;
|
||||
}
|
||||
let ranges = vec![Anchor::min()..Anchor::max()];
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.has_expanded_diff_hunks_in_ranges(&ranges, cx)
|
||||
}
|
||||
|
||||
fn toggle_diff_hunks_in_ranges(
|
||||
&mut self,
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
@@ -21248,9 +21118,9 @@ impl Editor {
|
||||
self.active_indent_guides_state.dirty = true;
|
||||
self.refresh_active_diagnostics(cx);
|
||||
self.refresh_code_actions(window, cx);
|
||||
// self.refresh_selected_text_highlights(true, window, cx);
|
||||
self.refresh_selected_text_highlights(true, window, cx);
|
||||
self.refresh_single_line_folds(window, cx);
|
||||
// self.refresh_matching_bracket_highlights(window, cx);
|
||||
self.refresh_matching_bracket_highlights(window, cx);
|
||||
if self.has_active_edit_prediction() {
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
}
|
||||
@@ -21345,7 +21215,6 @@ impl Editor {
|
||||
}
|
||||
multi_buffer::Event::Reparsed(buffer_id) => {
|
||||
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
|
||||
// self.refresh_selected_text_highlights(true, window, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
|
||||
cx.emit(EditorEvent::Reparsed(*buffer_id));
|
||||
@@ -21770,9 +21639,7 @@ impl Editor {
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|a| a.to_string()));
|
||||
|
||||
let vim_mode = vim_mode_setting::VimModeSetting::try_get(cx)
|
||||
.map(|vim_mode| vim_mode.0)
|
||||
.unwrap_or(false);
|
||||
let vim_mode = vim_flavor(cx).is_some();
|
||||
|
||||
let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider;
|
||||
let copilot_enabled = edit_predictions_provider
|
||||
@@ -22407,6 +22274,28 @@ fn edit_for_markdown_paste<'a>(
|
||||
(range, new_text)
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum VimFlavor {
|
||||
Vim,
|
||||
Helix,
|
||||
}
|
||||
|
||||
pub fn vim_flavor(cx: &App) -> Option<VimFlavor> {
|
||||
if vim_mode_setting::HelixModeSetting::try_get(cx)
|
||||
.map(|helix_mode| helix_mode.0)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(VimFlavor::Helix)
|
||||
} else if vim_mode_setting::VimModeSetting::try_get(cx)
|
||||
.map(|vim_mode| vim_mode.0)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(VimFlavor::Vim)
|
||||
} else {
|
||||
None // neither vim nor helix mode
|
||||
}
|
||||
}
|
||||
|
||||
fn process_completion_for_edit(
|
||||
completion: &Completion,
|
||||
intent: CompletionIntent,
|
||||
@@ -23259,11 +23148,10 @@ impl CodeActionProvider for Entity<Project> {
|
||||
fn snippet_completions(
|
||||
project: &Project,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_anchor: text::Anchor,
|
||||
classifier: CharClassifier,
|
||||
buffer_position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<CompletionResponse>> {
|
||||
let languages = buffer.read(cx).languages_at(buffer_anchor);
|
||||
let languages = buffer.read(cx).languages_at(buffer_position);
|
||||
let snippet_store = project.snippets().read(cx);
|
||||
|
||||
let scopes: Vec<_> = languages
|
||||
@@ -23292,146 +23180,97 @@ fn snippet_completions(
|
||||
let executor = cx.background_executor().clone();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let is_word_char = |c| classifier.is_word(c);
|
||||
|
||||
let mut is_incomplete = false;
|
||||
let mut completions: Vec<Completion> = Vec::new();
|
||||
for (scope, snippets) in scopes.into_iter() {
|
||||
let classifier =
|
||||
CharClassifier::new(Some(scope)).scope_context(Some(CharScopeContext::Completion));
|
||||
|
||||
const MAX_PREFIX_LEN: usize = 128;
|
||||
let buffer_offset = text::ToOffset::to_offset(&buffer_anchor, &snapshot);
|
||||
let window_start = buffer_offset.saturating_sub(MAX_PREFIX_LEN);
|
||||
let window_start = snapshot.clip_offset(window_start, Bias::Left);
|
||||
const MAX_WORD_PREFIX_LEN: usize = 128;
|
||||
let last_word: String = snapshot
|
||||
.reversed_chars_for_range(text::Anchor::MIN..buffer_position)
|
||||
.take(MAX_WORD_PREFIX_LEN)
|
||||
.take_while(|c| classifier.is_word(*c))
|
||||
.collect::<String>()
|
||||
.chars()
|
||||
.rev()
|
||||
.collect();
|
||||
|
||||
let max_buffer_window: String = snapshot
|
||||
.text_for_range(window_start..buffer_offset)
|
||||
.collect();
|
||||
|
||||
if max_buffer_window.is_empty() {
|
||||
return Ok(CompletionResponse {
|
||||
completions: vec![],
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
for (_scope, snippets) in scopes.into_iter() {
|
||||
// Sort snippets by word count to match longer snippet prefixes first.
|
||||
let mut sorted_snippet_candidates = snippets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(snippet_ix, snippet)| {
|
||||
snippet
|
||||
.prefix
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(prefix_ix, prefix)| {
|
||||
let word_count =
|
||||
snippet_candidate_suffixes(prefix, is_word_char).count();
|
||||
((snippet_ix, prefix_ix), prefix, word_count)
|
||||
})
|
||||
})
|
||||
.collect_vec();
|
||||
sorted_snippet_candidates
|
||||
.sort_unstable_by_key(|(_, _, word_count)| Reverse(*word_count));
|
||||
|
||||
// Each prefix may be matched multiple times; the completion menu must filter out duplicates.
|
||||
|
||||
let buffer_windows = snippet_candidate_suffixes(&max_buffer_window, is_word_char)
|
||||
.take(
|
||||
sorted_snippet_candidates
|
||||
.first()
|
||||
.map(|(_, _, word_count)| *word_count)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.collect_vec();
|
||||
|
||||
const MAX_RESULTS: usize = 100;
|
||||
// Each match also remembers how many characters from the buffer it consumed
|
||||
let mut matches: Vec<(StringMatch, usize)> = vec![];
|
||||
|
||||
let mut snippet_list_cutoff_index = 0;
|
||||
for (buffer_index, buffer_window) in buffer_windows.iter().enumerate().rev() {
|
||||
let word_count = buffer_index + 1;
|
||||
// Increase `snippet_list_cutoff_index` until we have all of the
|
||||
// snippets with sufficiently many words.
|
||||
while sorted_snippet_candidates
|
||||
.get(snippet_list_cutoff_index)
|
||||
.is_some_and(|(_ix, _prefix, snippet_word_count)| {
|
||||
*snippet_word_count >= word_count
|
||||
})
|
||||
{
|
||||
snippet_list_cutoff_index += 1;
|
||||
}
|
||||
|
||||
// Take only the candidates with at least `word_count` many words
|
||||
let snippet_candidates_at_word_len =
|
||||
&sorted_snippet_candidates[..snippet_list_cutoff_index];
|
||||
|
||||
let candidates = snippet_candidates_at_word_len
|
||||
.iter()
|
||||
.map(|(_snippet_ix, prefix, _snippet_word_count)| prefix)
|
||||
.enumerate() // index in `sorted_snippet_candidates`
|
||||
// First char must match
|
||||
.filter(|(_ix, prefix)| {
|
||||
itertools::equal(
|
||||
prefix
|
||||
.chars()
|
||||
.next()
|
||||
.into_iter()
|
||||
.flat_map(|c| c.to_lowercase()),
|
||||
buffer_window
|
||||
.chars()
|
||||
.next()
|
||||
.into_iter()
|
||||
.flat_map(|c| c.to_lowercase()),
|
||||
)
|
||||
})
|
||||
.map(|(ix, prefix)| StringMatchCandidate::new(ix, prefix))
|
||||
.collect::<Vec<StringMatchCandidate>>();
|
||||
|
||||
matches.extend(
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&buffer_window,
|
||||
buffer_window.chars().any(|c| c.is_uppercase()),
|
||||
true,
|
||||
MAX_RESULTS - matches.len(), // always prioritize longer snippets
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|string_match| (string_match, buffer_window.len())),
|
||||
);
|
||||
|
||||
if matches.len() >= MAX_RESULTS {
|
||||
break;
|
||||
}
|
||||
if last_word.is_empty() {
|
||||
return Ok(CompletionResponse {
|
||||
completions: vec![],
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot);
|
||||
let to_lsp = |point: &text::Anchor| {
|
||||
let end = text::ToPointUtf16::to_point_utf16(point, &snapshot);
|
||||
point_to_lsp(end)
|
||||
};
|
||||
let lsp_end = to_lsp(&buffer_anchor);
|
||||
let lsp_end = to_lsp(&buffer_position);
|
||||
|
||||
let candidates = snippets
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(ix, snippet)| {
|
||||
snippet
|
||||
.prefix
|
||||
.iter()
|
||||
.map(move |prefix| StringMatchCandidate::new(ix, prefix))
|
||||
})
|
||||
.collect::<Vec<StringMatchCandidate>>();
|
||||
|
||||
const MAX_RESULTS: usize = 100;
|
||||
let mut matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&last_word,
|
||||
last_word.chars().any(|c| c.is_uppercase()),
|
||||
true,
|
||||
MAX_RESULTS,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if matches.len() >= MAX_RESULTS {
|
||||
is_incomplete = true;
|
||||
}
|
||||
|
||||
completions.extend(matches.iter().map(|(string_match, buffer_window_len)| {
|
||||
let ((snippet_index, prefix_index), matching_prefix, _snippet_word_count) =
|
||||
sorted_snippet_candidates[string_match.candidate_id];
|
||||
let snippet = &snippets[snippet_index];
|
||||
let start = buffer_offset - buffer_window_len;
|
||||
// Remove all candidates where the query's start does not match the start of any word in the candidate
|
||||
if let Some(query_start) = last_word.chars().next() {
|
||||
matches.retain(|string_match| {
|
||||
split_words(&string_match.string).any(|word| {
|
||||
// Check that the first codepoint of the word as lowercase matches the first
|
||||
// codepoint of the query as lowercase
|
||||
word.chars()
|
||||
.flat_map(|codepoint| codepoint.to_lowercase())
|
||||
.zip(query_start.to_lowercase())
|
||||
.all(|(word_cp, query_cp)| word_cp == query_cp)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
let matched_strings = matches
|
||||
.into_iter()
|
||||
.map(|m| m.string)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
completions.extend(snippets.iter().filter_map(|snippet| {
|
||||
let matching_prefix = snippet
|
||||
.prefix
|
||||
.iter()
|
||||
.find(|prefix| matched_strings.contains(*prefix))?;
|
||||
let start = as_offset - last_word.len();
|
||||
let start = snapshot.anchor_before(start);
|
||||
let range = start..buffer_anchor;
|
||||
let range = start..buffer_position;
|
||||
let lsp_start = to_lsp(&start);
|
||||
let lsp_range = lsp::Range {
|
||||
start: lsp_start,
|
||||
end: lsp_end,
|
||||
};
|
||||
Completion {
|
||||
Some(Completion {
|
||||
replace_range: range,
|
||||
new_text: snippet.body.clone(),
|
||||
source: CompletionSource::Lsp {
|
||||
@@ -23461,11 +23300,7 @@ fn snippet_completions(
|
||||
}),
|
||||
lsp_defaults: None,
|
||||
},
|
||||
label: CodeLabel {
|
||||
text: matching_prefix.clone(),
|
||||
runs: Vec::new(),
|
||||
filter_range: 0..matching_prefix.len(),
|
||||
},
|
||||
label: CodeLabel::plain(matching_prefix.clone(), None),
|
||||
icon_path: None,
|
||||
documentation: Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
|
||||
single_line: snippet.name.clone().into(),
|
||||
@@ -23476,10 +23311,8 @@ fn snippet_completions(
|
||||
}),
|
||||
insert_text_mode: None,
|
||||
confirm: None,
|
||||
match_start: Some(start),
|
||||
snippet_deduplication_key: Some((snippet_index, prefix_index)),
|
||||
}
|
||||
}));
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
Ok(CompletionResponse {
|
||||
@@ -23908,10 +23741,6 @@ impl EditorSnapshot {
|
||||
self.scroll_anchor.scroll_position(&self.display_snapshot)
|
||||
}
|
||||
|
||||
pub fn scroll_near_end(&self) -> bool {
|
||||
self.scroll_anchor.near_end(&self.display_snapshot)
|
||||
}
|
||||
|
||||
fn gutter_dimensions(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
@@ -24729,33 +24558,6 @@ pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator<Item = &str> +
|
||||
})
|
||||
}
|
||||
|
||||
/// Given a string of text immediately before the cursor, iterates over possible
|
||||
/// strings a snippet could match to. More precisely: returns an iterator over
|
||||
/// suffixes of `text` created by splitting at word boundaries (before & after
|
||||
/// every non-word character).
|
||||
///
|
||||
/// Shorter suffixes are returned first.
|
||||
pub(crate) fn snippet_candidate_suffixes(
|
||||
text: &str,
|
||||
is_word_char: impl Fn(char) -> bool,
|
||||
) -> impl std::iter::Iterator<Item = &str> {
|
||||
let mut prev_index = text.len();
|
||||
let mut prev_codepoint = None;
|
||||
text.char_indices()
|
||||
.rev()
|
||||
.chain([(0, '\0')])
|
||||
.filter_map(move |(index, codepoint)| {
|
||||
let prev_index = std::mem::replace(&mut prev_index, index);
|
||||
let prev_codepoint = prev_codepoint.replace(codepoint)?;
|
||||
if is_word_char(prev_codepoint) && is_word_char(codepoint) {
|
||||
None
|
||||
} else {
|
||||
let chunk = &text[prev_index..]; // go to end of string
|
||||
Some(chunk)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub trait RangeToAnchorExt: Sized {
|
||||
fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
|
||||
|
||||
|
||||
@@ -162,15 +162,10 @@ pub struct DragAndDropSelection {
|
||||
pub struct SearchSettings {
|
||||
/// Whether to show the project search button in the status bar.
|
||||
pub button: bool,
|
||||
/// Whether to only match on whole words.
|
||||
pub whole_word: bool,
|
||||
/// Whether to match case sensitively.
|
||||
pub case_sensitive: bool,
|
||||
/// Whether to include gitignored files in search results.
|
||||
pub include_ignored: bool,
|
||||
/// Whether to interpret the search query as a regular expression.
|
||||
pub regex: bool,
|
||||
/// Whether to center the cursor on each search match when navigating.
|
||||
pub center_on_match: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ use language::{
|
||||
tree_sitter_python,
|
||||
};
|
||||
use language_settings::Formatter;
|
||||
use languages::markdown_lang;
|
||||
use languages::rust_lang;
|
||||
use lsp::CompletionParams;
|
||||
use multi_buffer::{IndentGuide, PathKey};
|
||||
@@ -45,8 +44,8 @@ use project::{
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use settings::{
|
||||
AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
|
||||
IndentGuideColoring, ProjectSettingsContent, SearchSettingsContent,
|
||||
AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
|
||||
ProjectSettingsContent,
|
||||
};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
|
||||
use std::{
|
||||
@@ -8315,15 +8314,8 @@ async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut Tes
|
||||
#[gpui::test]
|
||||
async fn test_select_next(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
// Enable case sensitive search.
|
||||
update_test_editor_settings(&mut cx, |settings| {
|
||||
let mut search_settings = SearchSettingsContent::default();
|
||||
search_settings.case_sensitive = Some(true);
|
||||
settings.search = Some(search_settings);
|
||||
});
|
||||
|
||||
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
|
||||
@@ -8354,40 +8346,13 @@ async fn test_select_next(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
|
||||
|
||||
// Test case sensitivity
|
||||
cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.select_next(&SelectNext::default(), window, cx).unwrap();
|
||||
});
|
||||
cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
|
||||
|
||||
// Disable case sensitive search.
|
||||
update_test_editor_settings(&mut cx, |settings| {
|
||||
let mut search_settings = SearchSettingsContent::default();
|
||||
search_settings.case_sensitive = Some(false);
|
||||
settings.search = Some(search_settings);
|
||||
});
|
||||
|
||||
cx.set_state("«ˇfoo»\nFOO\nFoo");
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.select_next(&SelectNext::default(), window, cx).unwrap();
|
||||
e.select_next(&SelectNext::default(), window, cx).unwrap();
|
||||
});
|
||||
cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_all_matches(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
// Enable case sensitive search.
|
||||
update_test_editor_settings(&mut cx, |settings| {
|
||||
let mut search_settings = SearchSettingsContent::default();
|
||||
search_settings.case_sensitive = Some(true);
|
||||
settings.search = Some(search_settings);
|
||||
});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
// Test caret-only selections
|
||||
cx.set_state("abc\nˇabc abc\ndefabc\nabc");
|
||||
@@ -8433,26 +8398,6 @@ async fn test_select_all_matches(cx: &mut TestAppContext) {
|
||||
e.set_clip_at_line_ends(false, cx);
|
||||
});
|
||||
cx.assert_editor_state("«abcˇ»");
|
||||
|
||||
// Test case sensitivity
|
||||
cx.set_state("fˇoo\nFOO\nFoo");
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
|
||||
});
|
||||
cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
|
||||
|
||||
// Disable case sensitive search.
|
||||
update_test_editor_settings(&mut cx, |settings| {
|
||||
let mut search_settings = SearchSettingsContent::default();
|
||||
search_settings.case_sensitive = Some(false);
|
||||
settings.search = Some(search_settings);
|
||||
});
|
||||
|
||||
cx.set_state("fˇoo\nFOO\nFoo");
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
|
||||
});
|
||||
cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -8824,15 +8769,8 @@ let foo = «2ˇ»;"#,
|
||||
#[gpui::test]
|
||||
async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
// Enable case sensitive search.
|
||||
update_test_editor_settings(&mut cx, |settings| {
|
||||
let mut search_settings = SearchSettingsContent::default();
|
||||
search_settings.case_sensitive = Some(true);
|
||||
settings.search = Some(search_settings);
|
||||
});
|
||||
|
||||
cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
|
||||
@@ -8857,32 +8795,6 @@ async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
|
||||
|
||||
// Test case sensitivity
|
||||
cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.select_previous(&SelectPrevious::default(), window, cx)
|
||||
.unwrap();
|
||||
e.select_previous(&SelectPrevious::default(), window, cx)
|
||||
.unwrap();
|
||||
});
|
||||
cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
|
||||
|
||||
// Disable case sensitive search.
|
||||
update_test_editor_settings(&mut cx, |settings| {
|
||||
let mut search_settings = SearchSettingsContent::default();
|
||||
search_settings.case_sensitive = Some(false);
|
||||
settings.search = Some(search_settings);
|
||||
});
|
||||
|
||||
cx.set_state("foo\nFOO\n«ˇFoo»");
|
||||
cx.update_editor(|e, window, cx| {
|
||||
e.select_previous(&SelectPrevious::default(), window, cx)
|
||||
.unwrap();
|
||||
e.select_previous(&SelectPrevious::default(), window, cx)
|
||||
.unwrap();
|
||||
});
|
||||
cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -11415,53 +11327,6 @@ async fn test_snippet_indentation(cx: &mut TestAppContext) {
|
||||
ˇ"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.project().unwrap().update(cx, |project, cx| {
|
||||
project.snippets().update(cx, |snippets, _cx| {
|
||||
let snippet = project::snippet_provider::Snippet {
|
||||
prefix: vec!["multi word".to_string()],
|
||||
body: "this is many words".to_string(),
|
||||
description: Some("description".to_string()),
|
||||
name: "multi-word snippet test".to_string(),
|
||||
};
|
||||
snippets.add_snippet_for_test(
|
||||
None,
|
||||
PathBuf::from("test_snippets.json"),
|
||||
vec![Arc::new(snippet)],
|
||||
);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
for (input_to_simulate, should_match_snippet) in [
|
||||
("m", true),
|
||||
("m ", true),
|
||||
("m w", true),
|
||||
("aa m w", true),
|
||||
("aa m g", false),
|
||||
] {
|
||||
cx.set_state("ˇ");
|
||||
cx.simulate_input(input_to_simulate); // fails correctly
|
||||
|
||||
cx.update_editor(|editor, _, _| {
|
||||
let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
|
||||
else {
|
||||
assert!(!should_match_snippet); // no completions! don't even show the menu
|
||||
return;
|
||||
};
|
||||
assert!(context_menu.visible());
|
||||
let completions = context_menu.completions.borrow();
|
||||
|
||||
assert_eq!(!completions.is_empty(), should_match_snippet);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_document_format_during_save(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -17417,41 +17282,6 @@ fn test_split_words() {
|
||||
assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_words_for_snippet_prefix() {
|
||||
fn split(text: &str) -> Vec<&str> {
|
||||
snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
|
||||
}
|
||||
|
||||
assert_eq!(split("HelloWorld"), &["HelloWorld"]);
|
||||
assert_eq!(split("hello_world"), &["hello_world"]);
|
||||
assert_eq!(split("_hello_world_"), &["_hello_world_"]);
|
||||
assert_eq!(split("Hello_World"), &["Hello_World"]);
|
||||
assert_eq!(split("helloWOrld"), &["helloWOrld"]);
|
||||
assert_eq!(split("helloworld"), &["helloworld"]);
|
||||
assert_eq!(
|
||||
split("this@is!@#$^many . symbols"),
|
||||
&[
|
||||
"symbols",
|
||||
" symbols",
|
||||
". symbols",
|
||||
" . symbols",
|
||||
" . symbols",
|
||||
" . symbols",
|
||||
"many . symbols",
|
||||
"^many . symbols",
|
||||
"$^many . symbols",
|
||||
"#$^many . symbols",
|
||||
"@#$^many . symbols",
|
||||
"!@#$^many . symbols",
|
||||
"is!@#$^many . symbols",
|
||||
"@is!@#$^many . symbols",
|
||||
"this@is!@#$^many . symbols",
|
||||
],
|
||||
);
|
||||
assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -17504,89 +17334,6 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
|
||||
language_registry.add(markdown_lang());
|
||||
language_registry.add(rust_lang());
|
||||
let buffer = cx.new(|cx| {
|
||||
let mut buffer = language::Buffer::local(
|
||||
indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}
|
||||
}
|
||||
```
|
||||
"},
|
||||
cx,
|
||||
);
|
||||
buffer.set_language_registry(language_registry.clone());
|
||||
buffer.set_language(Some(markdown_lang()), cx);
|
||||
buffer
|
||||
});
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Case 1: Test outer enclosing brackets
|
||||
select_ranges(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}
|
||||
}ˇ
|
||||
```
|
||||
"},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree ˇ{
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}
|
||||
}
|
||||
```
|
||||
"},
|
||||
cx,
|
||||
);
|
||||
// Case 2: Test inner enclosing brackets
|
||||
select_ranges(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}ˇ
|
||||
}
|
||||
```
|
||||
"},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
|
||||
}
|
||||
}
|
||||
```
|
||||
"},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -22544,7 +22291,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
|
||||
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\naaaa\nBbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
|
||||
"\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
|
||||
"After unfolding the first buffer, its and 2nd buffer's text should be displayed"
|
||||
);
|
||||
|
||||
@@ -22553,7 +22300,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) {
|
||||
});
|
||||
assert_eq!(
|
||||
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
"\n\naaaa\nBbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
|
||||
"\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
|
||||
"After unfolding the all buffers, all original text should be displayed"
|
||||
);
|
||||
}
|
||||
@@ -25786,195 +25533,6 @@ pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorL
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
cx.lsp
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "unsafe".into(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 0,
|
||||
character: 9,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 0,
|
||||
character: 11,
|
||||
},
|
||||
},
|
||||
new_text: "unsafe".to_string(),
|
||||
})),
|
||||
insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.project().unwrap().update(cx, |project, cx| {
|
||||
project.snippets().update(cx, |snippets, _cx| {
|
||||
snippets.add_snippet_for_test(
|
||||
None,
|
||||
PathBuf::from("test_snippets.json"),
|
||||
vec![
|
||||
Arc::new(project::snippet_provider::Snippet {
|
||||
prefix: vec![
|
||||
"unlimited word count".to_string(),
|
||||
"unlimit word count".to_string(),
|
||||
"unlimited unknown".to_string(),
|
||||
],
|
||||
body: "this is many words".to_string(),
|
||||
description: Some("description".to_string()),
|
||||
name: "multi-word snippet test".to_string(),
|
||||
}),
|
||||
Arc::new(project::snippet_provider::Snippet {
|
||||
prefix: vec!["unsnip".to_string(), "@few".to_string()],
|
||||
body: "fewer words".to_string(),
|
||||
description: Some("alt description".to_string()),
|
||||
name: "other name".to_string(),
|
||||
}),
|
||||
Arc::new(project::snippet_provider::Snippet {
|
||||
prefix: vec!["ab aa".to_string()],
|
||||
body: "abcd".to_string(),
|
||||
description: None,
|
||||
name: "alphabet".to_string(),
|
||||
}),
|
||||
],
|
||||
);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
let get_completions = |cx: &mut EditorLspTestContext| {
|
||||
cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
|
||||
Some(CodeContextMenu::Completions(context_menu)) => {
|
||||
let entries = context_menu.entries.borrow();
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| entry.string.clone())
|
||||
.collect_vec()
|
||||
}
|
||||
_ => vec![],
|
||||
})
|
||||
};
|
||||
|
||||
// snippets:
|
||||
// @foo
|
||||
// foo bar
|
||||
//
|
||||
// when typing:
|
||||
//
|
||||
// when typing:
|
||||
// - if I type a symbol "open the completions with snippets only"
|
||||
// - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
|
||||
//
|
||||
// stuff we need:
|
||||
// - filtering logic change?
|
||||
// - remember how far back the completion started.
|
||||
|
||||
let test_cases: &[(&str, &[&str])] = &[
|
||||
(
|
||||
"un",
|
||||
&[
|
||||
"unsafe",
|
||||
"unlimit word count",
|
||||
"unlimited unknown",
|
||||
"unlimited word count",
|
||||
"unsnip",
|
||||
],
|
||||
),
|
||||
(
|
||||
"u ",
|
||||
&[
|
||||
"unlimit word count",
|
||||
"unlimited unknown",
|
||||
"unlimited word count",
|
||||
],
|
||||
),
|
||||
("u a", &["ab aa", "unsafe"]), // unsAfe
|
||||
(
|
||||
"u u",
|
||||
&[
|
||||
"unsafe",
|
||||
"unlimit word count",
|
||||
"unlimited unknown", // ranked highest among snippets
|
||||
"unlimited word count",
|
||||
"unsnip",
|
||||
],
|
||||
),
|
||||
("uw c", &["unlimit word count", "unlimited word count"]),
|
||||
(
|
||||
"u w",
|
||||
&[
|
||||
"unlimit word count",
|
||||
"unlimited word count",
|
||||
"unlimited unknown",
|
||||
],
|
||||
),
|
||||
("u w ", &["unlimit word count", "unlimited word count"]),
|
||||
(
|
||||
"u ",
|
||||
&[
|
||||
"unlimit word count",
|
||||
"unlimited unknown",
|
||||
"unlimited word count",
|
||||
],
|
||||
),
|
||||
("wor", &[]),
|
||||
("uf", &["unsafe"]),
|
||||
("af", &["unsafe"]),
|
||||
("afu", &[]),
|
||||
(
|
||||
"ue",
|
||||
&["unsafe", "unlimited unknown", "unlimited word count"],
|
||||
),
|
||||
("@", &["@few"]),
|
||||
("@few", &["@few"]),
|
||||
("@ ", &[]),
|
||||
("a@", &["@few"]),
|
||||
("a@f", &["@few", "unsafe"]),
|
||||
("a@fw", &["@few"]),
|
||||
("a", &["ab aa", "unsafe"]),
|
||||
("aa", &["ab aa"]),
|
||||
("aaa", &["ab aa"]),
|
||||
("ab", &["ab aa"]),
|
||||
("ab ", &["ab aa"]),
|
||||
("ab a", &["ab aa", "unsafe"]),
|
||||
("ab ab", &["ab aa"]),
|
||||
("ab ab aa", &["ab aa"]),
|
||||
];
|
||||
|
||||
for &(input_to_simulate, expected_completions) in test_cases {
|
||||
cx.set_state("fn a() { ˇ }\n");
|
||||
for c in input_to_simulate.split("") {
|
||||
cx.simulate_input(c);
|
||||
cx.run_until_parked();
|
||||
}
|
||||
let expected_completions = expected_completions
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect_vec();
|
||||
assert_eq!(
|
||||
get_completions(&mut cx),
|
||||
expected_completions,
|
||||
"< actual / expected >, input = {input_to_simulate:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle completion request passing a marked string specifying where the completion
|
||||
/// should be triggered from using '|' character, what range should be replaced, and what completions
|
||||
/// should be returned using '<' and '>' to delimit the range.
|
||||
@@ -26159,17 +25717,6 @@ pub(crate) fn update_test_project_settings(
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn update_test_editor_settings(
|
||||
cx: &mut TestAppContext,
|
||||
f: impl Fn(&mut EditorSettingsContent),
|
||||
) {
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| f(&mut settings.editor));
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
|
||||
cx.update(|cx| {
|
||||
assets::Assets.load_test_fonts(cx);
|
||||
@@ -27459,60 +27006,6 @@ async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
|
||||
cx.assert_editor_state("line1\nline2\nˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.set_state("ˇline1\nˇline2\nˇline3\n");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
|
||||
|
||||
let clipboard_text = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.text().as_deref().map(str::to_string));
|
||||
|
||||
assert_eq!(
|
||||
clipboard_text,
|
||||
Some("line1\nline2\nline3\n".to_string()),
|
||||
"Copying multiple lines should include a single newline between lines"
|
||||
);
|
||||
|
||||
cx.set_state("lineA\nˇ");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
|
||||
|
||||
cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.set_state("ˇline1\nˇline2\nˇline3\n");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
|
||||
|
||||
let clipboard_text = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.text().as_deref().map(str::to_string));
|
||||
|
||||
assert_eq!(
|
||||
clipboard_text,
|
||||
Some("line1\nline2\nline3\n".to_string()),
|
||||
"Copying multiple lines should include a single newline between lines"
|
||||
);
|
||||
|
||||
cx.set_state("lineA\nˇ");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
|
||||
|
||||
cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_end_of_editor_context(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -27862,213 +27355,3 @@ async fn test_next_prev_reference(cx: &mut TestAppContext) {
|
||||
_move(Direction::Prev, 2, &mut cx).await;
|
||||
cx.assert_editor_state(CYCLE_POSITIONS[1]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
let multi_buffer = MultiBuffer::build_multi(
|
||||
[
|
||||
("1\n2\n3\n", vec![Point::row_range(0..3)]),
|
||||
("1\n2\n3\n", vec![Point::row_range(0..3)]),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
|
||||
});
|
||||
|
||||
let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
|
||||
let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
|
||||
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
ˇ1
|
||||
2
|
||||
3
|
||||
[EXCERPT]
|
||||
1
|
||||
2
|
||||
3
|
||||
"});
|
||||
|
||||
// Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.change_selections(None.into(), window, cx, |s| {
|
||||
s.select_ranges([2..3]);
|
||||
});
|
||||
});
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
1
|
||||
2ˇ
|
||||
3
|
||||
[EXCERPT]
|
||||
1
|
||||
2
|
||||
3
|
||||
"});
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor
|
||||
.select_all_matches(&SelectAllMatches, window, cx)
|
||||
.unwrap();
|
||||
});
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
1
|
||||
2ˇ
|
||||
3
|
||||
[EXCERPT]
|
||||
1
|
||||
2ˇ
|
||||
3
|
||||
"});
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("X", window, cx);
|
||||
});
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
1
|
||||
Xˇ
|
||||
3
|
||||
[EXCERPT]
|
||||
1
|
||||
Xˇ
|
||||
3
|
||||
"});
|
||||
|
||||
// Scenario 2: Select "2", then fold second buffer before insertion
|
||||
cx.update_multibuffer(|mb, cx| {
|
||||
for buffer_id in buffer_ids.iter() {
|
||||
let buffer = mb.buffer(*buffer_id).unwrap();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Select "2" and select all matches
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.change_selections(None.into(), window, cx, |s| {
|
||||
s.select_ranges([2..3]);
|
||||
});
|
||||
editor
|
||||
.select_all_matches(&SelectAllMatches, window, cx)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Fold second buffer - should remove selections from folded buffer
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.fold_buffer(buffer_ids[1], cx);
|
||||
});
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
1
|
||||
2ˇ
|
||||
3
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"});
|
||||
|
||||
// Insert text - should only affect first buffer
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("Y", window, cx);
|
||||
});
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.unfold_buffer(buffer_ids[1], cx);
|
||||
});
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
1
|
||||
Yˇ
|
||||
3
|
||||
[EXCERPT]
|
||||
1
|
||||
2
|
||||
3
|
||||
"});
|
||||
|
||||
// Scenario 3: Select "2", then fold first buffer before insertion
|
||||
cx.update_multibuffer(|mb, cx| {
|
||||
for buffer_id in buffer_ids.iter() {
|
||||
let buffer = mb.buffer(*buffer_id).unwrap();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Select "2" and select all matches
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.change_selections(None.into(), window, cx, |s| {
|
||||
s.select_ranges([2..3]);
|
||||
});
|
||||
editor
|
||||
.select_all_matches(&SelectAllMatches, window, cx)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Fold first buffer - should remove selections from folded buffer
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.fold_buffer(buffer_ids[0], cx);
|
||||
});
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
[EXCERPT]
|
||||
1
|
||||
2ˇ
|
||||
3
|
||||
"});
|
||||
|
||||
// Insert text - should only affect second buffer
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("Z", window, cx);
|
||||
});
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.unfold_buffer(buffer_ids[0], cx);
|
||||
});
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
1
|
||||
2
|
||||
3
|
||||
[EXCERPT]
|
||||
1
|
||||
Zˇ
|
||||
3
|
||||
"});
|
||||
|
||||
// Edge case scenario: fold all buffers, then try to insert
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.fold_buffer(buffer_ids[0], cx);
|
||||
editor.fold_buffer(buffer_ids[1], cx);
|
||||
});
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
ˇ[FOLDED]
|
||||
[EXCERPT]
|
||||
[FOLDED]
|
||||
"});
|
||||
|
||||
// Insert should work via default selection
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("W", window, cx);
|
||||
});
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
editor.unfold_buffer(buffer_ids[0], cx);
|
||||
editor.unfold_buffer(buffer_ids[1], cx);
|
||||
});
|
||||
cx.assert_excerpts_with_selections(indoc! {"
|
||||
[EXCERPT]
|
||||
Wˇ1
|
||||
2
|
||||
3
|
||||
[EXCERPT]
|
||||
1
|
||||
Z
|
||||
3
|
||||
"});
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ use crate::{
|
||||
EditDisplayMode, EditPrediction, Editor, EditorMode, EditorSettings, EditorSnapshot,
|
||||
EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp,
|
||||
HandleInput, HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp,
|
||||
MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown,
|
||||
PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
|
||||
SelectedTextHighlight, Selection, SelectionDragState, SelectionEffects, SizingBehavior,
|
||||
SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
|
||||
MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
|
||||
OpenExcerptsSplit, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt,
|
||||
SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SelectionEffects,
|
||||
SizingBehavior, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
|
||||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||||
display_map::{
|
||||
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
|
||||
@@ -3664,7 +3664,6 @@ impl EditorElement {
|
||||
row_block_types: &mut HashMap<DisplayRow, bool>,
|
||||
selections: &[Selection<Point>],
|
||||
selected_buffer_ids: &Vec<BufferId>,
|
||||
latest_selection_anchors: &HashMap<BufferId, Anchor>,
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
sticky_header_excerpt_id: Option<ExcerptId>,
|
||||
window: &mut Window,
|
||||
@@ -3740,13 +3739,7 @@ impl EditorElement {
|
||||
let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
|
||||
let result = v_flex().id(block_id).w_full().pr(editor_margins.right);
|
||||
|
||||
let jump_data = header_jump_data(
|
||||
snapshot,
|
||||
block_row_start,
|
||||
*height,
|
||||
first_excerpt,
|
||||
latest_selection_anchors,
|
||||
);
|
||||
let jump_data = header_jump_data(snapshot, block_row_start, *height, first_excerpt);
|
||||
result
|
||||
.child(self.render_buffer_header(
|
||||
first_excerpt,
|
||||
@@ -3781,13 +3774,7 @@ impl EditorElement {
|
||||
Block::BufferHeader { excerpt, height } => {
|
||||
let mut result = v_flex().id(block_id).w_full();
|
||||
|
||||
let jump_data = header_jump_data(
|
||||
snapshot,
|
||||
block_row_start,
|
||||
*height,
|
||||
excerpt,
|
||||
latest_selection_anchors,
|
||||
);
|
||||
let jump_data = header_jump_data(snapshot, block_row_start, *height, excerpt);
|
||||
|
||||
if sticky_header_excerpt_id != Some(excerpt.id) {
|
||||
let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
|
||||
@@ -4055,17 +4042,24 @@ impl EditorElement {
|
||||
)
|
||||
.group_hover("", |div| div.underline()),
|
||||
)
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
let jump_data = jump_data.clone();
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |event, window, cx| {
|
||||
if event.modifiers().secondary() {
|
||||
focus_handle.dispatch_action(
|
||||
&OpenExcerptsSplit,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
focus_handle.dispatch_action(
|
||||
&OpenExcerpts,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
})),
|
||||
}),
|
||||
)
|
||||
.when_some(parent_path, |then, path| {
|
||||
then.child(div().child(path).text_color(
|
||||
@@ -4093,17 +4087,24 @@ impl EditorElement {
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.on_click(window.listener_for(&self.editor, {
|
||||
let jump_data = jump_data.clone();
|
||||
move |editor, e: &ClickEvent, window, cx| {
|
||||
editor.open_excerpts_common(
|
||||
Some(jump_data.clone()),
|
||||
e.modifiers().secondary(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |event, window, cx| {
|
||||
if event.modifiers().secondary() {
|
||||
focus_handle.dispatch_action(
|
||||
&OpenExcerptsSplit,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
focus_handle.dispatch_action(
|
||||
&OpenExcerpts,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
})),
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -4249,7 +4250,6 @@ impl EditorElement {
|
||||
line_layouts: &mut [LineWithInvisibles],
|
||||
selections: &[Selection<Point>],
|
||||
selected_buffer_ids: &Vec<BufferId>,
|
||||
latest_selection_anchors: &HashMap<BufferId, Anchor>,
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
sticky_header_excerpt_id: Option<ExcerptId>,
|
||||
window: &mut Window,
|
||||
@@ -4293,7 +4293,6 @@ impl EditorElement {
|
||||
&mut row_block_types,
|
||||
selections,
|
||||
selected_buffer_ids,
|
||||
latest_selection_anchors,
|
||||
is_row_soft_wrapped,
|
||||
sticky_header_excerpt_id,
|
||||
window,
|
||||
@@ -4351,7 +4350,6 @@ impl EditorElement {
|
||||
&mut row_block_types,
|
||||
selections,
|
||||
selected_buffer_ids,
|
||||
latest_selection_anchors,
|
||||
is_row_soft_wrapped,
|
||||
sticky_header_excerpt_id,
|
||||
window,
|
||||
@@ -4407,7 +4405,6 @@ impl EditorElement {
|
||||
&mut row_block_types,
|
||||
selections,
|
||||
selected_buffer_ids,
|
||||
latest_selection_anchors,
|
||||
is_row_soft_wrapped,
|
||||
sticky_header_excerpt_id,
|
||||
window,
|
||||
@@ -4490,7 +4487,6 @@ impl EditorElement {
|
||||
hitbox: &Hitbox,
|
||||
selected_buffer_ids: &Vec<BufferId>,
|
||||
blocks: &[BlockLayout],
|
||||
latest_selection_anchors: &HashMap<BufferId, Anchor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
@@ -4499,7 +4495,6 @@ impl EditorElement {
|
||||
DisplayRow(scroll_position.y as u32),
|
||||
FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
excerpt,
|
||||
latest_selection_anchors,
|
||||
);
|
||||
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
@@ -7799,52 +7794,18 @@ fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
|
||||
}
|
||||
|
||||
fn header_jump_data(
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
block_row_start: DisplayRow,
|
||||
height: u32,
|
||||
first_excerpt: &ExcerptInfo,
|
||||
latest_selection_anchors: &HashMap<BufferId, Anchor>,
|
||||
) -> JumpData {
|
||||
let jump_target = if let Some(anchor) = latest_selection_anchors.get(&first_excerpt.buffer_id)
|
||||
&& let Some(range) = editor_snapshot.context_range_for_excerpt(anchor.excerpt_id)
|
||||
&& let Some(buffer) = editor_snapshot
|
||||
.buffer_snapshot()
|
||||
.buffer_for_excerpt(anchor.excerpt_id)
|
||||
{
|
||||
JumpTargetInExcerptInput {
|
||||
id: anchor.excerpt_id,
|
||||
buffer,
|
||||
excerpt_start_anchor: range.start,
|
||||
jump_anchor: anchor.text_anchor,
|
||||
}
|
||||
} else {
|
||||
JumpTargetInExcerptInput {
|
||||
id: first_excerpt.id,
|
||||
buffer: &first_excerpt.buffer,
|
||||
excerpt_start_anchor: first_excerpt.range.context.start,
|
||||
jump_anchor: first_excerpt.range.primary.start,
|
||||
}
|
||||
};
|
||||
header_jump_data_inner(editor_snapshot, block_row_start, height, &jump_target)
|
||||
}
|
||||
|
||||
struct JumpTargetInExcerptInput<'a> {
|
||||
id: ExcerptId,
|
||||
buffer: &'a language::BufferSnapshot,
|
||||
excerpt_start_anchor: text::Anchor,
|
||||
jump_anchor: text::Anchor,
|
||||
}
|
||||
|
||||
fn header_jump_data_inner(
|
||||
snapshot: &EditorSnapshot,
|
||||
block_row_start: DisplayRow,
|
||||
height: u32,
|
||||
for_excerpt: &JumpTargetInExcerptInput,
|
||||
for_excerpt: &ExcerptInfo,
|
||||
) -> JumpData {
|
||||
let range = &for_excerpt.range;
|
||||
let buffer = &for_excerpt.buffer;
|
||||
let jump_position = language::ToPoint::to_point(&for_excerpt.jump_anchor, buffer);
|
||||
let excerpt_start = for_excerpt.excerpt_start_anchor;
|
||||
let rows_from_excerpt_start = if for_excerpt.jump_anchor == excerpt_start {
|
||||
let jump_anchor = range.primary.start;
|
||||
|
||||
let excerpt_start = range.context.start;
|
||||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||
let rows_from_excerpt_start = if jump_anchor == excerpt_start {
|
||||
0
|
||||
} else {
|
||||
let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, buffer);
|
||||
@@ -7861,7 +7822,7 @@ fn header_jump_data_inner(
|
||||
|
||||
JumpData::MultiBufferPoint {
|
||||
excerpt_id: for_excerpt.id,
|
||||
anchor: for_excerpt.jump_anchor,
|
||||
anchor: jump_anchor,
|
||||
position: jump_position,
|
||||
line_offset_from_top,
|
||||
}
|
||||
@@ -8659,7 +8620,7 @@ impl LineWithInvisibles {
|
||||
let fragment_end_x = fragment_start_x + shaped_line.width;
|
||||
if x < fragment_end_x {
|
||||
return Some(
|
||||
fragment_start_index + shaped_line.index_for_x(x - fragment_start_x)?,
|
||||
fragment_start_index + shaped_line.index_for_x(x - fragment_start_x),
|
||||
);
|
||||
}
|
||||
fragment_start_x = fragment_end_x;
|
||||
@@ -9055,9 +9016,6 @@ impl Element for EditorElement {
|
||||
)
|
||||
});
|
||||
|
||||
if snapshot.scroll_near_end() {
|
||||
dbg!("near end!");
|
||||
}
|
||||
let mut scroll_position = snapshot.scroll_position();
|
||||
// The scroll position is a fractional point, the whole number of which represents
|
||||
// the top of the window in terms of display rows.
|
||||
@@ -9181,18 +9139,15 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
|
||||
let (local_selections, selected_buffer_ids, latest_selection_anchors): (
|
||||
let (local_selections, selected_buffer_ids): (
|
||||
Vec<Selection<Point>>,
|
||||
Vec<BufferId>,
|
||||
HashMap<BufferId, Anchor>,
|
||||
) = self
|
||||
.editor_with_selections(cx)
|
||||
.map(|editor| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let all_selections =
|
||||
editor.selections.all::<Point>(&snapshot.display_snapshot);
|
||||
let all_anchor_selections =
|
||||
editor.selections.all_anchors(&snapshot.display_snapshot);
|
||||
let selected_buffer_ids =
|
||||
if editor.buffer_kind(cx) == ItemBufferKind::Singleton {
|
||||
Vec::new()
|
||||
@@ -9221,31 +9176,10 @@ impl Element for EditorElement {
|
||||
selections
|
||||
.extend(editor.selections.pending(&snapshot.display_snapshot));
|
||||
|
||||
let mut anchors_by_buffer: HashMap<BufferId, (usize, Anchor)> =
|
||||
HashMap::default();
|
||||
for selection in all_anchor_selections.iter() {
|
||||
let head = selection.head();
|
||||
if let Some(buffer_id) = head.buffer_id {
|
||||
anchors_by_buffer
|
||||
.entry(buffer_id)
|
||||
.and_modify(|(latest_id, latest_anchor)| {
|
||||
if selection.id > *latest_id {
|
||||
*latest_id = selection.id;
|
||||
*latest_anchor = head;
|
||||
}
|
||||
})
|
||||
.or_insert((selection.id, head));
|
||||
}
|
||||
}
|
||||
let latest_selection_anchors = anchors_by_buffer
|
||||
.into_iter()
|
||||
.map(|(buffer_id, (_, anchor))| (buffer_id, anchor))
|
||||
.collect();
|
||||
|
||||
(selections, selected_buffer_ids, latest_selection_anchors)
|
||||
(selections, selected_buffer_ids)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| (Vec::new(), Vec::new(), HashMap::default()));
|
||||
.unwrap_or_default();
|
||||
|
||||
let (selections, mut active_rows, newest_selection_head) = self
|
||||
.layout_selections(
|
||||
@@ -9476,7 +9410,6 @@ impl Element for EditorElement {
|
||||
&mut line_layouts,
|
||||
&local_selections,
|
||||
&selected_buffer_ids,
|
||||
&latest_selection_anchors,
|
||||
is_row_soft_wrapped,
|
||||
sticky_header_excerpt_id,
|
||||
window,
|
||||
@@ -9510,7 +9443,6 @@ impl Element for EditorElement {
|
||||
&hitbox,
|
||||
&selected_buffer_ids,
|
||||
&blocks,
|
||||
&latest_selection_anchors,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -341,13 +341,7 @@ fn show_hover(
|
||||
renderer
|
||||
.as_ref()
|
||||
.and_then(|renderer| {
|
||||
renderer.render_hover(
|
||||
group,
|
||||
point_range,
|
||||
buffer_id,
|
||||
language_registry.clone(),
|
||||
cx,
|
||||
)
|
||||
renderer.render_hover(group, point_range, buffer_id, cx)
|
||||
})
|
||||
.context("no rendered diagnostic")
|
||||
})??;
|
||||
@@ -992,11 +986,6 @@ impl DiagnosticPopover {
|
||||
self.markdown.clone(),
|
||||
diagnostics_markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
copy_button_on_hover: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(
|
||||
move |link, window, cx| {
|
||||
if let Some(renderer) = GlobalDiagnosticRenderer::global(cx)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::{
|
||||
collections::hash_map,
|
||||
ops::{ControlFlow, Range},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -779,7 +778,6 @@ impl Editor {
|
||||
}
|
||||
|
||||
let excerpts = self.buffer.read(cx).excerpt_ids();
|
||||
let mut inserted_hint_text = HashMap::default();
|
||||
let hints_to_insert = new_hints
|
||||
.into_iter()
|
||||
.filter_map(|(chunk_range, hints_result)| {
|
||||
@@ -806,35 +804,8 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
})
|
||||
.flat_map(|new_hints| {
|
||||
let mut hints_deduplicated = Vec::new();
|
||||
|
||||
if new_hints.len() > 1 {
|
||||
for (server_id, new_hints) in new_hints {
|
||||
for (new_id, new_hint) in new_hints {
|
||||
let hints_text_for_position = inserted_hint_text
|
||||
.entry(new_hint.position)
|
||||
.or_insert_with(HashMap::default);
|
||||
let insert =
|
||||
match hints_text_for_position.entry(new_hint.text().to_string()) {
|
||||
hash_map::Entry::Occupied(o) => o.get() == &server_id,
|
||||
hash_map::Entry::Vacant(v) => {
|
||||
v.insert(server_id);
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if insert {
|
||||
hints_deduplicated.push((new_id, new_hint));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hints_deduplicated.extend(new_hints.into_values().flatten());
|
||||
}
|
||||
|
||||
hints_deduplicated
|
||||
})
|
||||
.flat_map(|hints| hints.into_values())
|
||||
.flatten()
|
||||
.filter_map(|(hint_id, lsp_hint)| {
|
||||
if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
|
||||
&& inlay_hints
|
||||
@@ -3761,7 +3732,6 @@ let c = 3;"#
|
||||
let mut fake_servers = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "rust-analyzer",
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
@@ -3834,78 +3804,6 @@ let c = 3;"#
|
||||
},
|
||||
);
|
||||
|
||||
// Add another server that does send the same, duplicate hints back
|
||||
let mut fake_servers_2 = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "CrabLang-ls",
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
initializer: Some(Box::new(move |fake_server| {
|
||||
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
|
||||
move |params, _| async move {
|
||||
if params.text_document.uri
|
||||
== lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
|
||||
{
|
||||
Ok(Some(vec![
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(1, 9),
|
||||
label: lsp::InlayHintLabel::String(": i32".to_owned()),
|
||||
kind: Some(lsp::InlayHintKind::TYPE),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(19, 9),
|
||||
label: lsp::InlayHintLabel::String(": i33".to_owned()),
|
||||
kind: Some(lsp::InlayHintKind::TYPE),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
]))
|
||||
} else if params.text_document.uri
|
||||
== lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
|
||||
{
|
||||
Ok(Some(vec![
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(1, 10),
|
||||
label: lsp::InlayHintLabel::String(": i34".to_owned()),
|
||||
kind: Some(lsp::InlayHintKind::TYPE),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
lsp::InlayHint {
|
||||
position: lsp::Position::new(29, 10),
|
||||
label: lsp::InlayHintLabel::String(": i35".to_owned()),
|
||||
kind: Some(lsp::InlayHintKind::TYPE),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
},
|
||||
]))
|
||||
} else {
|
||||
panic!("Unexpected file path {:?}", params.text_document.uri);
|
||||
}
|
||||
},
|
||||
);
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
let (buffer_1, _handle_1) = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
|
||||
@@ -3949,7 +3847,6 @@ let c = 3;"#
|
||||
});
|
||||
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
let _fake_server_2 = fake_servers_2.next().await.unwrap();
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
@@ -3958,16 +3855,11 @@ let c = 3;"#
|
||||
assert_eq!(
|
||||
vec![
|
||||
": i32".to_string(),
|
||||
": i32".to_string(),
|
||||
": i33".to_string(),
|
||||
": i33".to_string(),
|
||||
": i34".to_string(),
|
||||
": i34".to_string(),
|
||||
": i35".to_string(),
|
||||
": i35".to_string(),
|
||||
],
|
||||
sorted_cached_hint_labels(editor, cx),
|
||||
"We receive duplicate hints from 2 servers and cache them all"
|
||||
);
|
||||
assert_eq!(
|
||||
vec![
|
||||
@@ -3977,7 +3869,7 @@ let c = 3;"#
|
||||
": i33".to_string(),
|
||||
],
|
||||
visible_hint_labels(editor, cx),
|
||||
"lib.rs is added before main.rs , so its excerpts should be visible first; hints should be deduplicated per label"
|
||||
"lib.rs is added before main.rs , so its excerpts should be visible first"
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4027,12 +3919,8 @@ let c = 3;"#
|
||||
assert_eq!(
|
||||
vec![
|
||||
": i32".to_string(),
|
||||
": i32".to_string(),
|
||||
": i33".to_string(),
|
||||
": i33".to_string(),
|
||||
": i34".to_string(),
|
||||
": i34".to_string(),
|
||||
": i35".to_string(),
|
||||
": i35".to_string(),
|
||||
],
|
||||
sorted_cached_hint_labels(editor, cx),
|
||||
|
||||
@@ -1586,11 +1586,12 @@ impl SearchableItem for Editor {
|
||||
&mut self,
|
||||
index: usize,
|
||||
matches: &[Range<Anchor>],
|
||||
collapse: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.unfold_ranges(&[matches[index].clone()], false, true, cx);
|
||||
let range = self.range_for_match(&matches[index]);
|
||||
let range = self.range_for_match(&matches[index], collapse);
|
||||
let autoscroll = if EditorSettings::get_global(cx).search.center_on_match {
|
||||
Autoscroll::center()
|
||||
} else {
|
||||
@@ -1795,14 +1796,6 @@ impl SearchableItem for Editor {
|
||||
fn search_bar_visibility_changed(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {
|
||||
self.expect_bounds_change = self.last_bounds;
|
||||
}
|
||||
|
||||
fn set_search_is_case_sensitive(
|
||||
&mut self,
|
||||
case_sensitive: Option<bool>,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
self.select_next_is_case_sensitive = case_sensitive;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_match_index(
|
||||
|
||||
@@ -81,19 +81,7 @@ impl MouseContextMenu {
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Self {
|
||||
let context_menu_focus = context_menu.focus_handle(cx);
|
||||
|
||||
// Since `ContextMenu` is rendered in a deferred fashion its focus
|
||||
// handle is not linked to the Editor's until after the deferred draw
|
||||
// callback runs.
|
||||
// We need to wait for that to happen before focusing it, so that
|
||||
// calling `contains_focused` on the editor's focus handle returns
|
||||
// `true` when the `ContextMenu` is focused.
|
||||
let focus_handle = context_menu_focus.clone();
|
||||
cx.on_next_frame(window, move |_, window, cx| {
|
||||
cx.on_next_frame(window, move |_, window, _cx| {
|
||||
window.focus(&focus_handle);
|
||||
});
|
||||
});
|
||||
window.focus(&context_menu_focus);
|
||||
|
||||
let _dismiss_subscription = cx.subscribe_in(&context_menu, window, {
|
||||
let context_menu_focus = context_menu_focus.clone();
|
||||
@@ -341,18 +329,8 @@ mod tests {
|
||||
}
|
||||
"});
|
||||
cx.editor(|editor, _window, _app| assert!(editor.mouse_context_menu.is_none()));
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
deploy_context_menu(editor, Some(Default::default()), point, window, cx);
|
||||
|
||||
// Assert that, even after deploying the editor's mouse context
|
||||
// menu, the editor's focus handle still contains the focused
|
||||
// element. The pane's tab bar relies on this to determine whether
|
||||
// to show the tab bar buttons and there was a small flicker when
|
||||
// deploying the mouse context menu that would cause this to not be
|
||||
// true, making it so that the buttons would disappear for a couple
|
||||
// of frames.
|
||||
assert!(editor.focus_handle.contains_focused(window, cx));
|
||||
deploy_context_menu(editor, Some(Default::default()), point, window, cx)
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
@@ -46,20 +46,12 @@ impl ScrollAnchor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn near_end(&self, snapshot: &DisplaySnapshot) -> bool {
|
||||
let editor_length = snapshot.max_point().row().as_f64();
|
||||
let scroll_top = self.anchor.to_display_point(snapshot).row().as_f64();
|
||||
(scroll_top - editor_length).abs() < 300.0
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<ScrollOffset> {
|
||||
self.offset.apply_along(Axis::Vertical, |offset| {
|
||||
if self.anchor == Anchor::min() {
|
||||
0.
|
||||
} else {
|
||||
dbg!(snapshot.max_point().row().as_f64());
|
||||
let scroll_top = self.anchor.to_display_point(snapshot).row().as_f64();
|
||||
dbg!(scroll_top, offset);
|
||||
(offset + scroll_top).max(0.)
|
||||
}
|
||||
})
|
||||
@@ -251,11 +243,6 @@ impl ScrollManager {
|
||||
}
|
||||
}
|
||||
};
|
||||
let near_end = self.anchor.near_end(map);
|
||||
// // TODO call load more here
|
||||
// if near_end {
|
||||
// cx.read();
|
||||
// }
|
||||
|
||||
let scroll_top_row = DisplayRow(scroll_top as u32);
|
||||
let scroll_top_buffer_point = map
|
||||
|
||||
@@ -372,7 +372,7 @@ impl SelectionsCollection {
|
||||
let is_empty = positions.start == positions.end;
|
||||
let line_len = display_map.line_len(row);
|
||||
let line = display_map.layout_row(row, text_layout_details);
|
||||
let start_col = line.closest_index_for_x(positions.start) as u32;
|
||||
let start_col = line.index_for_x(positions.start) as u32;
|
||||
|
||||
let (start, end) = if is_empty {
|
||||
let point = DisplayPoint::new(row, std::cmp::min(start_col, line_len));
|
||||
@@ -382,7 +382,7 @@ impl SelectionsCollection {
|
||||
return None;
|
||||
}
|
||||
let start = DisplayPoint::new(row, start_col);
|
||||
let end_col = line.closest_index_for_x(positions.end) as u32;
|
||||
let end_col = line.index_for_x(positions.end) as u32;
|
||||
let end = DisplayPoint::new(row, end_col);
|
||||
(start, end)
|
||||
};
|
||||
@@ -487,43 +487,6 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
|
||||
self.selections_changed |= changed;
|
||||
}
|
||||
|
||||
pub fn remove_selections_from_buffer(&mut self, buffer_id: language::BufferId) {
|
||||
let mut changed = false;
|
||||
|
||||
let filtered_selections: Arc<[Selection<Anchor>]> = {
|
||||
self.disjoint
|
||||
.iter()
|
||||
.filter(|selection| {
|
||||
if let Some(selection_buffer_id) =
|
||||
self.snapshot.buffer_id_for_anchor(selection.start)
|
||||
{
|
||||
let should_remove = selection_buffer_id == buffer_id;
|
||||
changed |= should_remove;
|
||||
!should_remove
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
};
|
||||
|
||||
if filtered_selections.is_empty() {
|
||||
let default_anchor = self.snapshot.anchor_before(0);
|
||||
self.collection.disjoint = Arc::from([Selection {
|
||||
id: post_inc(&mut self.collection.next_selection_id),
|
||||
start: default_anchor,
|
||||
end: default_anchor,
|
||||
reversed: false,
|
||||
goal: SelectionGoal::None,
|
||||
}]);
|
||||
} else {
|
||||
self.collection.disjoint = filtered_selections;
|
||||
}
|
||||
|
||||
self.selections_changed |= changed;
|
||||
}
|
||||
|
||||
pub fn clear_pending(&mut self) {
|
||||
if self.collection.pending.is_some() {
|
||||
self.collection.pending = None;
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use language::{markdown_lang, rust_lang};
|
||||
use language::rust_lang;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Editor, ToPoint};
|
||||
@@ -313,22 +313,6 @@ impl EditorLspTestContext {
|
||||
Self::new(language, Default::default(), cx).await
|
||||
}
|
||||
|
||||
pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> Self {
|
||||
let context = Self::new(
|
||||
Arc::into_inner(markdown_lang()).unwrap(),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let language_registry = context.workspace.read_with(cx, |workspace, cx| {
|
||||
workspace.project().read(cx).languages().clone()
|
||||
});
|
||||
language_registry.add(rust_lang());
|
||||
|
||||
context
|
||||
}
|
||||
|
||||
/// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||
#[track_caller]
|
||||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||
|
||||
@@ -59,17 +59,6 @@ impl EditorTestContext {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let language = project
|
||||
.read_with(cx, |project, _cx| {
|
||||
project.languages().language_for_name("Plain Text")
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_language(Some(language), cx);
|
||||
});
|
||||
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
let editor = build_editor_with_project(
|
||||
project,
|
||||
|
||||
@@ -463,8 +463,8 @@ pub fn find_model(
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"No language model with ID {}/{} was available. Available models: {}",
|
||||
selected.provider.0,
|
||||
selected.model.0,
|
||||
selected.provider.0,
|
||||
model_registry
|
||||
.available_models(cx)
|
||||
.map(|model| format!("{}/{}", model.provider_id().0, model.id().0))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user