Compare commits
1 Commits
ex-local
...
editor/tog
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
350e355b15 |
81
.github/workflows/autofix_pr.yml
vendored
81
.github/workflows/autofix_pr.yml
vendored
@@ -9,23 +9,26 @@ on:
|
||||
description: pr_number
|
||||
required: true
|
||||
type: string
|
||||
run_clippy:
|
||||
description: run_clippy
|
||||
type: boolean
|
||||
default: 'true'
|
||||
jobs:
|
||||
run_autofix:
|
||||
runs-on: namespace-profile-16x32-ubuntu-2204
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
- id: get-app-token
|
||||
name: autofix_pr::run_autofix::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: steps::checkout_repo_with_token
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
token: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: autofix_pr::run_autofix::checkout_pr
|
||||
run: gh pr checkout ${{ inputs.pr_number }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: steps::setup_cargo_config
|
||||
run: |
|
||||
mkdir -p ./../.cargo
|
||||
@@ -55,74 +58,26 @@ jobs:
|
||||
run: cargo fmt --all
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: autofix_pr::run_autofix::run_clippy_fix
|
||||
if: ${{ inputs.run_clippy }}
|
||||
run: cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: create-patch
|
||||
name: autofix_pr::run_autofix::create_patch
|
||||
- name: autofix_pr::run_autofix::commit_and_push
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "No changes to commit"
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
git diff > autofix.patch
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
git add -A
|
||||
git commit -m "Autofix"
|
||||
git push
|
||||
fi
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: upload artifact autofix-patch
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
with:
|
||||
name: autofix-patch
|
||||
path: autofix.patch
|
||||
if-no-files-found: ignore
|
||||
retention-days: '1'
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
has_changes: ${{ steps.create-patch.outputs.has_changes }}
|
||||
commit_changes:
|
||||
needs:
|
||||
- run_autofix
|
||||
if: needs.run_autofix.outputs.has_changes == 'true'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: steps::checkout_repo_with_token
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
token: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: autofix_pr::commit_changes::checkout_pr
|
||||
run: gh pr checkout ${{ inputs.pr_number }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: autofix_pr::download_patch_artifact
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
with:
|
||||
name: autofix-patch
|
||||
- name: autofix_pr::commit_changes::apply_patch
|
||||
run: git apply autofix.patch
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: autofix_pr::commit_changes::commit_and_push
|
||||
run: |
|
||||
git commit -am "Autofix"
|
||||
git push
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GIT_COMMITTER_NAME: Zed Zippy
|
||||
GIT_COMMITTER_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
|
||||
GIT_AUTHOR_NAME: Zed Zippy
|
||||
GIT_AUTHOR_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
2
.github/workflows/cherry_pick.yml
vendored
2
.github/workflows/cherry_pick.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
name: cherry_pick::run_cherry_pick::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
|
||||
@@ -34,7 +34,6 @@ jobs:
|
||||
CharlesChen0823
|
||||
chbk
|
||||
cppcoffee
|
||||
davidbarsky
|
||||
davewa
|
||||
ddoemonn
|
||||
djsauble
|
||||
|
||||
3
.github/workflows/extension_tests.yml
vendored
3
.github/workflows/extension_tests.yml
vendored
@@ -61,8 +61,7 @@ jobs:
|
||||
uses: namespacelabs/nscloud-cache-action@v1
|
||||
with:
|
||||
cache: rust
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: extension_tests::run_clippy
|
||||
|
||||
24
.github/workflows/release.yml
vendored
24
.github/workflows/release.yml
vendored
@@ -26,8 +26,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -72,15 +71,9 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -94,8 +87,6 @@ jobs:
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_windows:
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||
@@ -114,8 +105,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -482,17 +472,11 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
|
||||
run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
notify_on_failure:
|
||||
needs:
|
||||
- upload_release_assets
|
||||
|
||||
6
.github/workflows/release_nightly.yml
vendored
6
.github/workflows/release_nightly.yml
vendored
@@ -20,8 +20,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/clippy
|
||||
@@ -45,8 +44,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
|
||||
51
.github/workflows/run_tests.yml
vendored
51
.github/workflows/run_tests.yml
vendored
@@ -74,19 +74,9 @@ jobs:
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
with:
|
||||
version: '9'
|
||||
- id: prettier
|
||||
name: steps::prettier
|
||||
- name: ./script/prettier
|
||||
run: ./script/prettier
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_style_failure
|
||||
name: steps::record_style_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/check-todos
|
||||
run: ./script/check-todos
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -97,8 +87,9 @@ jobs:
|
||||
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
|
||||
with:
|
||||
config: ./typos.toml
|
||||
outputs:
|
||||
style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
timeout-minutes: 60
|
||||
run_tests_windows:
|
||||
needs:
|
||||
@@ -119,8 +110,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -167,15 +157,9 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -189,8 +173,6 @@ jobs:
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_mac:
|
||||
needs:
|
||||
@@ -211,8 +193,7 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -592,24 +573,6 @@ jobs:
|
||||
|
||||
exit $EXIT_CODE
|
||||
shell: bash -euxo pipefail {0}
|
||||
call_autofix:
|
||||
needs:
|
||||
- check_style
|
||||
- run_tests_linux
|
||||
if: always() && (needs.check_style.outputs.style_failed == 'true' || needs.run_tests_linux.outputs.clippy_failed == 'true') && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: run_tests::call_autofix::dispatch_autofix
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=${{ needs.run_tests_linux.outputs.clippy_failed == 'true' }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
3
.mailmap
3
.mailmap
@@ -141,9 +141,6 @@ Uladzislau Kaminski <i@uladkaminski.com>
|
||||
Uladzislau Kaminski <i@uladkaminski.com> <uladzislau_kaminski@epam.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
|
||||
Yara <davidsk@zed.dev>
|
||||
Yara <git@davidsk.dev>
|
||||
Yara <git@yara.blue>
|
||||
Will Bradley <williambbradley@gmail.com>
|
||||
Will Bradley <williambbradley@gmail.com> <will@zed.dev>
|
||||
WindSoilder <WindSoilder@outlook.com>
|
||||
|
||||
428
Cargo.lock
generated
428
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -192,7 +192,6 @@ members = [
|
||||
"crates/vercel",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/which_key",
|
||||
"crates/watch",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
@@ -416,7 +415,6 @@ util_macros = { path = "crates/util_macros" }
|
||||
vercel = { path = "crates/vercel" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
which_key = { path = "crates/which_key" }
|
||||
|
||||
watch = { path = "crates/watch" }
|
||||
web_search = { path = "crates/web_search" }
|
||||
@@ -438,7 +436,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
|
||||
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
@@ -457,15 +455,15 @@ async-task = "4.7"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = "0.31.0"
|
||||
async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] }
|
||||
aws-config = { version = "1.8.10", features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { version = "1.2.8", features = [
|
||||
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { version = "1.2.2", features = [
|
||||
"hardcoded-credentials",
|
||||
] }
|
||||
aws-sdk-bedrockruntime = { version = "1.112.0", features = [
|
||||
aws-sdk-bedrockruntime = { version = "1.80.0", features = [
|
||||
"behavior-version-latest",
|
||||
] }
|
||||
aws-smithy-runtime-api = { version = "1.9.2", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.4", features = ["http-body-1-x"] }
|
||||
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.22"
|
||||
bincode = "1.2.1"
|
||||
@@ -478,7 +476,6 @@ bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
cfg-if = "1.0.3"
|
||||
chardetng = "0.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ciborium = "0.2"
|
||||
circular-buffer = "1.0"
|
||||
@@ -502,7 +499,6 @@ dotenvy = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
encoding_rs = "0.8"
|
||||
exec = "0.3.1"
|
||||
fancy-regex = "0.16.0"
|
||||
fork = "0.4.0"
|
||||
@@ -667,7 +663,7 @@ tokio-socks = { version = "0.5.2", default-features = false, features = ["future
|
||||
toml = "0.8"
|
||||
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.26", features = ["wasm"] }
|
||||
tree-sitter = { version = "0.25.10", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.25.1"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
|
||||
@@ -701,7 +697,7 @@ uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
walkdir = "2.5"
|
||||
wasm-encoder = "0.221"
|
||||
wasmparser = "0.221"
|
||||
wasmtime = { version = "33", default-features = false, features = [
|
||||
wasmtime = { version = "29", default-features = false, features = [
|
||||
"async",
|
||||
"demangle",
|
||||
"runtime",
|
||||
@@ -710,7 +706,7 @@ wasmtime = { version = "33", default-features = false, features = [
|
||||
"incremental-cache",
|
||||
"parallel-compilation",
|
||||
] }
|
||||
wasmtime-wasi = "33"
|
||||
wasmtime-wasi = "29"
|
||||
wax = "0.6"
|
||||
which = "6.0.0"
|
||||
windows-core = "0.61"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.92-bookworm as builder
|
||||
FROM rust:1.91.1-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ ai
|
||||
= @rtfeldman
|
||||
|
||||
audio
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
|
||||
crashes
|
||||
= @p1n3appl3
|
||||
@@ -53,7 +53,7 @@ extension
|
||||
git
|
||||
= @cole-miller
|
||||
= @danilo-leal
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
= @kubkon
|
||||
= @Anthony-Eid
|
||||
= @cameron1024
|
||||
@@ -76,7 +76,7 @@ languages
|
||||
|
||||
linux
|
||||
= @cole-miller
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
= @p1n3appl3
|
||||
= @probably-neb
|
||||
= @smitbarmase
|
||||
@@ -92,7 +92,7 @@ multi_buffer
|
||||
= @SomeoneToIgnore
|
||||
|
||||
pickers
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
= @p1n3appl3
|
||||
= @SomeoneToIgnore
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"ctrl-alt-z": "edit_prediction::RatePredictions",
|
||||
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-alt-shift-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -227,7 +226,6 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -253,7 +251,6 @@
|
||||
"ctrl-y": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"ctrl-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -265,9 +262,9 @@
|
||||
{
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-insert": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown",
|
||||
"copy": "markdown::Copy",
|
||||
"ctrl-insert": "markdown::Copy",
|
||||
"ctrl-c": "markdown::Copy",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -294,7 +291,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -306,7 +302,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -350,7 +345,6 @@
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -906,10 +900,8 @@
|
||||
{
|
||||
"context": "GitPanel && ChangesList",
|
||||
"bindings": {
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-y": "git::StageFile",
|
||||
"alt-shift-y": "git::UnstageFile",
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-cmd-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames",
|
||||
"ctrl-cmd-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -266,8 +265,6 @@
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-k l": "agent::OpenRulesLibrary",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -294,7 +291,6 @@
|
||||
"cmd-y": "agent::AllowOnce",
|
||||
"cmd-alt-y": "agent::AllowAlways",
|
||||
"cmd-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -307,7 +303,7 @@
|
||||
"context": "AgentPanel > Markdown",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-c": "markdown::CopyAsMarkdown",
|
||||
"cmd-c": "markdown::Copy",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -336,7 +332,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -349,7 +344,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -391,7 +385,6 @@
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -403,7 +396,6 @@
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -887,7 +879,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "agent::CycleNextInlineAssist",
|
||||
"cmd-shift-enter": "inline_assistant::ThumbsUpResult",
|
||||
@@ -984,12 +975,10 @@
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"cmd-up": "git_panel::FirstEntry",
|
||||
"cmd-down": "git_panel::LastEntry",
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
||||
"shift-alt-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-shift-alt-c": "editor::DisplayCursorNames",
|
||||
"ctrl-shift-alt-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -227,7 +226,6 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -254,7 +252,6 @@
|
||||
"shift-alt-a": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"shift-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -268,7 +265,7 @@
|
||||
"context": "AgentPanel > Markdown",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-c": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::Copy",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -297,7 +294,6 @@
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -310,7 +306,6 @@
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -346,7 +341,6 @@
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -358,7 +352,6 @@
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -911,10 +904,8 @@
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-y": "git::StageFile",
|
||||
"shift-alt-y": "git::UnstageFile",
|
||||
|
||||
@@ -14,6 +14,7 @@ The section you'll need to rewrite is marked with <rewrite_this></rewrite_this>
|
||||
The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
|
||||
{{/if}}
|
||||
|
||||
{{#if rewrite_section}}
|
||||
And here's the section to rewrite based on that prompt again for reference:
|
||||
|
||||
<rewrite_this>
|
||||
@@ -32,6 +33,8 @@ Below are the diagnostic errors visible to the user. If the user requests probl
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
|
||||
|
||||
Start at the indentation level in the original file in the rewritten {{content_type}}.
|
||||
|
||||
@@ -1705,12 +1705,7 @@
|
||||
// }
|
||||
//
|
||||
"file_types": {
|
||||
"JSONC": [
|
||||
"**/.zed/*.json",
|
||||
"**/.vscode/**/*.json",
|
||||
"**/{zed,Zed}/{settings,keymap,tasks,debug}.json",
|
||||
"tsconfig*.json",
|
||||
],
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
|
||||
"Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
|
||||
"Shell Script": [".env.*"],
|
||||
},
|
||||
@@ -2067,12 +2062,6 @@
|
||||
//
|
||||
// Default: true
|
||||
"restore_unsaved_buffers": true,
|
||||
// Whether or not to skip worktree trust checks.
|
||||
// When trusted, project settings are synchronized automatically,
|
||||
// language and MCP servers are downloaded and started automatically.
|
||||
//
|
||||
// Default: false
|
||||
"trust_all_worktrees": false,
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// Allows to enable/disable formatting with Prettier
|
||||
@@ -2157,13 +2146,6 @@
|
||||
// The shape can be one of the following: "block", "bar", "underline", "hollow".
|
||||
"cursor_shape": {},
|
||||
},
|
||||
// Which-key popup settings
|
||||
"which_key": {
|
||||
// Whether to show the which-key popup when holding down key combinations.
|
||||
"enabled": false,
|
||||
// Delay in milliseconds before showing the which-key popup.
|
||||
"delay_ms": 1000,
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
"server_url": "https://zed.dev",
|
||||
|
||||
@@ -43,7 +43,6 @@ pub struct UserMessage {
|
||||
pub content: ContentBlock,
|
||||
pub chunks: Vec<acp::ContentBlock>,
|
||||
pub checkpoint: Option<Checkpoint>,
|
||||
pub indented: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -74,7 +73,6 @@ impl UserMessage {
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct AssistantMessage {
|
||||
pub chunks: Vec<AssistantMessageChunk>,
|
||||
pub indented: bool,
|
||||
}
|
||||
|
||||
impl AssistantMessage {
|
||||
@@ -125,14 +123,6 @@ pub enum AgentThreadEntry {
|
||||
}
|
||||
|
||||
impl AgentThreadEntry {
|
||||
pub fn is_indented(&self) -> bool {
|
||||
match self {
|
||||
Self::UserMessage(message) => message.indented,
|
||||
Self::AssistantMessage(message) => message.indented,
|
||||
Self::ToolCall(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_markdown(&self, cx: &App) -> String {
|
||||
match self {
|
||||
Self::UserMessage(message) => message.to_markdown(cx),
|
||||
@@ -192,7 +182,6 @@ pub struct ToolCall {
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
pub resolved_locations: Vec<Option<AgentLocation>>,
|
||||
pub raw_input: Option<serde_json::Value>,
|
||||
pub raw_input_markdown: Option<Entity<Markdown>>,
|
||||
pub raw_output: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
@@ -223,11 +212,6 @@ impl ToolCall {
|
||||
}
|
||||
}
|
||||
|
||||
let raw_input_markdown = tool_call
|
||||
.raw_input
|
||||
.as_ref()
|
||||
.and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
|
||||
|
||||
let result = Self {
|
||||
id: tool_call.tool_call_id,
|
||||
label: cx
|
||||
@@ -238,7 +222,6 @@ impl ToolCall {
|
||||
resolved_locations: Vec::default(),
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
raw_input_markdown,
|
||||
raw_output: tool_call.raw_output,
|
||||
};
|
||||
Ok(result)
|
||||
@@ -314,7 +297,6 @@ impl ToolCall {
|
||||
}
|
||||
|
||||
if let Some(raw_input) = raw_input {
|
||||
self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
|
||||
self.raw_input = Some(raw_input);
|
||||
}
|
||||
|
||||
@@ -1202,16 +1184,6 @@ impl AcpThread {
|
||||
message_id: Option<UserMessageId>,
|
||||
chunk: acp::ContentBlock,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.push_user_content_block_with_indent(message_id, chunk, false, cx)
|
||||
}
|
||||
|
||||
pub fn push_user_content_block_with_indent(
|
||||
&mut self,
|
||||
message_id: Option<UserMessageId>,
|
||||
chunk: acp::ContentBlock,
|
||||
indented: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
@@ -1222,10 +1194,8 @@ impl AcpThread {
|
||||
id,
|
||||
content,
|
||||
chunks,
|
||||
indented: existing_indented,
|
||||
..
|
||||
}) = last_entry
|
||||
&& *existing_indented == indented
|
||||
{
|
||||
*id = message_id.or(id.take());
|
||||
content.append(chunk.clone(), &language_registry, path_style, cx);
|
||||
@@ -1240,7 +1210,6 @@ impl AcpThread {
|
||||
content,
|
||||
chunks: vec![chunk],
|
||||
checkpoint: None,
|
||||
indented,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -1252,26 +1221,12 @@ impl AcpThread {
|
||||
chunk: acp::ContentBlock,
|
||||
is_thought: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.push_assistant_content_block_with_indent(chunk, is_thought, false, cx)
|
||||
}
|
||||
|
||||
pub fn push_assistant_content_block_with_indent(
|
||||
&mut self,
|
||||
chunk: acp::ContentBlock,
|
||||
is_thought: bool,
|
||||
indented: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
let entries_len = self.entries.len();
|
||||
if let Some(last_entry) = self.entries.last_mut()
|
||||
&& let AgentThreadEntry::AssistantMessage(AssistantMessage {
|
||||
chunks,
|
||||
indented: existing_indented,
|
||||
}) = last_entry
|
||||
&& *existing_indented == indented
|
||||
&& let AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) = last_entry
|
||||
{
|
||||
let idx = entries_len - 1;
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(idx));
|
||||
@@ -1300,7 +1255,6 @@ impl AcpThread {
|
||||
self.push_entry(
|
||||
AgentThreadEntry::AssistantMessage(AssistantMessage {
|
||||
chunks: vec![chunk],
|
||||
indented,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -1363,7 +1317,6 @@ impl AcpThread {
|
||||
locations: Vec::new(),
|
||||
resolved_locations: Vec::new(),
|
||||
raw_input: None,
|
||||
raw_input_markdown: None,
|
||||
raw_output: None,
|
||||
};
|
||||
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
|
||||
@@ -1751,7 +1704,6 @@ impl AcpThread {
|
||||
content: block,
|
||||
chunks: message,
|
||||
checkpoint: None,
|
||||
indented: false,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -202,12 +202,6 @@ pub trait AgentModelSelector: 'static {
|
||||
fn should_render_footer(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether this selector supports the favorites feature.
|
||||
/// Only the native agent uses the model ID format that maps to settings.
|
||||
fn supports_favorites(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -245,10 +239,6 @@ impl AgentModelList {
|
||||
AgentModelList::Grouped(groups) => groups.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_flat(&self) -> bool {
|
||||
matches!(self, AgentModelList::Flat(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
|
||||
@@ -5,12 +5,12 @@ mod legacy_thread;
|
||||
mod native_agent_server;
|
||||
pub mod outline;
|
||||
mod templates;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod thread;
|
||||
mod tools;
|
||||
|
||||
use context_server::ContextServerId;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use db::*;
|
||||
pub use history_store::*;
|
||||
pub use native_agent_server::NativeAgentServer;
|
||||
@@ -18,11 +18,11 @@ pub use templates::*;
|
||||
pub use thread::*;
|
||||
pub use tools::*;
|
||||
|
||||
use acp_thread::{AcpThread, AgentModelSelector, UserMessageId};
|
||||
use acp_thread::{AcpThread, AgentModelSelector};
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use collections::{HashSet, IndexMap};
|
||||
use fs::Fs;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::future::Shared;
|
||||
@@ -39,6 +39,7 @@ use prompt_store::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{LanguageModelSelection, update_settings_file};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
@@ -251,24 +252,12 @@ impl NativeAgent {
|
||||
.await;
|
||||
|
||||
cx.new(|cx| {
|
||||
let context_server_store = project.read(cx).context_server_store();
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
|
||||
|
||||
let mut subscriptions = vec![
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
cx.subscribe(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
Self::handle_models_updated_event,
|
||||
),
|
||||
cx.subscribe(
|
||||
&context_server_store,
|
||||
Self::handle_context_server_store_updated,
|
||||
),
|
||||
cx.subscribe(
|
||||
&context_server_registry,
|
||||
Self::handle_context_server_registry_event,
|
||||
),
|
||||
];
|
||||
if let Some(prompt_store) = prompt_store.as_ref() {
|
||||
subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event))
|
||||
@@ -277,14 +266,16 @@ impl NativeAgent {
|
||||
let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) =
|
||||
watch::channel(());
|
||||
Self {
|
||||
sessions: HashMap::default(),
|
||||
sessions: HashMap::new(),
|
||||
history,
|
||||
project_context: cx.new(|_| project_context),
|
||||
project_context_needs_refresh: project_context_needs_refresh_tx,
|
||||
_maintain_project_context: cx.spawn(async move |this, cx| {
|
||||
Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await
|
||||
}),
|
||||
context_server_registry,
|
||||
context_server_registry: cx.new(|cx| {
|
||||
ContextServerRegistry::new(project.read(cx).context_server_store(), cx)
|
||||
}),
|
||||
templates,
|
||||
models: LanguageModels::new(cx),
|
||||
project,
|
||||
@@ -353,9 +344,6 @@ impl NativeAgent {
|
||||
pending_save: Task::ready(()),
|
||||
},
|
||||
);
|
||||
|
||||
self.update_available_commands(cx);
|
||||
|
||||
acp_thread
|
||||
}
|
||||
|
||||
@@ -426,7 +414,10 @@ impl NativeAgent {
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(UserRulesContext {
|
||||
uuid: prompt_metadata.id.user_id()?,
|
||||
uuid: match prompt_metadata.id {
|
||||
prompt_store::PromptId::User { uuid } => uuid,
|
||||
prompt_store::PromptId::EditWorkflow => return None,
|
||||
},
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
@@ -620,99 +611,6 @@ impl NativeAgent {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_context_server_store_updated(
|
||||
&mut self,
|
||||
_store: Entity<project::context_server_store::ContextServerStore>,
|
||||
_event: &project::context_server_store::Event,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.update_available_commands(cx);
|
||||
}
|
||||
|
||||
fn handle_context_server_registry_event(
|
||||
&mut self,
|
||||
_registry: Entity<ContextServerRegistry>,
|
||||
event: &ContextServerRegistryEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ContextServerRegistryEvent::ToolsChanged => {}
|
||||
ContextServerRegistryEvent::PromptsChanged => {
|
||||
self.update_available_commands(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_available_commands(&self, cx: &mut Context<Self>) {
|
||||
let available_commands = self.build_available_commands(cx);
|
||||
for session in self.sessions.values() {
|
||||
if let Some(acp_thread) = session.acp_thread.upgrade() {
|
||||
acp_thread.update(cx, |thread, cx| {
|
||||
thread
|
||||
.handle_session_update(
|
||||
acp::SessionUpdate::AvailableCommandsUpdate(
|
||||
acp::AvailableCommandsUpdate::new(available_commands.clone()),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_available_commands(&self, cx: &App) -> Vec<acp::AvailableCommand> {
|
||||
let registry = self.context_server_registry.read(cx);
|
||||
|
||||
let mut prompt_name_counts: HashMap<&str, usize> = HashMap::default();
|
||||
for context_server_prompt in registry.prompts() {
|
||||
*prompt_name_counts
|
||||
.entry(context_server_prompt.prompt.name.as_str())
|
||||
.or_insert(0) += 1;
|
||||
}
|
||||
|
||||
registry
|
||||
.prompts()
|
||||
.flat_map(|context_server_prompt| {
|
||||
let prompt = &context_server_prompt.prompt;
|
||||
|
||||
let should_prefix = prompt_name_counts
|
||||
.get(prompt.name.as_str())
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
> 1;
|
||||
|
||||
let name = if should_prefix {
|
||||
format!("{}.{}", context_server_prompt.server_id, prompt.name)
|
||||
} else {
|
||||
prompt.name.clone()
|
||||
};
|
||||
|
||||
let mut command = acp::AvailableCommand::new(
|
||||
name,
|
||||
prompt.description.clone().unwrap_or_default(),
|
||||
);
|
||||
|
||||
match prompt.arguments.as_deref() {
|
||||
Some([arg]) => {
|
||||
let hint = format!("<{}>", arg.name);
|
||||
|
||||
command = command.input(acp::AvailableCommandInput::Unstructured(
|
||||
acp::UnstructuredCommandInput::new(hint),
|
||||
));
|
||||
}
|
||||
Some([]) | None => {}
|
||||
Some(_) => {
|
||||
// skip >1 argument commands since we don't support them yet
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(command)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn load_thread(
|
||||
&mut self,
|
||||
id: acp::SessionId,
|
||||
@@ -811,102 +709,6 @@ impl NativeAgent {
|
||||
history.update(cx, |history, cx| history.reload(cx)).ok();
|
||||
});
|
||||
}
|
||||
|
||||
fn send_mcp_prompt(
|
||||
&self,
|
||||
message_id: UserMessageId,
|
||||
session_id: agent_client_protocol::SessionId,
|
||||
prompt_name: String,
|
||||
server_id: ContextServerId,
|
||||
arguments: HashMap<String, String>,
|
||||
original_content: Vec<acp::ContentBlock>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let server_store = self.context_server_registry.read(cx).server_store().clone();
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let prompt =
|
||||
crate::get_prompt(&server_store, &server_id, &prompt_name, arguments, cx).await?;
|
||||
|
||||
let (acp_thread, thread) = this.update(cx, |this, _cx| {
|
||||
let session = this
|
||||
.sessions
|
||||
.get(&session_id)
|
||||
.context("Failed to get session")?;
|
||||
anyhow::Ok((session.acp_thread.clone(), session.thread.clone()))
|
||||
})??;
|
||||
|
||||
let mut last_is_user = true;
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_acp_user_block(
|
||||
message_id,
|
||||
original_content.into_iter().skip(1),
|
||||
path_style,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
for message in prompt.messages {
|
||||
let context_server::types::PromptMessage { role, content } = message;
|
||||
let block = mcp_message_content_to_acp_content_block(content);
|
||||
|
||||
match role {
|
||||
context_server::types::Role::User => {
|
||||
let id = acp_thread::UserMessageId::new();
|
||||
|
||||
acp_thread.update(cx, |acp_thread, cx| {
|
||||
acp_thread.push_user_content_block_with_indent(
|
||||
Some(id.clone()),
|
||||
block.clone(),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_acp_user_block(id, [block], path_style, cx);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
}
|
||||
context_server::types::Role::Assistant => {
|
||||
acp_thread.update(cx, |acp_thread, cx| {
|
||||
acp_thread.push_assistant_content_block_with_indent(
|
||||
block.clone(),
|
||||
false,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_acp_agent_block(block, cx);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
}
|
||||
}
|
||||
|
||||
last_is_user = role == context_server::types::Role::User;
|
||||
}
|
||||
|
||||
let response_stream = thread.update(cx, |thread, cx| {
|
||||
if last_is_user {
|
||||
thread.send_existing(cx)
|
||||
} else {
|
||||
// Resume if MCP prompt did not end with a user message
|
||||
thread.resume(cx)
|
||||
}
|
||||
})??;
|
||||
|
||||
cx.update(|cx| {
|
||||
NativeAgentConnection::handle_thread_events(response_stream, acp_thread, cx)
|
||||
})?
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper struct that implements the AgentConnection trait
|
||||
@@ -1041,39 +843,6 @@ impl NativeAgentConnection {
|
||||
}
|
||||
}
|
||||
|
||||
struct Command<'a> {
|
||||
prompt_name: &'a str,
|
||||
arg_value: &'a str,
|
||||
explicit_server_id: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> Command<'a> {
|
||||
fn parse(prompt: &'a [acp::ContentBlock]) -> Option<Self> {
|
||||
let acp::ContentBlock::Text(text_content) = prompt.first()? else {
|
||||
return None;
|
||||
};
|
||||
let text = text_content.text.trim();
|
||||
let command = text.strip_prefix('/')?;
|
||||
let (command, arg_value) = command
|
||||
.split_once(char::is_whitespace)
|
||||
.unwrap_or((command, ""));
|
||||
|
||||
if let Some((server_id, prompt_name)) = command.split_once('.') {
|
||||
Some(Self {
|
||||
prompt_name,
|
||||
arg_value,
|
||||
explicit_server_id: Some(server_id),
|
||||
})
|
||||
} else {
|
||||
Some(Self {
|
||||
prompt_name: command,
|
||||
arg_value,
|
||||
explicit_server_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NativeAgentModelSelector {
|
||||
session_id: acp::SessionId,
|
||||
connection: NativeAgentConnection,
|
||||
@@ -1164,10 +933,6 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
|
||||
fn should_render_footer(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_favorites(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
@@ -1243,47 +1008,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
let session_id = params.session_id.clone();
|
||||
log::info!("Received prompt request for session: {}", session_id);
|
||||
log::debug!("Prompt blocks count: {}", params.prompt.len());
|
||||
|
||||
if let Some(parsed_command) = Command::parse(¶ms.prompt) {
|
||||
let registry = self.0.read(cx).context_server_registry.read(cx);
|
||||
|
||||
let explicit_server_id = parsed_command
|
||||
.explicit_server_id
|
||||
.map(|server_id| ContextServerId(server_id.into()));
|
||||
|
||||
if let Some(prompt) =
|
||||
registry.find_prompt(explicit_server_id.as_ref(), parsed_command.prompt_name)
|
||||
{
|
||||
let arguments = if !parsed_command.arg_value.is_empty()
|
||||
&& let Some(arg_name) = prompt
|
||||
.prompt
|
||||
.arguments
|
||||
.as_ref()
|
||||
.and_then(|args| args.first())
|
||||
.map(|arg| arg.name.clone())
|
||||
{
|
||||
HashMap::from_iter([(arg_name, parsed_command.arg_value.to_string())])
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let prompt_name = prompt.prompt.name.clone();
|
||||
let server_id = prompt.server_id.clone();
|
||||
|
||||
return self.0.update(cx, |agent, cx| {
|
||||
agent.send_mcp_prompt(
|
||||
id,
|
||||
session_id.clone(),
|
||||
prompt_name,
|
||||
server_id,
|
||||
arguments,
|
||||
params.prompt,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
let path_style = self.0.read(cx).project.read(cx).path_style(cx);
|
||||
|
||||
self.run_turn(session_id, cx, move |thread, cx| {
|
||||
@@ -1880,35 +1604,3 @@ mod internal_tests {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_message_content_to_acp_content_block(
|
||||
content: context_server::types::MessageContent,
|
||||
) -> acp::ContentBlock {
|
||||
match content {
|
||||
context_server::types::MessageContent::Text {
|
||||
text,
|
||||
annotations: _,
|
||||
} => text.into(),
|
||||
context_server::types::MessageContent::Image {
|
||||
data,
|
||||
mime_type,
|
||||
annotations: _,
|
||||
} => acp::ContentBlock::Image(acp::ImageContent::new(data, mime_type)),
|
||||
context_server::types::MessageContent::Audio {
|
||||
data,
|
||||
mime_type,
|
||||
annotations: _,
|
||||
} => acp::ContentBlock::Audio(acp::AudioContent::new(data, mime_type)),
|
||||
context_server::types::MessageContent::Resource {
|
||||
resource,
|
||||
annotations: _,
|
||||
} => {
|
||||
let mut link =
|
||||
acp::ResourceLink::new(resource.uri.to_string(), resource.uri.to_string());
|
||||
if let Some(mime_type) = resource.mime_type {
|
||||
link = link.mime_type(mime_type);
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,10 +216,14 @@ impl HistoryStore {
|
||||
}
|
||||
|
||||
pub fn reload(&self, cx: &mut Context<Self>) {
|
||||
let database_connection = ThreadsDatabase::connect(cx);
|
||||
let database_future = ThreadsDatabase::connect(cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let database = database_connection.await;
|
||||
let threads = database.map_err(|err| anyhow!(err))?.list_threads().await?;
|
||||
let threads = database_future
|
||||
.await
|
||||
.map_err(|err| anyhow!(err))?
|
||||
.list_threads()
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES {
|
||||
for thread in threads
|
||||
@@ -340,8 +344,7 @@ impl HistoryStore {
|
||||
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
|
||||
cx.background_spawn(async move {
|
||||
if cfg!(any(feature = "test-support", test)) {
|
||||
log::warn!("history store does not persist in tests");
|
||||
return Ok(VecDeque::new());
|
||||
anyhow::bail!("history store does not persist in tests");
|
||||
}
|
||||
let json = KEY_VALUE_STORE
|
||||
.read_kvp(RECENTLY_OPENED_THREADS_KEY)?
|
||||
|
||||
@@ -2809,181 +2809,3 @@ fn setup_context_server(
|
||||
cx.run_until_parked();
|
||||
mcp_tool_calls_rx
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tokens_before_message(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
// First message
|
||||
let message_1_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_1_id.clone(), ["First message"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Before any response, tokens_before_message should return None for first message
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message should have no tokens before it"
|
||||
);
|
||||
});
|
||||
|
||||
// Complete first message with usage
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 1");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 100,
|
||||
output_tokens: 50,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// First message still has no tokens before it
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message should still have no tokens before it after response"
|
||||
);
|
||||
});
|
||||
|
||||
// Second message
|
||||
let message_2_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_2_id.clone(), ["Second message"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Second message should have first message's input tokens before it
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_2_id),
|
||||
Some(100),
|
||||
"Second message should have 100 tokens before it (from first request)"
|
||||
);
|
||||
});
|
||||
|
||||
// Complete second message
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 2");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 250, // Total for this request (includes previous context)
|
||||
output_tokens: 75,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Third message
|
||||
let message_3_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_3_id.clone(), ["Third message"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Third message should have second message's input tokens (250) before it
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_3_id),
|
||||
Some(250),
|
||||
"Third message should have 250 tokens before it (from second request)"
|
||||
);
|
||||
// Second message should still have 100
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_2_id),
|
||||
Some(100),
|
||||
"Second message should still have 100 tokens before it"
|
||||
);
|
||||
// First message still has none
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message should still have no tokens before it"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tokens_before_message_after_truncate(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
// Set up three messages with responses
|
||||
let message_1_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_1_id.clone(), ["Message 1"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 1");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 100,
|
||||
output_tokens: 50,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
let message_2_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_2_id.clone(), ["Message 2"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 2");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 250,
|
||||
output_tokens: 75,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify initial state
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(thread.tokens_before_message(&message_2_id), Some(100));
|
||||
});
|
||||
|
||||
// Truncate at message 2 (removes message 2 and everything after)
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.truncate(message_2_id.clone(), cx))
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// After truncation, message_2_id no longer exists, so lookup should return None
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_2_id),
|
||||
None,
|
||||
"After truncation, message 2 no longer exists"
|
||||
);
|
||||
// Message 1 still exists but has no tokens before it
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message still has no tokens before it"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,13 +108,7 @@ impl Message {
|
||||
|
||||
pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
|
||||
match self {
|
||||
Message::User(message) => {
|
||||
if message.content.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![message.to_request()]
|
||||
}
|
||||
}
|
||||
Message::User(message) => vec![message.to_request()],
|
||||
Message::Agent(message) => message.to_request(),
|
||||
Message::Resume => vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
@@ -1095,28 +1089,6 @@ impl Thread {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the total input token count as of the message before the given message.
|
||||
///
|
||||
/// Returns `None` if:
|
||||
/// - `target_id` is the first message (no previous message)
|
||||
/// - The previous message hasn't received a response yet (no usage data)
|
||||
/// - `target_id` is not found in the messages
|
||||
pub fn tokens_before_message(&self, target_id: &UserMessageId) -> Option<u64> {
|
||||
let mut previous_user_message_id: Option<&UserMessageId> = None;
|
||||
|
||||
for message in &self.messages {
|
||||
if let Message::User(user_msg) = message {
|
||||
if &user_msg.id == target_id {
|
||||
let prev_id = previous_user_message_id?;
|
||||
let usage = self.request_token_usage.get(prev_id)?;
|
||||
return Some(usage.input_tokens);
|
||||
}
|
||||
previous_user_message_id = Some(&user_msg.id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Look up the active profile and resolve its preferred model if one is configured.
|
||||
fn resolve_profile_model(
|
||||
profile_id: &AgentProfileId,
|
||||
@@ -1169,6 +1141,11 @@ impl Thread {
|
||||
where
|
||||
T: Into<UserMessageContent>,
|
||||
{
|
||||
let model = self.model().context("No language model configured")?;
|
||||
|
||||
log::info!("Thread::send called with model: {}", model.name().0);
|
||||
self.advance_prompt_id();
|
||||
|
||||
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
log::debug!("Thread::send content: {:?}", content);
|
||||
|
||||
@@ -1176,59 +1153,10 @@ impl Thread {
|
||||
.push(Message::User(UserMessage { id, content }));
|
||||
cx.notify();
|
||||
|
||||
self.send_existing(cx)
|
||||
}
|
||||
|
||||
pub fn send_existing(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
|
||||
let model = self.model().context("No language model configured")?;
|
||||
|
||||
log::info!("Thread::send called with model: {}", model.name().0);
|
||||
self.advance_prompt_id();
|
||||
|
||||
log::debug!("Total messages in thread: {}", self.messages.len());
|
||||
self.run_turn(cx)
|
||||
}
|
||||
|
||||
pub fn push_acp_user_block(
|
||||
&mut self,
|
||||
id: UserMessageId,
|
||||
blocks: impl IntoIterator<Item = acp::ContentBlock>,
|
||||
path_style: PathStyle,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let content = blocks
|
||||
.into_iter()
|
||||
.map(|block| UserMessageContent::from_content_block(block, path_style))
|
||||
.collect::<Vec<_>>();
|
||||
self.messages
|
||||
.push(Message::User(UserMessage { id, content }));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn push_acp_agent_block(&mut self, block: acp::ContentBlock, cx: &mut Context<Self>) {
|
||||
let text = match block {
|
||||
acp::ContentBlock::Text(text_content) => text_content.text,
|
||||
acp::ContentBlock::Image(_) => "[image]".to_string(),
|
||||
acp::ContentBlock::Audio(_) => "[audio]".to_string(),
|
||||
acp::ContentBlock::ResourceLink(resource_link) => resource_link.uri,
|
||||
acp::ContentBlock::Resource(resource) => match resource.resource {
|
||||
acp::EmbeddedResourceResource::TextResourceContents(resource) => resource.uri,
|
||||
acp::EmbeddedResourceResource::BlobResourceContents(resource) => resource.uri,
|
||||
_ => "[resource]".to_string(),
|
||||
},
|
||||
_ => "[unknown]".to_string(),
|
||||
};
|
||||
|
||||
self.messages.push(Message::Agent(AgentMessage {
|
||||
content: vec![AgentMessageContent::Text(text)],
|
||||
..Default::default()
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(feature = "eval")]
|
||||
pub fn proceed(
|
||||
&mut self,
|
||||
@@ -1725,10 +1653,6 @@ impl Thread {
|
||||
self.pending_summary_generation.is_some()
|
||||
}
|
||||
|
||||
pub fn is_generating_title(&self) -> bool {
|
||||
self.pending_title_generation.is_some()
|
||||
}
|
||||
|
||||
pub fn summary(&mut self, cx: &mut Context<Self>) -> Shared<Task<Option<SharedString>>> {
|
||||
if let Some(summary) = self.summary.as_ref() {
|
||||
return Task::ready(Some(summary.clone())).shared();
|
||||
@@ -1796,7 +1720,7 @@ impl Thread {
|
||||
task
|
||||
}
|
||||
|
||||
pub fn generate_title(&mut self, cx: &mut Context<Self>) {
|
||||
fn generate_title(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(model) = self.summarization_model.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -2,24 +2,12 @@ use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream};
|
||||
use agent_client_protocol::ToolKind;
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use context_server::{ContextServerId, client::NotificationSubscription};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
|
||||
use context_server::ContextServerId;
|
||||
use gpui::{App, Context, Entity, SharedString, Task};
|
||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct ContextServerPrompt {
|
||||
pub server_id: ContextServerId,
|
||||
pub prompt: context_server::types::Prompt,
|
||||
}
|
||||
|
||||
pub enum ContextServerRegistryEvent {
|
||||
ToolsChanged,
|
||||
PromptsChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<ContextServerRegistryEvent> for ContextServerRegistry {}
|
||||
|
||||
pub struct ContextServerRegistry {
|
||||
server_store: Entity<ContextServerStore>,
|
||||
registered_servers: HashMap<ContextServerId, RegisteredContextServer>,
|
||||
@@ -28,10 +16,7 @@ pub struct ContextServerRegistry {
|
||||
|
||||
struct RegisteredContextServer {
|
||||
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||
prompts: BTreeMap<SharedString, ContextServerPrompt>,
|
||||
load_tools: Task<Result<()>>,
|
||||
load_prompts: Task<Result<()>>,
|
||||
_tools_updated_subscription: Option<NotificationSubscription>,
|
||||
}
|
||||
|
||||
impl ContextServerRegistry {
|
||||
@@ -43,7 +28,6 @@ impl ContextServerRegistry {
|
||||
};
|
||||
for server in server_store.read(cx).running_servers() {
|
||||
this.reload_tools_for_server(server.id(), cx);
|
||||
this.reload_prompts_for_server(server.id(), cx);
|
||||
}
|
||||
this
|
||||
}
|
||||
@@ -72,88 +56,6 @@ impl ContextServerRegistry {
|
||||
.map(|(id, server)| (id, &server.tools))
|
||||
}
|
||||
|
||||
pub fn prompts(&self) -> impl Iterator<Item = &ContextServerPrompt> {
|
||||
self.registered_servers
|
||||
.values()
|
||||
.flat_map(|server| server.prompts.values())
|
||||
}
|
||||
|
||||
pub fn find_prompt(
|
||||
&self,
|
||||
server_id: Option<&ContextServerId>,
|
||||
name: &str,
|
||||
) -> Option<&ContextServerPrompt> {
|
||||
if let Some(server_id) = server_id {
|
||||
self.registered_servers
|
||||
.get(server_id)
|
||||
.and_then(|server| server.prompts.get(name))
|
||||
} else {
|
||||
self.registered_servers
|
||||
.values()
|
||||
.find_map(|server| server.prompts.get(name))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_store(&self) -> &Entity<ContextServerStore> {
|
||||
&self.server_store
|
||||
}
|
||||
|
||||
fn get_or_register_server(
|
||||
&mut self,
|
||||
server_id: &ContextServerId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> &mut RegisteredContextServer {
|
||||
self.registered_servers
|
||||
.entry(server_id.clone())
|
||||
.or_insert_with(|| Self::init_registered_server(server_id, &self.server_store, cx))
|
||||
}
|
||||
|
||||
fn init_registered_server(
|
||||
server_id: &ContextServerId,
|
||||
server_store: &Entity<ContextServerStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> RegisteredContextServer {
|
||||
let tools_updated_subscription = server_store
|
||||
.read(cx)
|
||||
.get_running_server(server_id)
|
||||
.and_then(|server| {
|
||||
let client = server.client()?;
|
||||
|
||||
if !client.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let server_id = server.id();
|
||||
let this = cx.entity().downgrade();
|
||||
|
||||
Some(client.on_notification(
|
||||
"notifications/tools/list_changed",
|
||||
Box::new(move |_params, cx: AsyncApp| {
|
||||
let server_id = server_id.clone();
|
||||
let this = this.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
log::info!(
|
||||
"Received tools/list_changed notification for server {}",
|
||||
server_id
|
||||
);
|
||||
this.reload_tools_for_server(server_id, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}),
|
||||
))
|
||||
});
|
||||
|
||||
RegisteredContextServer {
|
||||
tools: BTreeMap::default(),
|
||||
prompts: BTreeMap::default(),
|
||||
load_tools: Task::ready(Ok(())),
|
||||
load_prompts: Task::ready(Ok(())),
|
||||
_tools_updated_subscription: tools_updated_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
|
||||
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
|
||||
return;
|
||||
@@ -161,12 +63,17 @@ impl ContextServerRegistry {
|
||||
let Some(client) = server.client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !client.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
return;
|
||||
}
|
||||
|
||||
let registered_server = self.get_or_register_server(&server_id, cx);
|
||||
let registered_server =
|
||||
self.registered_servers
|
||||
.entry(server_id.clone())
|
||||
.or_insert(RegisteredContextServer {
|
||||
tools: BTreeMap::default(),
|
||||
load_tools: Task::ready(Ok(())),
|
||||
});
|
||||
registered_server.load_tools = cx.spawn(async move |this, cx| {
|
||||
let response = client
|
||||
.request::<context_server::types::requests::ListTools>(())
|
||||
@@ -187,49 +94,6 @@ impl ContextServerRegistry {
|
||||
));
|
||||
registered_server.tools.insert(tool.name(), tool);
|
||||
}
|
||||
cx.emit(ContextServerRegistryEvent::ToolsChanged);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn reload_prompts_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
|
||||
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = server.client() else {
|
||||
return;
|
||||
};
|
||||
if !client.capable(context_server::protocol::ServerCapability::Prompts) {
|
||||
return;
|
||||
}
|
||||
|
||||
let registered_server = self.get_or_register_server(&server_id, cx);
|
||||
|
||||
registered_server.load_prompts = cx.spawn(async move |this, cx| {
|
||||
let response = client
|
||||
.request::<context_server::types::requests::PromptsList>(())
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let Some(registered_server) = this.registered_servers.get_mut(&server_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
registered_server.prompts.clear();
|
||||
if let Some(response) = response.log_err() {
|
||||
for prompt in response.prompts {
|
||||
let name: SharedString = prompt.name.clone().into();
|
||||
registered_server.prompts.insert(
|
||||
name,
|
||||
ContextServerPrompt {
|
||||
server_id: server_id.clone(),
|
||||
prompt,
|
||||
},
|
||||
);
|
||||
}
|
||||
cx.emit(ContextServerRegistryEvent::PromptsChanged);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
@@ -248,17 +112,9 @@ impl ContextServerRegistry {
|
||||
ContextServerStatus::Starting => {}
|
||||
ContextServerStatus::Running => {
|
||||
self.reload_tools_for_server(server_id.clone(), cx);
|
||||
self.reload_prompts_for_server(server_id.clone(), cx);
|
||||
}
|
||||
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
||||
if let Some(registered_server) = self.registered_servers.remove(server_id) {
|
||||
if !registered_server.tools.is_empty() {
|
||||
cx.emit(ContextServerRegistryEvent::ToolsChanged);
|
||||
}
|
||||
if !registered_server.prompts.is_empty() {
|
||||
cx.emit(ContextServerRegistryEvent::PromptsChanged);
|
||||
}
|
||||
}
|
||||
self.registered_servers.remove(server_id);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -395,39 +251,3 @@ impl AnyAgentTool for ContextServerTool {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_prompt(
|
||||
server_store: &Entity<ContextServerStore>,
|
||||
server_id: &ContextServerId,
|
||||
prompt_name: &str,
|
||||
arguments: HashMap<String, String>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<context_server::types::PromptsGetResponse>> {
|
||||
let server = match cx.update(|cx| server_store.read(cx).get_running_server(server_id)) {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let Some(server) = server else {
|
||||
return Task::ready(Err(anyhow::anyhow!("Context server not found")));
|
||||
};
|
||||
|
||||
let Some(protocol) = server.client() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("Context server not initialized")));
|
||||
};
|
||||
|
||||
let prompt_name = prompt_name.to_string();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let response = protocol
|
||||
.request::<context_server::types::requests::PromptsGet>(
|
||||
context_server::types::PromptsGetParams {
|
||||
name: prompt_name,
|
||||
arguments: (!arguments.is_empty()).then(|| arguments),
|
||||
meta: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(response)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ workspace = true
|
||||
path = "src/agent_settings.rs"
|
||||
|
||||
[dependencies]
|
||||
agent-client-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
|
||||
@@ -2,8 +2,7 @@ mod agent_profile;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::ModelId;
|
||||
use collections::{HashSet, IndexMap};
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, Pixels, px};
|
||||
use language_model::LanguageModel;
|
||||
use project::DisableAiSettings;
|
||||
@@ -34,7 +33,6 @@ pub struct AgentSettings {
|
||||
pub commit_message_model: Option<LanguageModelSelection>,
|
||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub favorite_models: Vec<LanguageModelSelection>,
|
||||
pub default_profile: AgentProfileId,
|
||||
pub default_view: DefaultAgentView,
|
||||
pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
|
||||
@@ -98,13 +96,6 @@ impl AgentSettings {
|
||||
pub fn set_message_editor_max_lines(&self) -> usize {
|
||||
self.message_editor_min_lines * 2
|
||||
}
|
||||
|
||||
pub fn favorite_model_ids(&self) -> HashSet<ModelId> {
|
||||
self.favorite_models
|
||||
.iter()
|
||||
.map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
@@ -173,7 +164,6 @@ impl Settings for AgentSettings {
|
||||
commit_message_model: agent.commit_message_model,
|
||||
thread_summary_model: agent.thread_summary_model,
|
||||
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
|
||||
favorite_models: agent.favorite_models,
|
||||
default_profile: AgentProfileId(agent.default_profile.unwrap()),
|
||||
default_view: agent.default_view.unwrap(),
|
||||
profiles: agent
|
||||
|
||||
@@ -13,7 +13,7 @@ path = "src/agent_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support", "agent/test-support"]
|
||||
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support"]
|
||||
unit-eval = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -34,7 +34,7 @@ use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::{Chat, PasteRaw};
|
||||
use zed_actions::agent::Chat;
|
||||
|
||||
pub struct MessageEditor {
|
||||
mention_set: Entity<MentionSet>,
|
||||
@@ -543,9 +543,6 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
@@ -556,127 +553,133 @@ impl MessageEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
// 2. Have an associated file path
|
||||
// 3. Span multiple lines (not single-line selections)
|
||||
// 4. Belong to a file that exists in the current project
|
||||
let should_insert_creases = util::maybe!({
|
||||
let selections = editor_clipboard_selections.as_ref()?;
|
||||
if selections.len() > 1 {
|
||||
return Some(false);
|
||||
}
|
||||
let selection = selections.first()?;
|
||||
let file_path = selection.file_path.as_ref()?;
|
||||
let line_range = selection.line_range.as_ref()?;
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
|
||||
if line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_insert_creases && let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
let insertion_target = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let crease_text =
|
||||
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||
|
||||
let mention_uri = MentionUri::Selection {
|
||||
abs_path: Some(file_path.clone()),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
let (excerpt_id, text_anchor, content_len) =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
|
||||
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(*excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
crease_text.into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = cx
|
||||
.spawn({
|
||||
let project = project.clone();
|
||||
async move |_, cx| {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&file_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "project path not found".to_string())?;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let start = Point::new(*line_range.start(), 0)
|
||||
.min(buffer.max_point());
|
||||
let end = Point::new(*line_range.end() + 1, 0)
|
||||
.min(buffer.max_point());
|
||||
let content = buffer.text_for_range(start..end).collect();
|
||||
Mention::Text {
|
||||
content,
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _cx| {
|
||||
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||
});
|
||||
if has_file_context {
|
||||
if let Some((workspace, selections)) =
|
||||
self.workspace.upgrade().zip(editor_clipboard_selections)
|
||||
{
|
||||
let Some(first_selection) = selections.first() else {
|
||||
return;
|
||||
};
|
||||
if let Some(file_path) = &first_selection.file_path {
|
||||
// In case someone pastes selections from another window
|
||||
// with a different project, we don't want to insert the
|
||||
// crease (containing the absolute path) since the agent
|
||||
// cannot access files outside the project.
|
||||
let is_in_project = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some();
|
||||
if !is_in_project {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cx.stop_propagation();
|
||||
let insertion_target = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let crease_text =
|
||||
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||
|
||||
let mention_uri = MentionUri::Selection {
|
||||
abs_path: Some(file_path.clone()),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
let (excerpt_id, text_anchor, content_len) =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.as_singleton().unwrap();
|
||||
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(*excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
crease_text.into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = cx
|
||||
.spawn({
|
||||
let project = project.clone();
|
||||
async move |_, cx| {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&file_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "project path not found".to_string())?;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let start = Point::new(*line_range.start(), 0)
|
||||
.min(buffer.max_point());
|
||||
let end = Point::new(*line_range.end() + 1, 0)
|
||||
.min(buffer.max_point());
|
||||
let content =
|
||||
buffer.text_for_range(start..end).collect();
|
||||
Mention::Text {
|
||||
content,
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _cx| {
|
||||
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if self.prompt_capabilities.borrow().image
|
||||
@@ -687,13 +690,6 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let editor = self.editor.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_dragged_files(
|
||||
&mut self,
|
||||
paths: Vec<project::ProjectPath>,
|
||||
@@ -971,7 +967,6 @@ impl Render for MessageEditor {
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::chat_with_follow))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::paste_raw))
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.flex_1()
|
||||
.child({
|
||||
@@ -1370,7 +1365,7 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
message_editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
message_editor.read(cx).focus_handle(cx).focus(window);
|
||||
message_editor.read(cx).editor().clone()
|
||||
});
|
||||
|
||||
@@ -1592,7 +1587,7 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
message_editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
message_editor.read(cx).focus_handle(cx).focus(window);
|
||||
let editor = message_editor.read(cx).editor().clone();
|
||||
(message_editor, editor)
|
||||
});
|
||||
@@ -2320,7 +2315,7 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
message_editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
message_editor.read(cx).focus_handle(cx).focus(window);
|
||||
let editor = message_editor.read(cx).editor().clone();
|
||||
(message_editor, editor)
|
||||
});
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use agent_client_protocol::ModelId;
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use collections::{HashSet, IndexMap};
|
||||
use collections::IndexMap;
|
||||
use fs::Fs;
|
||||
use futures::FutureExt;
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, prelude::*};
|
||||
use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, prelude::*};
|
||||
use util::ResultExt;
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
@@ -42,7 +38,7 @@ pub fn acp_model_selector(
|
||||
|
||||
enum AcpModelPickerEntry {
|
||||
Separator(SharedString),
|
||||
Model(AgentModelInfo, bool),
|
||||
Model(AgentModelInfo),
|
||||
}
|
||||
|
||||
pub struct AcpModelPickerDelegate {
|
||||
@@ -119,67 +115,6 @@ impl AcpModelPickerDelegate {
|
||||
pub fn active_model(&self) -> Option<&AgentModelInfo> {
|
||||
self.selected_model.as_ref()
|
||||
}
|
||||
|
||||
pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if !self.selector.supports_favorites() {
|
||||
return;
|
||||
}
|
||||
|
||||
let favorites = AgentSettings::get_global(cx).favorite_model_ids();
|
||||
|
||||
if favorites.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(models) = self.models.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let all_models: Vec<AgentModelInfo> = match models {
|
||||
AgentModelList::Flat(list) => list,
|
||||
AgentModelList::Grouped(index_map) => index_map
|
||||
.into_values()
|
||||
.flatten()
|
||||
.collect::<Vec<AgentModelInfo>>(),
|
||||
};
|
||||
|
||||
let favorite_models = all_models
|
||||
.iter()
|
||||
.filter(|model| favorites.contains(&model.id))
|
||||
.unique_by(|model| &model.id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let current_id = self.selected_model.as_ref().map(|m| m.id.clone());
|
||||
|
||||
let current_index_in_favorites = current_id
|
||||
.as_ref()
|
||||
.and_then(|id| favorite_models.iter().position(|m| &m.id == id))
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
let next_index = if current_index_in_favorites == usize::MAX {
|
||||
0
|
||||
} else {
|
||||
(current_index_in_favorites + 1) % favorite_models.len()
|
||||
};
|
||||
|
||||
let next_model = favorite_models[next_index].clone();
|
||||
|
||||
self.selector
|
||||
.select_model(next_model.id.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
self.selected_model = Some(next_model);
|
||||
|
||||
// Keep the picker selection aligned with the newly-selected model
|
||||
if let Some(new_index) = self.filtered_entries.iter().position(|entry| {
|
||||
matches!(entry, AcpModelPickerEntry::Model(model_info, _) if self.selected_model.as_ref().is_some_and(|selected| model_info.id == selected.id))
|
||||
}) {
|
||||
self.set_selected_index(new_index, window, cx);
|
||||
} else {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for AcpModelPickerDelegate {
|
||||
@@ -205,7 +140,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> bool {
|
||||
match self.filtered_entries.get(ix) {
|
||||
Some(AcpModelPickerEntry::Model(_, _)) => true,
|
||||
Some(AcpModelPickerEntry::Model(_)) => true,
|
||||
Some(AcpModelPickerEntry::Separator(_)) | None => false,
|
||||
}
|
||||
}
|
||||
@@ -220,12 +155,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let favorites = if self.selector.supports_favorites() {
|
||||
AgentSettings::get_global(cx).favorite_model_ids()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_models = match this
|
||||
.read_with(cx, |this, cx| {
|
||||
@@ -242,7 +171,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries =
|
||||
info_list_to_picker_entries(filtered_models, &favorites);
|
||||
info_list_to_picker_entries(filtered_models).collect();
|
||||
// Finds the currently selected model in the list
|
||||
let new_index = this
|
||||
.delegate
|
||||
@@ -250,7 +179,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
.as_ref()
|
||||
.and_then(|selected| {
|
||||
this.delegate.filtered_entries.iter().position(|entry| {
|
||||
if let AcpModelPickerEntry::Model(model_info, _) = entry {
|
||||
if let AcpModelPickerEntry::Model(model_info) = entry {
|
||||
model_info.id == selected.id
|
||||
} else {
|
||||
false
|
||||
@@ -266,7 +195,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if let Some(AcpModelPickerEntry::Model(model_info, _)) =
|
||||
if let Some(AcpModelPickerEntry::Model(model_info)) =
|
||||
self.filtered_entries.get(self.selected_index)
|
||||
{
|
||||
if window.modifiers().secondary() {
|
||||
@@ -304,7 +233,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
is_focused: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
@@ -312,53 +241,32 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
AcpModelPickerEntry::Separator(title) => {
|
||||
Some(ModelSelectorHeader::new(title, ix > 1).into_any_element())
|
||||
}
|
||||
AcpModelPickerEntry::Model(model_info, is_favorite) => {
|
||||
AcpModelPickerEntry::Model(model_info) => {
|
||||
let is_selected = Some(model_info) == self.selected_model.as_ref();
|
||||
let default_model = self.agent_server.default_model(cx);
|
||||
let is_default = default_model.as_ref() == Some(&model_info.id);
|
||||
|
||||
let supports_favorites = self.selector.supports_favorites();
|
||||
|
||||
let is_favorite = *is_favorite;
|
||||
let handle_action_click = {
|
||||
let model_id = model_info.id.clone();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
move |cx: &App| {
|
||||
crate::favorite_models::toggle_model_id_in_settings(
|
||||
model_id.clone(),
|
||||
!is_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Some(
|
||||
div()
|
||||
.id(("model-picker-menu-child", ix))
|
||||
.when_some(model_info.description.clone(), |this, description| {
|
||||
this.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||
if *hovered {
|
||||
menu.delegate.selected_description =
|
||||
Some((ix, description.clone(), is_default));
|
||||
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) {
|
||||
menu.delegate.selected_description = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
this
|
||||
.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||
if *hovered {
|
||||
menu.delegate.selected_description = Some((ix, description.clone(), is_default));
|
||||
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) {
|
||||
menu.delegate.selected_description = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
ModelSelectorListItem::new(ix, model_info.name.clone())
|
||||
.when_some(model_info.icon, |this, icon| this.icon(icon))
|
||||
.is_focused(is_focused)
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.when(supports_favorites, |this| {
|
||||
this.is_favorite(is_favorite)
|
||||
.on_toggle_favorite(handle_action_click)
|
||||
}),
|
||||
.when_some(model_info.icon, |this, icon| this.icon(icon)),
|
||||
)
|
||||
.into_any_element(),
|
||||
.into_any_element()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -406,51 +314,18 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
fn info_list_to_picker_entries(
|
||||
model_list: AgentModelList,
|
||||
favorites: &HashSet<ModelId>,
|
||||
) -> Vec<AcpModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let all_models: Vec<_> = match &model_list {
|
||||
AgentModelList::Flat(list) => list.iter().collect(),
|
||||
AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
|
||||
};
|
||||
|
||||
let favorite_models: Vec<_> = all_models
|
||||
.iter()
|
||||
.filter(|m| favorites.contains(&m.id))
|
||||
.unique_by(|m| &m.id)
|
||||
.collect();
|
||||
|
||||
let has_favorites = !favorite_models.is_empty();
|
||||
if has_favorites {
|
||||
entries.push(AcpModelPickerEntry::Separator("Favorite".into()));
|
||||
for model in favorite_models {
|
||||
entries.push(AcpModelPickerEntry::Model((*model).clone(), true));
|
||||
}
|
||||
}
|
||||
|
||||
) -> impl Iterator<Item = AcpModelPickerEntry> {
|
||||
match model_list {
|
||||
AgentModelList::Flat(list) => {
|
||||
if has_favorites {
|
||||
entries.push(AcpModelPickerEntry::Separator("All".into()));
|
||||
}
|
||||
for model in list {
|
||||
let is_favorite = favorites.contains(&model.id);
|
||||
entries.push(AcpModelPickerEntry::Model(model, is_favorite));
|
||||
}
|
||||
itertools::Either::Left(list.into_iter().map(AcpModelPickerEntry::Model))
|
||||
}
|
||||
AgentModelList::Grouped(index_map) => {
|
||||
for (group_name, models) in index_map {
|
||||
entries.push(AcpModelPickerEntry::Separator(group_name.0));
|
||||
for model in models {
|
||||
let is_favorite = favorites.contains(&model.id);
|
||||
entries.push(AcpModelPickerEntry::Model(model, is_favorite));
|
||||
}
|
||||
}
|
||||
itertools::Either::Right(index_map.into_iter().flat_map(|(group_name, models)| {
|
||||
std::iter::once(AcpModelPickerEntry::Separator(group_name.0))
|
||||
.chain(models.into_iter().map(AcpModelPickerEntry::Model))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
async fn fuzzy_search(
|
||||
@@ -572,168 +447,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
|
||||
models
|
||||
.into_iter()
|
||||
.map(|m| ModelId::new(m.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
AcpModelPickerEntry::Model(info, _) => Some(info.id.0.as_ref()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_entry_labels(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
|
||||
entries
|
||||
.iter()
|
||||
.map(|entry| match entry {
|
||||
AcpModelPickerEntry::Model(info, _) => info.id.0.as_ref(),
|
||||
AcpModelPickerEntry::Separator(s) => &s,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
("zed", vec!["zed/claude", "zed/gemini"]),
|
||||
("openai", vec!["openai/gpt-5"]),
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
|
||||
));
|
||||
|
||||
let model_ids = get_entry_model_ids(&entries);
|
||||
assert_eq!(model_ids[0], "zed/gemini");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
|
||||
let favorites = create_favorites(vec![]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(AcpModelPickerEntry::Separator(s)) if s == "zed"
|
||||
));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
("zed", vec!["zed/claude", "zed/gemini"]),
|
||||
("openai", vec!["openai/gpt-5"]),
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/claude"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
for entry in &entries {
|
||||
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
|
||||
if info.id.0.as_ref() == "zed/claude" {
|
||||
assert!(is_favorite, "zed/claude should be a favorite");
|
||||
} else {
|
||||
assert!(!is_favorite, "{} should not be a favorite", info.id.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_appear_in_both_sections(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
("zed", vec!["zed/claude", "zed/gemini"]),
|
||||
("openai", vec!["openai/gpt-5", "openai/gpt-4"]),
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let model_ids = get_entry_model_ids(&entries);
|
||||
|
||||
assert_eq!(model_ids[0], "zed/gemini");
|
||||
assert_eq!(model_ids[1], "openai/gpt-5");
|
||||
|
||||
assert!(model_ids[2..].contains(&"zed/gemini"));
|
||||
assert!(model_ids[2..].contains(&"openai/gpt-5"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_are_not_duplicated_when_repeated_in_other_sections(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
("Recommended", vec!["zed/claude", "anthropic/claude"]),
|
||||
("Zed", vec!["zed/claude", "zed/gpt-5"]),
|
||||
("Antropic", vec!["anthropic/claude"]),
|
||||
("OpenAI", vec!["openai/gpt-5"]),
|
||||
]);
|
||||
|
||||
let favorites = create_favorites(vec!["zed/claude"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let labels = get_entry_labels(&entries);
|
||||
|
||||
assert_eq!(
|
||||
labels,
|
||||
vec![
|
||||
"Favorite",
|
||||
"zed/claude",
|
||||
"Recommended",
|
||||
"zed/claude",
|
||||
"anthropic/claude",
|
||||
"Zed",
|
||||
"zed/claude",
|
||||
"zed/gpt-5",
|
||||
"Antropic",
|
||||
"anthropic/claude",
|
||||
"OpenAI",
|
||||
"openai/gpt-5"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_flat_model_list_with_favorites(_cx: &mut TestAppContext) {
|
||||
let models = AgentModelList::Flat(vec![
|
||||
acp_thread::AgentModelInfo {
|
||||
id: acp::ModelId::new("zed/claude".to_string()),
|
||||
name: "Claude".into(),
|
||||
description: None,
|
||||
icon: None,
|
||||
},
|
||||
acp_thread::AgentModelInfo {
|
||||
id: acp::ModelId::new("zed/gemini".to_string()),
|
||||
name: "Gemini".into(),
|
||||
description: None,
|
||||
icon: None,
|
||||
},
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
|
||||
));
|
||||
|
||||
assert!(entries.iter().any(|e| matches!(
|
||||
e,
|
||||
AcpModelPickerEntry::Separator(s) if s == "All"
|
||||
)));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fuzzy_match(cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
|
||||
@@ -3,15 +3,15 @@ use std::sync::Arc;
|
||||
|
||||
use acp_thread::{AgentModelInfo, AgentModelSelector};
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::Settings as _;
|
||||
use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
use ui::{
|
||||
ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, TintColor, Tooltip, Window,
|
||||
prelude::*,
|
||||
};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
|
||||
|
||||
pub struct AcpModelSelectorPopover {
|
||||
@@ -54,12 +54,6 @@ impl AcpModelSelectorPopover {
|
||||
pub fn active_model<'a>(&self, cx: &'a App) -> Option<&'a AgentModelInfo> {
|
||||
self.selector.read(cx).delegate.active_model()
|
||||
}
|
||||
|
||||
pub fn cycle_favorite_models(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selector.update(cx, |selector, cx| {
|
||||
selector.delegate.cycle_favorite_models(window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AcpModelSelectorPopover {
|
||||
@@ -80,46 +74,6 @@ impl Render for AcpModelSelectorPopover {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
let should_show_cycle_row = !AgentSettings::get_global(cx)
|
||||
.favorite_model_ids()
|
||||
.is_empty();
|
||||
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Change Model"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(should_show_cycle_row, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.pt_1()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_between()
|
||||
.child(Label::new("Cycle Favorited Models"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&CycleFavoriteModels,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
});
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
@@ -134,7 +88,9 @@ impl Render for AcpModelSelectorPopover {
|
||||
.ml_0p5(),
|
||||
)
|
||||
.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)),
|
||||
tooltip,
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
},
|
||||
gpui::Corner::BottomRight,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ use language::Buffer;
|
||||
|
||||
use language_model::LanguageModelRegistry;
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
|
||||
use project::{Project, ProjectEntryId};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
use rope::Point;
|
||||
use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
|
||||
@@ -66,8 +66,8 @@ use crate::profile_selector::{ProfileProvider, ProfileSelector};
|
||||
use crate::ui::{AgentNotification, AgentNotificationEvent, BurnModeTooltip, UsageCallout};
|
||||
use crate::{
|
||||
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
|
||||
CycleFavoriteModels, CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread,
|
||||
OpenAgentDiff, OpenHistory, RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
|
||||
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory,
|
||||
RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -253,14 +253,13 @@ impl ThreadFeedbackState {
|
||||
editor
|
||||
});
|
||||
|
||||
editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
editor.read(cx).focus_handle(cx).focus(window);
|
||||
editor
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpThreadView {
|
||||
agent: Rc<dyn AgentServer>,
|
||||
agent_server_store: Entity<AgentServerStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
thread_state: ThreadState,
|
||||
@@ -390,24 +389,12 @@ impl AcpThreadView {
|
||||
),
|
||||
];
|
||||
|
||||
cx.on_release(|this, cx| {
|
||||
for window in this.notifications.drain(..) {
|
||||
window
|
||||
.update(cx, |_, window, _| {
|
||||
window.remove_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let show_codex_windows_warning = cfg!(windows)
|
||||
&& project.read(cx).is_local()
|
||||
&& agent.clone().downcast::<agent_servers::Codex>().is_some();
|
||||
|
||||
Self {
|
||||
agent: agent.clone(),
|
||||
agent_server_store,
|
||||
workspace: workspace.clone(),
|
||||
project: project.clone(),
|
||||
entry_view_state,
|
||||
@@ -684,7 +671,7 @@ impl AcpThreadView {
|
||||
})
|
||||
});
|
||||
|
||||
this.message_editor.focus_handle(cx).focus(window, cx);
|
||||
this.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
@@ -739,7 +726,7 @@ impl AcpThreadView {
|
||||
cx: &mut App,
|
||||
) {
|
||||
let agent_name = agent.name();
|
||||
let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
|
||||
let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
|
||||
let sub = window.subscribe(®istry, cx, {
|
||||
@@ -781,11 +768,12 @@ impl AcpThreadView {
|
||||
configuration_view,
|
||||
description: err
|
||||
.description
|
||||
.clone()
|
||||
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
|
||||
_subscription: subscription,
|
||||
};
|
||||
if this.message_editor.focus_handle(cx).is_focused(window) {
|
||||
this.focus_handle.focus(window, cx)
|
||||
this.focus_handle.focus(window)
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
@@ -805,7 +793,7 @@ impl AcpThreadView {
|
||||
ThreadState::LoadError(LoadError::Other(format!("{:#}", err).into()))
|
||||
}
|
||||
if self.message_editor.focus_handle(cx).is_focused(window) {
|
||||
self.focus_handle.focus(window, cx)
|
||||
self.focus_handle.focus(window)
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1089,7 +1077,10 @@ impl AcpThreadView {
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired::new(),
|
||||
AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
@@ -1268,7 +1259,7 @@ impl AcpThreadView {
|
||||
}
|
||||
})
|
||||
};
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1320,11 +1311,11 @@ impl AcpThreadView {
|
||||
.await?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.send_impl(message_editor, window, cx);
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn open_edited_buffer(
|
||||
@@ -1463,7 +1454,7 @@ impl AcpThreadView {
|
||||
self.thread_retry_status.take();
|
||||
self.thread_state = ThreadState::LoadError(error.clone());
|
||||
if self.message_editor.focus_handle(cx).is_focused(window) {
|
||||
self.focus_handle.focus(window, cx)
|
||||
self.focus_handle.focus(window)
|
||||
}
|
||||
}
|
||||
AcpThreadEvent::TitleUpdated => {
|
||||
@@ -1661,6 +1652,44 @@ impl AcpThreadView {
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if method.0.as_ref() == "anthropic-api-key" {
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
let provider = registry
|
||||
.read(cx)
|
||||
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
|
||||
.unwrap();
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
if !provider.is_authenticated(cx) {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
|
||||
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
this.project.clone(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else if method.0.as_ref() == "vertex-ai"
|
||||
&& std::env::var("GOOGLE_API_KEY").is_err()
|
||||
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
|
||||
@@ -1858,17 +1887,6 @@ impl AcpThreadView {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_user_submitted_prompt(&self, cx: &App) -> bool {
|
||||
self.thread().is_some_and(|thread| {
|
||||
thread.read(cx).entries().iter().any(|entry| {
|
||||
matches!(
|
||||
entry,
|
||||
AgentThreadEntry::UserMessage(user_message) if user_message.id.is_some()
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn authorize_tool_call(
|
||||
&mut self,
|
||||
tool_call_id: acp::ToolCallId,
|
||||
@@ -1922,16 +1940,6 @@ impl AcpThreadView {
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let is_indented = entry.is_indented();
|
||||
let is_first_indented = is_indented
|
||||
&& self.thread().is_some_and(|thread| {
|
||||
thread
|
||||
.read(cx)
|
||||
.entries()
|
||||
.get(entry_ix.saturating_sub(1))
|
||||
.is_none_or(|entry| !entry.is_indented())
|
||||
});
|
||||
|
||||
let primary = match &entry {
|
||||
AgentThreadEntry::UserMessage(message) => {
|
||||
let Some(editor) = self
|
||||
@@ -1964,9 +1972,7 @@ impl AcpThreadView {
|
||||
v_flex()
|
||||
.id(("user_message", entry_ix))
|
||||
.map(|this| {
|
||||
if is_first_indented {
|
||||
this.pt_0p5()
|
||||
} else if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() {
|
||||
if entry_ix == 0 && !has_checkpoint_button && rules_item.is_none() {
|
||||
this.pt(rems_from_px(18.))
|
||||
} else if rules_item.is_some() {
|
||||
this.pt_3()
|
||||
@@ -2012,9 +2018,6 @@ impl AcpThreadView {
|
||||
.shadow_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.when(is_indented, |this| {
|
||||
this.py_2().px_2().shadow_sm()
|
||||
})
|
||||
.when(editing && !editor_focus, |this| this.border_dashed())
|
||||
.border_color(cx.theme().colors().border)
|
||||
.map(|this|{
|
||||
@@ -2109,11 +2112,7 @@ impl AcpThreadView {
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
AgentThreadEntry::AssistantMessage(AssistantMessage {
|
||||
chunks,
|
||||
indented: _,
|
||||
}) => {
|
||||
let mut is_blank = true;
|
||||
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
|
||||
let is_last = entry_ix + 1 == total_entries;
|
||||
|
||||
let style = default_markdown_style(false, false, window, cx);
|
||||
@@ -2123,101 +2122,52 @@ impl AcpThreadView {
|
||||
.children(chunks.iter().enumerate().filter_map(
|
||||
|(chunk_ix, chunk)| match chunk {
|
||||
AssistantMessageChunk::Message { block } => {
|
||||
block.markdown().and_then(|md| {
|
||||
let this_is_blank = md.read(cx).source().trim().is_empty();
|
||||
is_blank = is_blank && this_is_blank;
|
||||
if this_is_blank {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_markdown(md.clone(), style.clone())
|
||||
.into_any_element(),
|
||||
)
|
||||
block.markdown().map(|md| {
|
||||
self.render_markdown(md.clone(), style.clone())
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
AssistantMessageChunk::Thought { block } => {
|
||||
block.markdown().and_then(|md| {
|
||||
let this_is_blank = md.read(cx).source().trim().is_empty();
|
||||
is_blank = is_blank && this_is_blank;
|
||||
if this_is_blank {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_thinking_block(
|
||||
entry_ix,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
block.markdown().map(|md| {
|
||||
self.render_thinking_block(
|
||||
entry_ix,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
},
|
||||
))
|
||||
.into_any();
|
||||
|
||||
if is_blank {
|
||||
Empty.into_any()
|
||||
} else {
|
||||
v_flex()
|
||||
.px_5()
|
||||
.py_1p5()
|
||||
.when(is_last, |this| this.pb_4())
|
||||
.w_full()
|
||||
.text_ui(cx)
|
||||
.child(message_body)
|
||||
.into_any()
|
||||
}
|
||||
v_flex()
|
||||
.px_5()
|
||||
.py_1p5()
|
||||
.when(is_last, |this| this.pb_4())
|
||||
.w_full()
|
||||
.text_ui(cx)
|
||||
.child(message_body)
|
||||
.into_any()
|
||||
}
|
||||
AgentThreadEntry::ToolCall(tool_call) => {
|
||||
let has_terminals = tool_call.terminals().next().is_some();
|
||||
|
||||
div()
|
||||
.w_full()
|
||||
.map(|this| {
|
||||
if has_terminals {
|
||||
this.children(tool_call.terminals().map(|terminal| {
|
||||
self.render_terminal_tool_call(
|
||||
entry_ix, terminal, tool_call, window, cx,
|
||||
)
|
||||
}))
|
||||
} else {
|
||||
this.child(self.render_tool_call(entry_ix, tool_call, window, cx))
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
div().w_full().map(|this| {
|
||||
if has_terminals {
|
||||
this.children(tool_call.terminals().map(|terminal| {
|
||||
self.render_terminal_tool_call(
|
||||
entry_ix, terminal, tool_call, window, cx,
|
||||
)
|
||||
}))
|
||||
} else {
|
||||
this.child(self.render_tool_call(entry_ix, tool_call, window, cx))
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let primary = if is_indented {
|
||||
let line_top = if is_first_indented {
|
||||
rems_from_px(-12.0)
|
||||
} else {
|
||||
rems_from_px(0.0)
|
||||
};
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.w_full()
|
||||
.pl_5()
|
||||
.bg(cx.theme().colors().panel_background.opacity(0.2))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.left(rems_from_px(18.0))
|
||||
.top(line_top)
|
||||
.bottom_0()
|
||||
.w_px()
|
||||
.bg(cx.theme().colors().border.opacity(0.6)),
|
||||
)
|
||||
.child(primary)
|
||||
.into_any_element()
|
||||
} else {
|
||||
primary
|
||||
.into_any(),
|
||||
};
|
||||
|
||||
let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry {
|
||||
@@ -2420,12 +2370,6 @@ impl AcpThreadView {
|
||||
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
||||
|
||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
let input_output_header = |label: SharedString| {
|
||||
Label::new(label)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx)
|
||||
};
|
||||
|
||||
let tool_output_display =
|
||||
if is_open {
|
||||
@@ -2467,25 +2411,7 @@ impl AcpThreadView {
|
||||
| ToolCallStatus::Completed
|
||||
| ToolCallStatus::Failed
|
||||
| ToolCallStatus::Canceled => v_flex()
|
||||
.when(!is_edit && !is_terminal_tool, |this| {
|
||||
this.mt_1p5().w_full().child(
|
||||
v_flex()
|
||||
.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.pb_1()
|
||||
.gap_1()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(input_output_header("Raw Input:".into()))
|
||||
.children(tool_call.raw_input_markdown.clone().map(|input| {
|
||||
self.render_markdown(
|
||||
input,
|
||||
default_markdown_style(false, false, window, cx),
|
||||
)
|
||||
}))
|
||||
.child(input_output_header("Output:".into())),
|
||||
)
|
||||
})
|
||||
.w_full()
|
||||
.children(tool_call.content.iter().enumerate().map(
|
||||
|(content_ix, content)| {
|
||||
div().child(self.render_tool_call_content(
|
||||
@@ -2584,7 +2510,7 @@ impl AcpThreadView {
|
||||
.gap_px()
|
||||
.when(is_collapsible, |this| {
|
||||
this.child(
|
||||
Disclosure::new(("expand-output", entry_ix), is_open)
|
||||
Disclosure::new(("expand", entry_ix), is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.visible_on_hover(&card_header_id)
|
||||
@@ -2770,20 +2696,20 @@ impl AcpThreadView {
|
||||
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
|
||||
|
||||
v_flex()
|
||||
.mt_1p5()
|
||||
.gap_2()
|
||||
.map(|this| {
|
||||
if card_layout {
|
||||
this.when(context_ix > 0, |this| {
|
||||
this.pt_2()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
})
|
||||
} else {
|
||||
this.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.border_l_1()
|
||||
.when(!card_layout, |this| {
|
||||
this.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
})
|
||||
.when(card_layout, |this| {
|
||||
this.px_2().pb_2().when(context_ix > 0, |this| {
|
||||
this.border_t_1()
|
||||
.pt_2()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
}
|
||||
})
|
||||
})
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
@@ -3504,119 +3430,138 @@ impl AcpThreadView {
|
||||
pending_auth_method: Option<&acp::AuthMethodId>,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
) -> Div {
|
||||
let show_description =
|
||||
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
|
||||
|
||||
let auth_methods = connection.auth_methods();
|
||||
|
||||
let agent_display_name = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
|
||||
.unwrap_or_else(|| self.agent.name());
|
||||
|
||||
let show_fallback_description = auth_methods.len() > 1
|
||||
&& configuration_view.is_none()
|
||||
&& description.is_none()
|
||||
&& pending_auth_method.is_none();
|
||||
|
||||
let auth_buttons = || {
|
||||
h_flex().justify_end().flex_wrap().gap_1().children(
|
||||
connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.map(|(ix, method)| {
|
||||
let (method_id, name) = if self.project.read(cx).is_via_remote_server()
|
||||
&& method.id.0.as_ref() == "oauth-personal"
|
||||
&& method.name == "Log in with Google"
|
||||
{
|
||||
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
|
||||
} else {
|
||||
(method.id.0.clone(), method.name.clone())
|
||||
};
|
||||
|
||||
let agent_telemetry_id = connection.telemetry_id();
|
||||
|
||||
Button::new(method_id.clone(), name)
|
||||
.label_size(LabelSize::Small)
|
||||
.map(|this| {
|
||||
if ix == 0 {
|
||||
this.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
} else {
|
||||
this.style(ButtonStyle::Outlined)
|
||||
}
|
||||
})
|
||||
.when_some(method.description.clone(), |this, description| {
|
||||
this.tooltip(Tooltip::text(description))
|
||||
})
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = agent_telemetry_id,
|
||||
method = method_id
|
||||
);
|
||||
|
||||
this.authenticate(
|
||||
acp::AuthMethodId::new(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
if pending_auth_method.is_some() {
|
||||
return Callout::new()
|
||||
.icon(IconName::Info)
|
||||
.title(format!("Authenticating to {}…", agent_display_name))
|
||||
.actions_slot(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
v_flex().flex_1().size_full().justify_end().child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
.pr_3()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().status().warning.opacity(0.04))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.color(Color::Warning)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(Label::new("Authentication Required").size(LabelSize::Small)),
|
||||
)
|
||||
.into_any_element();
|
||||
}
|
||||
.children(description.map(|desc| {
|
||||
div().text_ui(cx).child(self.render_markdown(
|
||||
desc.clone(),
|
||||
default_markdown_style(false, false, window, cx),
|
||||
))
|
||||
}))
|
||||
.children(
|
||||
configuration_view
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.when(show_description, |el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}.{}",
|
||||
self.agent.name(),
|
||||
if auth_methods.len() > 1 {
|
||||
" Please choose one of the following options:"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
})
|
||||
.when_some(pending_auth_method, |el, _| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.py_4()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
.child(Label::new("Authenticating…").size(LabelSize::Small)),
|
||||
)
|
||||
})
|
||||
.when(!auth_methods.is_empty(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.when(!show_description, |this| {
|
||||
this.border_t_1()
|
||||
.mt_1()
|
||||
.pt_2()
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
})
|
||||
.children(connection.auth_methods().iter().enumerate().rev().map(
|
||||
|(ix, method)| {
|
||||
let (method_id, name) = if self
|
||||
.project
|
||||
.read(cx)
|
||||
.is_via_remote_server()
|
||||
&& method.id.0.as_ref() == "oauth-personal"
|
||||
&& method.name == "Log in with Google"
|
||||
{
|
||||
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
|
||||
} else {
|
||||
(method.id.0.clone(), method.name.clone())
|
||||
};
|
||||
|
||||
Callout::new()
|
||||
.icon(IconName::Info)
|
||||
.title(format!("Authenticate to {}", agent_display_name))
|
||||
.when(auth_methods.len() == 1, |this| {
|
||||
this.actions_slot(auth_buttons())
|
||||
})
|
||||
.description_slot(
|
||||
v_flex()
|
||||
.text_ui(cx)
|
||||
.map(|this| {
|
||||
if show_fallback_description {
|
||||
this.child(
|
||||
Label::new("Choose one of the following authentication options:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
this.children(
|
||||
configuration_view
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.children(description.map(|desc| {
|
||||
self.render_markdown(
|
||||
desc.clone(),
|
||||
default_markdown_style(false, false, window, cx),
|
||||
)
|
||||
}))
|
||||
}
|
||||
})
|
||||
.when(auth_methods.len() > 1, |this| {
|
||||
this.gap_1().child(auth_buttons())
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
let agent_telemetry_id = connection.telemetry_id();
|
||||
|
||||
Button::new(method_id.clone(), name)
|
||||
.label_size(LabelSize::Small)
|
||||
.map(|this| {
|
||||
if ix == 0 {
|
||||
this.style(ButtonStyle::Tinted(TintColor::Warning))
|
||||
} else {
|
||||
this.style(ButtonStyle::Outlined)
|
||||
}
|
||||
})
|
||||
.when_some(
|
||||
method.description.clone(),
|
||||
|this, description| {
|
||||
this.tooltip(Tooltip::text(description))
|
||||
},
|
||||
)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = agent_telemetry_id,
|
||||
method = method_id
|
||||
);
|
||||
|
||||
this.authenticate(
|
||||
acp::AuthMethodId::new(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_load_error(
|
||||
@@ -4106,8 +4051,6 @@ impl AcpThreadView {
|
||||
.ml_1p5()
|
||||
});
|
||||
|
||||
let full_path = path.display(path_style).to_string();
|
||||
|
||||
let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
|
||||
@@ -4141,6 +4084,7 @@ impl AcpThreadView {
|
||||
.relative()
|
||||
.pr_8()
|
||||
.w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
h_flex()
|
||||
.id(("file-name-path", index))
|
||||
@@ -4152,14 +4096,7 @@ impl AcpThreadView {
|
||||
.child(file_icon)
|
||||
.children(file_name)
|
||||
.children(file_path)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Go to File",
|
||||
None,
|
||||
full_path.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.tooltip(Tooltip::text("Go to File"))
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
@@ -4297,13 +4234,6 @@ impl AcpThreadView {
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
|
||||
if let Some(model_selector) = this.model_selector.as_ref() {
|
||||
model_selector.update(cx, |model_selector, cx| {
|
||||
model_selector.cycle_favorite_models(window, cx);
|
||||
});
|
||||
}
|
||||
}))
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
@@ -5064,8 +4994,8 @@ impl AcpThreadView {
|
||||
});
|
||||
|
||||
if let Some(screen_window) = cx
|
||||
.open_window(options, |_window, cx| {
|
||||
cx.new(|_cx| {
|
||||
.open_window(options, |_, cx| {
|
||||
cx.new(|_| {
|
||||
AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
|
||||
})
|
||||
})
|
||||
@@ -5865,6 +5795,10 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let err = AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
};
|
||||
this.clear_thread_error(cx);
|
||||
if let Some(message) = this.in_flight_prompt.take() {
|
||||
this.message_editor.update(cx, |editor, cx| {
|
||||
@@ -5873,14 +5807,7 @@ impl AcpThreadView {
|
||||
}
|
||||
let this = cx.weak_entity();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired::new(),
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
@@ -5893,10 +5820,14 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let err = AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
};
|
||||
self.clear_thread_error(cx);
|
||||
let this = cx.weak_entity();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx);
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5999,19 +5930,16 @@ impl Render for AcpThreadView {
|
||||
configuration_view,
|
||||
pending_auth_method,
|
||||
..
|
||||
} => v_flex()
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.justify_end()
|
||||
.child(self.render_auth_required_state(
|
||||
} => self
|
||||
.render_auth_required_state(
|
||||
connection,
|
||||
description.as_ref(),
|
||||
configuration_view.as_ref(),
|
||||
pending_auth_method.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any(),
|
||||
ThreadState::Loading { .. } => v_flex()
|
||||
.flex_1()
|
||||
.child(self.render_recent_history(cx))
|
||||
@@ -6493,57 +6421,6 @@ pub(crate) mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_notification_closed_when_thread_view_dropped(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
|
||||
|
||||
let weak_view = thread_view.downgrade();
|
||||
|
||||
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||
message_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Hello", window, cx);
|
||||
});
|
||||
|
||||
cx.deactivate_window();
|
||||
|
||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||
thread_view.send(window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify notification is shown
|
||||
assert!(
|
||||
cx.windows()
|
||||
.iter()
|
||||
.any(|window| window.downcast::<AgentNotification>().is_some()),
|
||||
"Expected notification to be shown"
|
||||
);
|
||||
|
||||
// Drop the thread view (simulating navigation to a new thread)
|
||||
drop(thread_view);
|
||||
drop(message_editor);
|
||||
// Trigger an update to flush effects, which will call release_dropped_entities
|
||||
cx.update(|_window, _cx| {});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify the entity was actually released
|
||||
assert!(
|
||||
!weak_view.is_upgradable(),
|
||||
"Thread view entity should be released after dropping"
|
||||
);
|
||||
|
||||
// The notification should be automatically closed via on_release
|
||||
assert!(
|
||||
!cx.windows()
|
||||
.iter()
|
||||
.any(|window| window.downcast::<AgentNotification>().is_some()),
|
||||
"Notification should be closed when thread view is dropped"
|
||||
);
|
||||
}
|
||||
|
||||
async fn setup_thread_view(
|
||||
agent: impl AgentServer + 'static,
|
||||
cx: &mut TestAppContext,
|
||||
|
||||
@@ -446,17 +446,17 @@ impl AddLlmProviderModal {
|
||||
})
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus_next(cx);
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_next();
|
||||
}
|
||||
|
||||
fn on_tab_prev(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
window.focus_prev(cx);
|
||||
window.focus_prev();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +493,7 @@ impl Render for AddLlmProviderModal {
|
||||
.on_action(cx.listener(Self::on_tab))
|
||||
.on_action(cx.listener(Self::on_tab_prev))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.child(
|
||||
Modal::new("configure-context-server", None)
|
||||
|
||||
@@ -831,7 +831,7 @@ impl Render for ConfigureContextServerModal {
|
||||
}),
|
||||
)
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.child(
|
||||
Modal::new("configure-context-server", None)
|
||||
|
||||
@@ -156,7 +156,7 @@ impl ManageProfilesModal {
|
||||
cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
|
||||
if matches!(this.mode, Mode::ChooseProfile(_)) {
|
||||
this.mode = Mode::choose_profile(window, cx);
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
@@ -173,7 +173,7 @@ impl ManageProfilesModal {
|
||||
|
||||
fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mode = Mode::choose_profile(window, cx);
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn new_profile(
|
||||
@@ -191,7 +191,7 @@ impl ManageProfilesModal {
|
||||
name_editor,
|
||||
base_profile_id,
|
||||
});
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
pub fn view_profile(
|
||||
@@ -209,7 +209,7 @@ impl ManageProfilesModal {
|
||||
delete_profile: NavigableEntry::focusable(cx),
|
||||
cancel_item: NavigableEntry::focusable(cx),
|
||||
});
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_default_model(
|
||||
@@ -222,6 +222,7 @@ impl ManageProfilesModal {
|
||||
let profile_id_for_closure = profile_id.clone();
|
||||
|
||||
let model_picker = cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
let profile_id = profile_id_for_closure.clone();
|
||||
|
||||
language_model_selector(
|
||||
@@ -249,36 +250,22 @@ impl ManageProfilesModal {
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
let profile_id = profile_id.clone();
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
let profile_id = profile_id.clone();
|
||||
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
let agent_settings = settings.agent.get_or_insert_default();
|
||||
if let Some(profiles) = agent_settings.profiles.as_mut() {
|
||||
if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
|
||||
profile.default_model = Some(LanguageModelSelection {
|
||||
provider: LanguageModelProviderSetting(provider.clone()),
|
||||
model: model_id.clone(),
|
||||
});
|
||||
}
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
let agent_settings = settings.agent.get_or_insert_default();
|
||||
if let Some(profiles) = agent_settings.profiles.as_mut() {
|
||||
if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
|
||||
profile.default_model = Some(LanguageModelSelection {
|
||||
provider: LanguageModelProviderSetting(provider.clone()),
|
||||
model: model_id.clone(),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, should_be_favorite, cx| {
|
||||
crate::favorite_models::toggle_in_settings(
|
||||
model,
|
||||
should_be_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
false, // Do not use popover styles for the model picker
|
||||
self.focus_handle.clone(),
|
||||
@@ -300,7 +287,7 @@ impl ManageProfilesModal {
|
||||
model_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
};
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_mcp_tools(
|
||||
@@ -336,7 +323,7 @@ impl ManageProfilesModal {
|
||||
tool_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
};
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_builtin_tools(
|
||||
@@ -377,7 +364,7 @@ impl ManageProfilesModal {
|
||||
tool_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
};
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -951,7 +938,7 @@ impl Render for ManageProfilesModal {
|
||||
.on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
|
||||
.child(match &self.mode {
|
||||
|
||||
@@ -212,10 +212,10 @@ impl AgentDiffPane {
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx)
|
||||
{
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
} else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.focus_handle(cx).focus(window, cx);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -874,12 +874,12 @@ impl AgentDiffToolbar {
|
||||
match active_item {
|
||||
AgentDiffToolbarItem::Pane(agent_diff) => {
|
||||
if let Some(agent_diff) = agent_diff.upgrade() {
|
||||
agent_diff.focus_handle(cx).focus(window, cx);
|
||||
agent_diff.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
AgentDiffToolbarItem::Editor { editor, .. } => {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
editor.read(cx).focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,39 +29,26 @@ impl AgentModelSelector {
|
||||
|
||||
Self {
|
||||
selector: cx.new(move |cx| {
|
||||
let fs = fs.clone();
|
||||
language_model_selector(
|
||||
{
|
||||
let model_context = model_usage_context.clone();
|
||||
move |cx| model_context.configured_model(cx)
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
match &model_usage_context {
|
||||
ModelUsageContext::InlineAssistant => {
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_inline_assistant_model(provider.clone(), model_id);
|
||||
});
|
||||
}
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
match &model_usage_context {
|
||||
ModelUsageContext::InlineAssistant => {
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_inline_assistant_model(provider.clone(), model_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, should_be_favorite, cx| {
|
||||
crate::favorite_models::toggle_in_settings(
|
||||
model,
|
||||
should_be_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
true, // Use popover styles for picker
|
||||
focus_handle_clone,
|
||||
window,
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
use acp_thread::AcpThread;
|
||||
use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore};
|
||||
use agent_servers::AgentServer;
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use project::{
|
||||
ExternalAgentServerName,
|
||||
@@ -288,7 +287,7 @@ impl ActiveView {
|
||||
}
|
||||
}
|
||||
|
||||
fn native_agent(
|
||||
pub fn native_agent(
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
history_store: Entity<agent::HistoryStore>,
|
||||
@@ -443,7 +442,6 @@ pub struct AgentPanel {
|
||||
pending_serialization: Option<Task<Result<()>>>,
|
||||
onboarding: Entity<AgentPanelOnboarding>,
|
||||
selected_agent: AgentType,
|
||||
show_trust_workspace_message: bool,
|
||||
}
|
||||
|
||||
impl AgentPanel {
|
||||
@@ -694,7 +692,6 @@ impl AgentPanel {
|
||||
history_store,
|
||||
selected_agent: AgentType::default(),
|
||||
loading: false,
|
||||
show_trust_workspace_message: false,
|
||||
};
|
||||
|
||||
// Initial sync of agent servers from extensions
|
||||
@@ -822,7 +819,7 @@ impl AgentPanel {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
text_thread_editor.focus_handle(cx).focus(window, cx);
|
||||
text_thread_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn external_thread(
|
||||
@@ -888,21 +885,36 @@ impl AgentPanel {
|
||||
};
|
||||
|
||||
let server = ext_agent.server(fs, history);
|
||||
this.update_in(cx, |agent_panel, window, cx| {
|
||||
agent_panel._external_thread(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace,
|
||||
project,
|
||||
loading,
|
||||
ext_agent,
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let selected_agent = ext_agent.into();
|
||||
if this.selected_agent != selected_agent {
|
||||
this.selected_agent = selected_agent;
|
||||
this.serialize(cx);
|
||||
}
|
||||
|
||||
let thread_view = cx.new(|cx| {
|
||||
crate::acp::AcpThreadView::new(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace.clone(),
|
||||
project,
|
||||
this.history_store.clone(),
|
||||
this.prompt_store.clone(),
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
this.set_active_view(
|
||||
ActiveView::ExternalAgentThread { thread_view },
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -935,7 +947,7 @@ impl AgentPanel {
|
||||
if let Some(thread_view) = self.active_thread_view() {
|
||||
thread_view.update(cx, |view, cx| {
|
||||
view.expand_message_editor(&ExpandMessageEditor, window, cx);
|
||||
view.focus_handle(cx).focus(window, cx);
|
||||
view.focus_handle(cx).focus(window);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1016,12 +1028,12 @@ impl AgentPanel {
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => {
|
||||
thread_view.focus_handle(cx).focus(window, cx);
|
||||
thread_view.focus_handle(cx).focus(window);
|
||||
}
|
||||
ActiveView::TextThread {
|
||||
text_thread_editor, ..
|
||||
} => {
|
||||
text_thread_editor.focus_handle(cx).focus(window, cx);
|
||||
text_thread_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
@@ -1169,7 +1181,7 @@ impl AgentPanel {
|
||||
Self::handle_agent_configuration_event,
|
||||
));
|
||||
|
||||
configuration.focus_handle(cx).focus(window, cx);
|
||||
configuration.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1305,7 +1317,7 @@ impl AgentPanel {
|
||||
}
|
||||
|
||||
if focus {
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1465,47 +1477,6 @@ impl AgentPanel {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn _external_thread(
|
||||
&mut self,
|
||||
server: Rc<dyn AgentServer>,
|
||||
resume_thread: Option<DbThreadMetadata>,
|
||||
summarize_thread: Option<DbThreadMetadata>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
loading: bool,
|
||||
ext_agent: ExternalAgent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let selected_agent = AgentType::from(ext_agent);
|
||||
if self.selected_agent != selected_agent {
|
||||
self.selected_agent = selected_agent;
|
||||
self.serialize(cx);
|
||||
}
|
||||
|
||||
let thread_view = cx.new(|cx| {
|
||||
crate::acp::AcpThreadView::new(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace.clone(),
|
||||
project,
|
||||
self.history_store.clone(),
|
||||
self.prompt_store.clone(),
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
self.set_active_view(
|
||||
ActiveView::ExternalAgentThread { thread_view },
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AgentPanel {
|
||||
@@ -1620,19 +1591,14 @@ impl AgentPanel {
|
||||
|
||||
let content = match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => {
|
||||
let is_generating_title = thread_view
|
||||
.read(cx)
|
||||
.as_native_thread(cx)
|
||||
.map_or(false, |t| t.read(cx).is_generating_title());
|
||||
|
||||
if let Some(title_editor) = thread_view.read(cx).title_editor() {
|
||||
let container = div()
|
||||
div()
|
||||
.w_full()
|
||||
.on_action({
|
||||
let thread_view = thread_view.downgrade();
|
||||
move |_: &menu::Confirm, window, cx| {
|
||||
if let Some(thread_view) = thread_view.upgrade() {
|
||||
thread_view.focus_handle(cx).focus(window, cx);
|
||||
thread_view.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1640,25 +1606,12 @@ impl AgentPanel {
|
||||
let thread_view = thread_view.downgrade();
|
||||
move |_: &editor::actions::Cancel, window, cx| {
|
||||
if let Some(thread_view) = thread_view.upgrade() {
|
||||
thread_view.focus_handle(cx).focus(window, cx);
|
||||
thread_view.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
})
|
||||
.child(title_editor);
|
||||
|
||||
if is_generating_title {
|
||||
container
|
||||
.with_animation(
|
||||
"generating_title",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|div, delta| div.opacity(delta),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
container.into_any_element()
|
||||
}
|
||||
.child(title_editor)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(thread_view.read(cx).title(cx))
|
||||
.color(Color::Muted)
|
||||
@@ -1688,13 +1641,6 @@ impl AgentPanel {
|
||||
Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
||||
.truncate()
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"generating_title",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.alpha(delta),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -1738,25 +1684,6 @@ impl AgentPanel {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn handle_regenerate_thread_title(thread_view: Entity<AcpThreadView>, cx: &mut App) {
|
||||
thread_view.update(cx, |thread_view, cx| {
|
||||
if let Some(thread) = thread_view.as_native_thread(cx) {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.generate_title(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_regenerate_text_thread_title(
|
||||
text_thread_editor: Entity<TextThreadEditor>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
text_thread_editor.update(cx, |text_thread_editor, cx| {
|
||||
text_thread_editor.regenerate_summary(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_panel_options_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
@@ -1776,35 +1703,6 @@ impl AgentPanel {
|
||||
|
||||
let selected_agent = self.selected_agent.clone();
|
||||
|
||||
let text_thread_view = match &self.active_view {
|
||||
ActiveView::TextThread {
|
||||
text_thread_editor, ..
|
||||
} => Some(text_thread_editor.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let text_thread_with_messages = match &self.active_view {
|
||||
ActiveView::TextThread {
|
||||
text_thread_editor, ..
|
||||
} => text_thread_editor
|
||||
.read(cx)
|
||||
.text_thread()
|
||||
.read(cx)
|
||||
.messages(cx)
|
||||
.any(|message| message.role == language_model::Role::Assistant),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let thread_view = match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => Some(thread_view.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let thread_with_messages = match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => {
|
||||
thread_view.read(cx).has_user_submitted_prompt(cx)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
PopoverMenu::new("agent-options-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
||||
@@ -1827,7 +1725,6 @@ impl AgentPanel {
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
|
||||
menu = menu.context(focus_handle.clone());
|
||||
|
||||
if let Some(usage) = usage {
|
||||
menu = menu
|
||||
.header_with_link("Prompt Usage", "Manage", account_url.clone())
|
||||
@@ -1865,38 +1762,6 @@ impl AgentPanel {
|
||||
.separator()
|
||||
}
|
||||
|
||||
if thread_with_messages | text_thread_with_messages {
|
||||
menu = menu.header("Current Thread");
|
||||
|
||||
if let Some(text_thread_view) = text_thread_view.as_ref() {
|
||||
menu = menu
|
||||
.entry("Regenerate Thread Title", None, {
|
||||
let text_thread_view = text_thread_view.clone();
|
||||
move |_, cx| {
|
||||
Self::handle_regenerate_text_thread_title(
|
||||
text_thread_view.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.separator();
|
||||
}
|
||||
|
||||
if let Some(thread_view) = thread_view.as_ref() {
|
||||
menu = menu
|
||||
.entry("Regenerate Thread Title", None, {
|
||||
let thread_view = thread_view.clone();
|
||||
move |_, cx| {
|
||||
Self::handle_regenerate_thread_title(
|
||||
thread_view.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.separator();
|
||||
}
|
||||
}
|
||||
|
||||
menu = menu
|
||||
.header("MCP Servers")
|
||||
.action(
|
||||
@@ -2692,38 +2557,6 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
|
||||
if !self.show_trust_workspace_message {
|
||||
return None;
|
||||
}
|
||||
|
||||
let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
|
||||
|
||||
Some(
|
||||
Callout::new()
|
||||
.icon(IconName::Warning)
|
||||
.severity(Severity::Warning)
|
||||
.border_position(ui::BorderPosition::Bottom)
|
||||
.title("You're in Restricted Mode")
|
||||
.description(description)
|
||||
.actions_slot(
|
||||
Button::new("open-trust-modal", "Configure Project Trust")
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.show_worktree_trust_security_modal(true, window, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
@@ -2776,7 +2609,6 @@ impl Render for AgentPanel {
|
||||
}
|
||||
}))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.children(self.render_workspace_trust_message(cx))
|
||||
.children(self.render_onboarding(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view, .. } => parent
|
||||
|
||||
@@ -7,7 +7,6 @@ mod buffer_codegen;
|
||||
mod completion_provider;
|
||||
mod context;
|
||||
mod context_server_configuration;
|
||||
mod favorite_models;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod language_model_selector;
|
||||
@@ -68,8 +67,6 @@ actions!(
|
||||
ToggleProfileSelector,
|
||||
/// Cycles through available session modes.
|
||||
CycleModeSelector,
|
||||
/// Cycles through favorited models in the ACP model selector.
|
||||
CycleFavoriteModels,
|
||||
/// Expands the message editor to full size.
|
||||
ExpandMessageEditor,
|
||||
/// Removes all thread history.
|
||||
@@ -460,7 +457,6 @@ mod tests {
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: vec![],
|
||||
favorite_models: vec![],
|
||||
default_profile: AgentProfileId::default(),
|
||||
default_view: DefaultAgentView::Thread,
|
||||
profiles: Default::default(),
|
||||
|
||||
@@ -75,9 +75,6 @@ pub struct BufferCodegen {
|
||||
session_id: Uuid,
|
||||
}
|
||||
|
||||
pub const REWRITE_SECTION_TOOL_NAME: &str = "rewrite_section";
|
||||
pub const FAILURE_MESSAGE_TOOL_NAME: &str = "failure_message";
|
||||
|
||||
impl BufferCodegen {
|
||||
pub fn new(
|
||||
buffer: Entity<MultiBuffer>,
|
||||
@@ -444,8 +441,7 @@ impl CodegenAlternative {
|
||||
})
|
||||
.boxed_local()
|
||||
};
|
||||
self.generation =
|
||||
self.handle_stream(model, /* strip_invalid_spans: */ true, stream, cx);
|
||||
self.generation = self.handle_stream(model, stream, cx);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -525,12 +521,12 @@ impl CodegenAlternative {
|
||||
|
||||
let tools = vec![
|
||||
LanguageModelRequestTool {
|
||||
name: REWRITE_SECTION_TOOL_NAME.to_string(),
|
||||
name: "rewrite_section".to_string(),
|
||||
description: "Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
|
||||
},
|
||||
LanguageModelRequestTool {
|
||||
name: FAILURE_MESSAGE_TOOL_NAME.to_string(),
|
||||
name: "failure_message".to_string(),
|
||||
description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
|
||||
},
|
||||
@@ -633,7 +629,6 @@ impl CodegenAlternative {
|
||||
pub fn handle_stream(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
strip_invalid_spans: bool,
|
||||
stream: impl 'static + Future<Output = Result<LanguageModelTextStream>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<()> {
|
||||
@@ -718,16 +713,10 @@ impl CodegenAlternative {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let raw_stream = stream?.stream.map_err(|error| error.into());
|
||||
|
||||
let stripped;
|
||||
let mut chunks: Pin<Box<dyn Stream<Item = Result<String>> + Send>> =
|
||||
if strip_invalid_spans {
|
||||
stripped = StripInvalidSpans::new(raw_stream);
|
||||
Box::pin(stripped)
|
||||
} else {
|
||||
Box::pin(raw_stream)
|
||||
};
|
||||
let chunks = StripInvalidSpans::new(
|
||||
stream?.stream.map_err(|error| error.into()),
|
||||
);
|
||||
futures::pin_mut!(chunks);
|
||||
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
let mut line_diff = LineDiff::default();
|
||||
@@ -1170,7 +1159,7 @@ impl CodegenAlternative {
|
||||
let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option<ToolUseOutput> {
|
||||
let mut chars_read_so_far = chars_read_so_far.lock();
|
||||
match tool_use.name.as_ref() {
|
||||
REWRITE_SECTION_TOOL_NAME => {
|
||||
"rewrite_section" => {
|
||||
let Ok(input) =
|
||||
serde_json::from_value::<RewriteSectionInput>(tool_use.input)
|
||||
else {
|
||||
@@ -1183,7 +1172,7 @@ impl CodegenAlternative {
|
||||
description: None,
|
||||
})
|
||||
}
|
||||
FAILURE_MESSAGE_TOOL_NAME => {
|
||||
"failure_message" => {
|
||||
let Ok(mut input) =
|
||||
serde_json::from_value::<FailureMessageInput>(tool_use.input)
|
||||
else {
|
||||
@@ -1318,12 +1307,7 @@ impl CodegenAlternative {
|
||||
|
||||
let Some(task) = codegen
|
||||
.update(cx, move |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
model,
|
||||
/* strip_invalid_spans: */ false,
|
||||
async { Ok(language_model_text_stream) },
|
||||
cx,
|
||||
)
|
||||
codegen.handle_stream(model, async { Ok(language_model_text_stream) }, cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
@@ -1496,10 +1480,7 @@ mod tests {
|
||||
use indoc::indoc;
|
||||
use language::{Buffer, Point};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use language_model::{
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRegistry,
|
||||
LanguageModelToolUse, StopReason, TokenUsage,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use languages::rust_lang;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
@@ -1811,51 +1792,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// When not streaming tool calls, we strip backticks as part of parsing the model's
|
||||
// plain text response. This is a regression test for a bug where we stripped
|
||||
// backticks incorrectly.
|
||||
#[gpui::test]
|
||||
async fn test_allows_model_to_output_backticks(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let text = "- Improved; `cmd+click` behavior. Now requires `cmd` to be pressed before the click starts or it doesn't run. ([#44579](https://github.com/zed-industries/zed/pull/44579); thanks [Zachiah](https://github.com/Zachiah))";
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(0, 0))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
prompt_builder,
|
||||
Uuid::new_v4(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let events_tx = simulate_tool_based_completion(&codegen, cx);
|
||||
let chunk_len = text.find('`').unwrap();
|
||||
events_tx
|
||||
.unbounded_send(rewrite_tool_use("tool_1", &text[..chunk_len], false))
|
||||
.unwrap();
|
||||
events_tx
|
||||
.unbounded_send(rewrite_tool_use("tool_2", &text, true))
|
||||
.unwrap();
|
||||
events_tx
|
||||
.unbounded_send(LanguageModelCompletionEvent::Stop(StopReason::EndTurn))
|
||||
.unwrap();
|
||||
drop(events_tx);
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await;
|
||||
@@ -1910,7 +1846,6 @@ mod tests {
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.generation = codegen.handle_stream(
|
||||
model,
|
||||
/* strip_invalid_spans: */ false,
|
||||
future::ready(Ok(LanguageModelTextStream {
|
||||
message_id: None,
|
||||
stream: chunks_rx.map(Ok).boxed(),
|
||||
@@ -1921,39 +1856,4 @@ mod tests {
|
||||
});
|
||||
chunks_tx
|
||||
}
|
||||
|
||||
fn simulate_tool_based_completion(
|
||||
codegen: &Entity<CodegenAlternative>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> mpsc::UnboundedSender<LanguageModelCompletionEvent> {
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
let completion_stream = Task::ready(Ok(events_rx.map(Ok).boxed()
|
||||
as BoxStream<
|
||||
'static,
|
||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>));
|
||||
codegen.generation = codegen.handle_completion(model, completion_stream, cx);
|
||||
});
|
||||
events_tx
|
||||
}
|
||||
|
||||
fn rewrite_tool_use(
|
||||
id: &str,
|
||||
replacement_text: &str,
|
||||
is_complete: bool,
|
||||
) -> LanguageModelCompletionEvent {
|
||||
let input = RewriteSectionInput {
|
||||
replacement_text: replacement_text.into(),
|
||||
};
|
||||
LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
|
||||
id: id.into(),
|
||||
name: REWRITE_SECTION_TOOL_NAME.into(),
|
||||
raw_input: serde_json::to_string(&input).unwrap(),
|
||||
input: serde_json::to_value(&input).unwrap(),
|
||||
is_input_complete: is_complete,
|
||||
thought_signature: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse,
|
||||
PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId,
|
||||
};
|
||||
use prompt_store::{PromptStore, UserPromptId};
|
||||
use prompt_store::{PromptId, PromptStore, UserPromptId};
|
||||
use rope::Point;
|
||||
use text::{Anchor, ToPoint as _};
|
||||
use ui::prelude::*;
|
||||
@@ -1585,10 +1585,13 @@ pub(crate) fn search_rules(
|
||||
if metadata.default {
|
||||
None
|
||||
} else {
|
||||
Some(RulesContextEntry {
|
||||
prompt_id: metadata.id.user_id()?,
|
||||
title: metadata.title?,
|
||||
})
|
||||
match metadata.id {
|
||||
PromptId::EditWorkflow => None,
|
||||
PromptId::User { uuid } => Some(RulesContextEntry {
|
||||
prompt_id: uuid,
|
||||
title: metadata.title?,
|
||||
}),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::ModelId;
|
||||
use fs::Fs;
|
||||
use language_model::LanguageModel;
|
||||
use settings::{LanguageModelSelection, update_settings_file};
|
||||
use ui::App;
|
||||
|
||||
fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelSelection {
|
||||
LanguageModelSelection {
|
||||
provider: model.provider_id().to_string().into(),
|
||||
model: model.id().0.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn model_id_to_selection(model_id: &ModelId) -> LanguageModelSelection {
|
||||
let id = model_id.0.as_ref();
|
||||
let (provider, model) = id.split_once('/').unwrap_or(("", id));
|
||||
LanguageModelSelection {
|
||||
provider: provider.to_owned().into(),
|
||||
model: model.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_in_settings(
|
||||
model: Arc<dyn LanguageModel>,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let selection = language_model_to_selection(&model);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let agent = settings.agent.get_or_insert_default();
|
||||
if should_be_favorite {
|
||||
agent.add_favorite_model(selection.clone());
|
||||
} else {
|
||||
agent.remove_favorite_model(&selection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_model_id_in_settings(
|
||||
model_id: ModelId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let selection = model_id_to_selection(&model_id);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let agent = settings.agent.get_or_insert_default();
|
||||
if should_be_favorite {
|
||||
agent.add_favorite_model(selection.clone());
|
||||
} else {
|
||||
agent.remove_favorite_model(&selection);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1197,7 +1197,7 @@ impl InlineAssistant {
|
||||
|
||||
assist
|
||||
.editor
|
||||
.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
|
||||
.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -1209,7 +1209,7 @@ impl InlineAssistant {
|
||||
if let Some(decorations) = assist.decorations.as_ref() {
|
||||
decorations.prompt_editor.update(cx, |prompt_editor, cx| {
|
||||
prompt_editor.editor.update(cx, |editor, cx| {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.select_all(&SelectAll, window, cx);
|
||||
})
|
||||
});
|
||||
@@ -2271,36 +2271,6 @@ pub mod evals {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||
fn eval_empty_buffer() {
|
||||
run_eval(
|
||||
20,
|
||||
1.0,
|
||||
"Write a Python hello, world program".to_string(),
|
||||
"ˇ".to_string(),
|
||||
|output| match output {
|
||||
InlineAssistantOutput::Success {
|
||||
full_buffer_text, ..
|
||||
} => {
|
||||
if full_buffer_text.is_empty() {
|
||||
EvalOutput::failed("expected some output".to_string())
|
||||
} else {
|
||||
EvalOutput::passed(format!("Produced {full_buffer_text}"))
|
||||
}
|
||||
}
|
||||
o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn run_eval(
|
||||
iterations: usize,
|
||||
expected_pass_ratio: f32,
|
||||
|
||||
@@ -357,7 +357,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
|
||||
|
||||
if focus {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
}
|
||||
editor
|
||||
});
|
||||
@@ -844,59 +844,26 @@ impl<T: 'static> PromptEditor<T> {
|
||||
|
||||
if show_rating_buttons {
|
||||
buttons.push(
|
||||
h_flex()
|
||||
.pl_1()
|
||||
.gap_1()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
IconButton::new("thumbs-up", IconName::ThumbsUp)
|
||||
.shape(IconButtonShape::Square)
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Good Result",
|
||||
None,
|
||||
"You already rated this result",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Good Result"))
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_up(&ThumbsUpResult, window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("thumbs-down", IconName::ThumbsDown)
|
||||
.shape(IconButtonShape::Square)
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Bad Result",
|
||||
None,
|
||||
"You already rated this result",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Bad Result"))
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_down(&ThumbsDownResult, window, cx);
|
||||
})),
|
||||
)
|
||||
IconButton::new("thumbs-down", IconName::ThumbsDown)
|
||||
.icon_color(if rated { Color::Muted } else { Color::Default })
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(rated)
|
||||
.tooltip(Tooltip::text("Bad result"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_down(&ThumbsDownResult, window, cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
IconButton::new("thumbs-up", IconName::ThumbsUp)
|
||||
.icon_color(if rated { Color::Muted } else { Color::Default })
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(rated)
|
||||
.tooltip(Tooltip::text("Good result"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_up(&ThumbsUpResult, window, cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
@@ -960,21 +927,10 @@ impl<T: 'static> PromptEditor<T> {
|
||||
}
|
||||
|
||||
fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip({
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Close Assistant",
|
||||
&editor::actions::Cancel,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.tooltip(Tooltip::text("Close Assistant"))
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
use std::{cmp::Reverse, sync::Arc};
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use collections::IndexMap;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelRegistry,
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||
LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use ui::prelude::*;
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
@@ -20,14 +18,12 @@ use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem}
|
||||
|
||||
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
||||
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
|
||||
type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &App) + 'static>;
|
||||
|
||||
pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
|
||||
|
||||
pub fn language_model_selector(
|
||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -36,7 +32,6 @@ pub fn language_model_selector(
|
||||
let delegate = LanguageModelPickerDelegate::new(
|
||||
get_active_model,
|
||||
on_model_changed,
|
||||
on_toggle_favorite,
|
||||
popover_styles,
|
||||
focus_handle,
|
||||
window,
|
||||
@@ -54,17 +49,7 @@ pub fn language_model_selector(
|
||||
}
|
||||
|
||||
fn all_models(cx: &App) -> GroupedModels {
|
||||
let lm_registry = LanguageModelRegistry::global(cx).read(cx);
|
||||
let providers = lm_registry.providers();
|
||||
|
||||
let mut favorites_index = FavoritesIndex::default();
|
||||
|
||||
for sel in &AgentSettings::get_global(cx).favorite_models {
|
||||
favorites_index
|
||||
.entry(sel.provider.0.clone().into())
|
||||
.or_default()
|
||||
.insert(sel.model.clone().into());
|
||||
}
|
||||
let providers = LanguageModelRegistry::global(cx).read(cx).providers();
|
||||
|
||||
let recommended = providers
|
||||
.iter()
|
||||
@@ -72,7 +57,10 @@ fn all_models(cx: &App) -> GroupedModels {
|
||||
provider
|
||||
.recommended_models(cx)
|
||||
.into_iter()
|
||||
.map(|model| ModelInfo::new(&**provider, model, &favorites_index))
|
||||
.map(|model| ModelInfo {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -82,44 +70,25 @@ fn all_models(cx: &App) -> GroupedModels {
|
||||
provider
|
||||
.provided_models(cx)
|
||||
.into_iter()
|
||||
.map(|model| ModelInfo::new(&**provider, model, &favorites_index))
|
||||
.map(|model| ModelInfo {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
GroupedModels::new(all, recommended)
|
||||
}
|
||||
|
||||
type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ModelInfo {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
icon: IconName,
|
||||
is_favorite: bool,
|
||||
}
|
||||
|
||||
impl ModelInfo {
|
||||
fn new(
|
||||
provider: &dyn LanguageModelProvider,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
favorites_index: &FavoritesIndex,
|
||||
) -> Self {
|
||||
let is_favorite = favorites_index
|
||||
.get(&provider.id())
|
||||
.map_or(false, |set| set.contains(&model.id()));
|
||||
|
||||
Self {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
is_favorite,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LanguageModelPickerDelegate {
|
||||
on_model_changed: OnModelChanged,
|
||||
get_active_model: GetActiveModel,
|
||||
on_toggle_favorite: OnToggleFavorite,
|
||||
all_models: Arc<GroupedModels>,
|
||||
filtered_entries: Vec<LanguageModelPickerEntry>,
|
||||
selected_index: usize,
|
||||
@@ -133,7 +102,6 @@ impl LanguageModelPickerDelegate {
|
||||
fn new(
|
||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -149,7 +117,6 @@ impl LanguageModelPickerDelegate {
|
||||
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
|
||||
filtered_entries: entries,
|
||||
get_active_model: Arc::new(get_active_model),
|
||||
on_toggle_favorite: Arc::new(on_toggle_favorite),
|
||||
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
|
||||
_subscriptions: vec![cx.subscribe_in(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
@@ -249,57 +216,15 @@ impl LanguageModelPickerDelegate {
|
||||
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
(self.get_active_model)(cx)
|
||||
}
|
||||
|
||||
pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if self.all_models.favorites.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let active_model = (self.get_active_model)(cx);
|
||||
let active_provider_id = active_model.as_ref().map(|m| m.provider.id());
|
||||
let active_model_id = active_model.as_ref().map(|m| m.model.id());
|
||||
|
||||
let current_index = self
|
||||
.all_models
|
||||
.favorites
|
||||
.iter()
|
||||
.position(|info| {
|
||||
Some(info.model.provider_id()) == active_provider_id
|
||||
&& Some(info.model.id()) == active_model_id
|
||||
})
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
let next_index = if current_index == usize::MAX {
|
||||
0
|
||||
} else {
|
||||
(current_index + 1) % self.all_models.favorites.len()
|
||||
};
|
||||
|
||||
let next_model = self.all_models.favorites[next_index].model.clone();
|
||||
|
||||
(self.on_model_changed)(next_model, cx);
|
||||
|
||||
// Align the picker selection with the newly-active model
|
||||
let new_index =
|
||||
Self::get_active_model_index(&self.filtered_entries, (self.get_active_model)(cx));
|
||||
self.set_selected_index(new_index, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupedModels {
|
||||
favorites: Vec<ModelInfo>,
|
||||
recommended: Vec<ModelInfo>,
|
||||
all: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>,
|
||||
}
|
||||
|
||||
impl GroupedModels {
|
||||
pub fn new(all: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
|
||||
let favorites = all
|
||||
.iter()
|
||||
.filter(|info| info.is_favorite)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let mut all_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
|
||||
for model in all {
|
||||
let provider = model.model.provider_id();
|
||||
@@ -311,7 +236,6 @@ impl GroupedModels {
|
||||
}
|
||||
|
||||
Self {
|
||||
favorites,
|
||||
recommended,
|
||||
all: all_by_provider,
|
||||
}
|
||||
@@ -320,18 +244,13 @@ impl GroupedModels {
|
||||
fn entries(&self) -> Vec<LanguageModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
if !self.favorites.is_empty() {
|
||||
entries.push(LanguageModelPickerEntry::Separator("Favorite".into()));
|
||||
for info in &self.favorites {
|
||||
entries.push(LanguageModelPickerEntry::Model(info.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if !self.recommended.is_empty() {
|
||||
entries.push(LanguageModelPickerEntry::Separator("Recommended".into()));
|
||||
for info in &self.recommended {
|
||||
entries.push(LanguageModelPickerEntry::Model(info.clone()));
|
||||
}
|
||||
entries.extend(
|
||||
self.recommended
|
||||
.iter()
|
||||
.map(|info| LanguageModelPickerEntry::Model(info.clone())),
|
||||
);
|
||||
}
|
||||
|
||||
for models in self.all.values() {
|
||||
@@ -341,11 +260,12 @@ impl GroupedModels {
|
||||
entries.push(LanguageModelPickerEntry::Separator(
|
||||
models[0].model.provider_name().0,
|
||||
));
|
||||
for info in models {
|
||||
entries.push(LanguageModelPickerEntry::Model(info.clone()));
|
||||
}
|
||||
entries.extend(
|
||||
models
|
||||
.iter()
|
||||
.map(|info| LanguageModelPickerEntry::Model(info.clone())),
|
||||
);
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
}
|
||||
@@ -541,7 +461,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
is_focused: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
@@ -557,20 +477,11 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
|
||||
&& Some(model_info.model.id()) == active_model_id;
|
||||
|
||||
let is_favorite = model_info.is_favorite;
|
||||
let handle_action_click = {
|
||||
let model = model_info.model.clone();
|
||||
let on_toggle_favorite = self.on_toggle_favorite.clone();
|
||||
move |cx: &App| on_toggle_favorite(model.clone(), !is_favorite, cx)
|
||||
};
|
||||
|
||||
Some(
|
||||
ModelSelectorListItem::new(ix, model_info.model.name().0)
|
||||
.icon(model_info.icon)
|
||||
.is_focused(is_focused)
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.is_favorite(is_favorite)
|
||||
.on_toggle_favorite(handle_action_click)
|
||||
.icon(model_info.icon)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
@@ -582,12 +493,12 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<gpui::AnyElement> {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
if !self.popover_styles {
|
||||
return None;
|
||||
}
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
|
||||
}
|
||||
}
|
||||
@@ -687,24 +598,11 @@ mod tests {
|
||||
}
|
||||
|
||||
fn create_models(model_specs: Vec<(&str, &str)>) -> Vec<ModelInfo> {
|
||||
create_models_with_favorites(model_specs, vec![])
|
||||
}
|
||||
|
||||
fn create_models_with_favorites(
|
||||
model_specs: Vec<(&str, &str)>,
|
||||
favorites: Vec<(&str, &str)>,
|
||||
) -> Vec<ModelInfo> {
|
||||
model_specs
|
||||
.into_iter()
|
||||
.map(|(provider, name)| {
|
||||
let is_favorite = favorites
|
||||
.iter()
|
||||
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
|
||||
ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconName::Ai,
|
||||
is_favorite,
|
||||
}
|
||||
.map(|(provider, name)| ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconName::Ai,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -842,93 +740,4 @@ mod tests {
|
||||
vec!["zed/claude", "zed/gemini", "copilot/claude"],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models_with_favorites(
|
||||
vec![("zed", "claude"), ("zed", "gemini"), ("openai", "gpt-4")],
|
||||
vec![("zed", "gemini")],
|
||||
);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
let entries = grouped_models.entries();
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(LanguageModelPickerEntry::Separator(s)) if s == "Favorite"
|
||||
));
|
||||
|
||||
assert_models_eq(grouped_models.favorites, vec!["zed/gemini"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models(vec![("zed", "claude"), ("zed", "gemini")]);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
let entries = grouped_models.entries();
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(LanguageModelPickerEntry::Separator(s)) if s == "Recommended"
|
||||
));
|
||||
|
||||
assert!(grouped_models.favorites.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
|
||||
let recommended_models =
|
||||
create_models_with_favorites(vec![("zed", "claude")], vec![("zed", "claude")]);
|
||||
let all_models = create_models_with_favorites(
|
||||
vec![("zed", "claude"), ("zed", "gemini"), ("openai", "gpt-4")],
|
||||
vec![("zed", "claude")],
|
||||
);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
let entries = grouped_models.entries();
|
||||
|
||||
for entry in &entries {
|
||||
if let LanguageModelPickerEntry::Model(info) = entry {
|
||||
if info.model.telemetry_id() == "zed/claude" {
|
||||
assert!(info.is_favorite, "zed/claude should be a favorite");
|
||||
} else {
|
||||
assert!(
|
||||
!info.is_favorite,
|
||||
"{} should not be a favorite",
|
||||
info.model.telemetry_id()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_appear_in_other_sections(_cx: &mut TestAppContext) {
|
||||
let favorites = vec![("zed", "gemini"), ("openai", "gpt-4")];
|
||||
|
||||
let recommended_models =
|
||||
create_models_with_favorites(vec![("zed", "claude")], favorites.clone());
|
||||
|
||||
let all_models = create_models_with_favorites(
|
||||
vec![
|
||||
("zed", "claude"),
|
||||
("zed", "gemini"),
|
||||
("openai", "gpt-4"),
|
||||
("openai", "gpt-3.5"),
|
||||
],
|
||||
favorites,
|
||||
);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
|
||||
assert_models_eq(grouped_models.favorites, vec!["zed/gemini", "openai/gpt-4"]);
|
||||
assert_models_eq(grouped_models.recommended, vec!["zed/claude"]);
|
||||
assert_models_eq(
|
||||
grouped_models.all.values().flatten().cloned().collect(),
|
||||
vec!["zed/claude", "zed/gemini", "openai/gpt-4", "openai/gpt-3.5"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ impl TerminalInlineAssistant {
|
||||
if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
|
||||
prompt_editor.update(cx, |this, cx| {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.select_all(&SelectAll, window, cx);
|
||||
});
|
||||
});
|
||||
@@ -292,7 +292,7 @@ impl TerminalInlineAssistant {
|
||||
.terminal
|
||||
.update(cx, |this, cx| {
|
||||
this.clear_block_below_cursor(cx);
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
@@ -369,7 +369,7 @@ impl TerminalInlineAssistant {
|
||||
.terminal
|
||||
.update(cx, |this, cx| {
|
||||
this.clear_block_below_cursor(cx);
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
ui::BurnModeTooltip,
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use agent_settings::CompletionMode;
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
|
||||
use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
|
||||
@@ -71,9 +71,7 @@ use workspace::{
|
||||
pane,
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
};
|
||||
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
|
||||
|
||||
use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
|
||||
use assistant_text_thread::{
|
||||
@@ -306,31 +304,17 @@ impl TextThreadEditor {
|
||||
language_model_selector: cx.new(|cx| {
|
||||
language_model_selector(
|
||||
|cx| LanguageModelRegistry::read_global(cx).default_model(),
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, cx| {
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model = model.id().0.to_string();
|
||||
settings.agent.get_or_insert_default().set_model(
|
||||
LanguageModelSelection {
|
||||
provider: LanguageModelProviderSetting(provider),
|
||||
model,
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, should_be_favorite, cx| {
|
||||
crate::favorite_models::toggle_in_settings(
|
||||
model,
|
||||
should_be_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
move |model, cx| {
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model = model.id().0.to_string();
|
||||
settings.agent.get_or_insert_default().set_model(
|
||||
LanguageModelSelection {
|
||||
provider: LanguageModelProviderSetting(provider),
|
||||
model,
|
||||
},
|
||||
)
|
||||
});
|
||||
},
|
||||
true, // Use popover styles for picker
|
||||
focus_handle,
|
||||
@@ -1341,7 +1325,7 @@ impl TextThreadEditor {
|
||||
if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
|
||||
active_editor_view.update(cx, |editor, cx| {
|
||||
editor.insert(&text, window, cx);
|
||||
editor.focus_handle(cx).focus(window, cx);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1698,9 +1682,6 @@ impl TextThreadEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
@@ -1711,101 +1692,84 @@ impl TextThreadEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
// 2. Have an associated file path
|
||||
// 3. Span multiple lines (not single-line selections)
|
||||
// 4. Belong to a file that exists in the current project
|
||||
let should_insert_creases = util::maybe!({
|
||||
let selections = editor_clipboard_selections.as_ref()?;
|
||||
if selections.len() > 1 {
|
||||
return Some(false);
|
||||
}
|
||||
let selection = selections.first()?;
|
||||
let file_path = selection.file_path.as_ref()?;
|
||||
let line_range = selection.line_range.as_ref()?;
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
|
||||
if line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
if has_file_context {
|
||||
if let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) =
|
||||
clipboard_item.entries().first()
|
||||
{
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
|
||||
if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let selected_text =
|
||||
&text[current_offset..current_offset + selection.len];
|
||||
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||
file_path.to_str(),
|
||||
Some(line_range.clone()),
|
||||
);
|
||||
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let selected_text =
|
||||
&text[current_offset..current_offset + selection.len];
|
||||
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||
file_path.to_str(),
|
||||
Some(line_range.clone()),
|
||||
);
|
||||
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_before = snapshot.anchor_after(insert_point);
|
||||
let anchor_after = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.bias_left(&snapshot);
|
||||
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
editor.insert("\n", window, cx);
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_before = snapshot.anchor_after(insert_point);
|
||||
let anchor_after = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.bias_left(&snapshot);
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
|
||||
editor.insert("\n", window, cx);
|
||||
let fold_placeholder = quote_selection_fold_placeholder(
|
||||
crease_text,
|
||||
weak_editor.clone(),
|
||||
);
|
||||
let crease = Crease::inline(
|
||||
anchor_before..anchor_after,
|
||||
fold_placeholder,
|
||||
render_quote_selection_output_toggle,
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
|
||||
let fold_placeholder = quote_selection_fold_placeholder(
|
||||
crease_text,
|
||||
weak_editor.clone(),
|
||||
);
|
||||
let crease = Crease::inline(
|
||||
anchor_before..anchor_after,
|
||||
fold_placeholder,
|
||||
render_quote_selection_output_toggle,
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1964,12 +1928,6 @@ impl TextThreadEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.paste(&editor::actions::Paste, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
@@ -2237,53 +2195,12 @@ impl TextThreadEditor {
|
||||
};
|
||||
|
||||
let focus_handle = self.editor().focus_handle(cx);
|
||||
|
||||
let (color, icon) = if self.language_model_selector_menu_handle.is_deployed() {
|
||||
(Color::Accent, IconName::ChevronUp)
|
||||
} else {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
let should_show_cycle_row = !AgentSettings::get_global(cx)
|
||||
.favorite_model_ids()
|
||||
.is_empty();
|
||||
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Change Model"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(should_show_cycle_row, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.pt_1()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_between()
|
||||
.child(Label::new("Cycle Favorited Models"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&CycleFavoriteModels,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
});
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
@@ -2300,7 +2217,9 @@ impl TextThreadEditor {
|
||||
)
|
||||
.child(Icon::new(icon).color(color).size(IconSize::XSmall)),
|
||||
),
|
||||
tooltip,
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
},
|
||||
gpui::Corner::BottomRight,
|
||||
cx,
|
||||
)
|
||||
@@ -2653,7 +2572,6 @@ impl Render for TextThreadEditor {
|
||||
.capture_action(cx.listener(TextThreadEditor::copy))
|
||||
.capture_action(cx.listener(TextThreadEditor::cut))
|
||||
.capture_action(cx.listener(TextThreadEditor::paste))
|
||||
.on_action(cx.listener(TextThreadEditor::paste_raw))
|
||||
.capture_action(cx.listener(TextThreadEditor::cycle_message_role))
|
||||
.capture_action(cx.listener(TextThreadEditor::confirm_command))
|
||||
.on_action(cx.listener(TextThreadEditor::assist))
|
||||
@@ -2661,11 +2579,6 @@ impl Render for TextThreadEditor {
|
||||
.on_action(move |_: &ToggleModelSelector, window, cx| {
|
||||
language_model_selector.toggle(window, cx);
|
||||
})
|
||||
.on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
|
||||
this.language_model_selector.update(cx, |selector, cx| {
|
||||
selector.delegate.cycle_favorite_models(window, cx);
|
||||
});
|
||||
}))
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -222,8 +222,8 @@ impl Render for AcpOnboardingModal {
|
||||
acp_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(illustration)
|
||||
.child(
|
||||
|
||||
@@ -230,8 +230,8 @@ impl Render for ClaudeCodeOnboardingModal {
|
||||
claude_code_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(illustration)
|
||||
.child(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use gpui::{Action, FocusHandle, prelude::*};
|
||||
use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelectorHeader {
|
||||
@@ -42,8 +42,6 @@ pub struct ModelSelectorListItem {
|
||||
icon: Option<IconName>,
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
is_favorite: bool,
|
||||
on_toggle_favorite: Option<Box<dyn Fn(&App) + 'static>>,
|
||||
}
|
||||
|
||||
impl ModelSelectorListItem {
|
||||
@@ -54,8 +52,6 @@ impl ModelSelectorListItem {
|
||||
icon: None,
|
||||
is_selected: false,
|
||||
is_focused: false,
|
||||
is_favorite: false,
|
||||
on_toggle_favorite: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,16 +69,6 @@ impl ModelSelectorListItem {
|
||||
self.is_focused = is_focused;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_favorite(mut self, is_favorite: bool) -> Self {
|
||||
self.is_favorite = is_favorite;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle_favorite(mut self, handler: impl Fn(&App) + 'static) -> Self {
|
||||
self.on_toggle_favorite = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ModelSelectorListItem {
|
||||
@@ -93,8 +79,6 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let is_favorite = self.is_favorite;
|
||||
|
||||
ListItem::new(self.index)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
@@ -113,24 +97,11 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
.child(Label::new(self.title).truncate()),
|
||||
)
|
||||
.end_slot(div().pr_2().when(self.is_selected, |this| {
|
||||
this.child(Icon::new(IconName::Check).color(Color::Accent))
|
||||
}))
|
||||
.end_hover_slot(div().pr_1p5().when_some(self.on_toggle_favorite, {
|
||||
|this, handle_click| {
|
||||
let (icon, color, tooltip) = if is_favorite {
|
||||
(IconName::StarFilled, Color::Accent, "Unfavorite Model")
|
||||
} else {
|
||||
(IconName::Star, Color::Default, "Favorite Model")
|
||||
};
|
||||
this.child(
|
||||
IconButton::new(("toggle-favorite", self.index), icon)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.icon_color(color)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.on_click(move |_, _, cx| (handle_click)(cx)),
|
||||
)
|
||||
}
|
||||
this.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ impl Render for AgentOnboardingModal {
|
||||
agent_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -12,10 +12,6 @@ workspace = true
|
||||
path = "src/agent_ui_v2.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["agent/test-support"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
agent_servers.workspace = true
|
||||
@@ -42,6 +38,3 @@ time_format.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
agent = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1 +1 @@
|
||||
../../LICENSE-GPL
|
||||
LICENSE-GPL
|
||||
@@ -1052,71 +1052,6 @@ pub fn parse_prompt_too_long(message: &str) -> Option<u64> {
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Request body for the token counting API.
|
||||
/// Similar to `Request` but without `max_tokens` since it's not needed for counting.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CountTokensRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<Message>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub system: Option<StringOrContents>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tools: Vec<Tool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub thinking: Option<Thinking>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<ToolChoice>,
|
||||
}
|
||||
|
||||
/// Response from the token counting API.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CountTokensResponse {
|
||||
pub input_tokens: u64,
|
||||
}
|
||||
|
||||
/// Count the number of tokens in a message without creating it.
|
||||
pub async fn count_tokens(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: CountTokensRequest,
|
||||
) -> Result<CountTokensResponse, AnthropicError> {
|
||||
let uri = format!("{api_url}/v1/messages/count_tokens");
|
||||
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("X-Api-Key", api_key.trim())
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
let serialized_request =
|
||||
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
||||
let http_request = request_builder
|
||||
.body(AsyncBody::from(serialized_request))
|
||||
.map_err(AnthropicError::BuildRequestBody)?;
|
||||
|
||||
let mut response = client
|
||||
.send(http_request)
|
||||
.await
|
||||
.map_err(AnthropicError::HttpSend)?;
|
||||
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
|
||||
if response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.map_err(AnthropicError::ReadResponse)?;
|
||||
|
||||
serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)
|
||||
} else {
|
||||
Err(handle_error_response(response, rate_limits).await)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_window_exceeded() {
|
||||
let error = ApiError {
|
||||
|
||||
@@ -87,7 +87,7 @@ pub async fn stream_completion(
|
||||
Ok(None) => None,
|
||||
Err(err) => Some((
|
||||
Err(BedrockError::ClientError(anyhow!(
|
||||
"{}",
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
))),
|
||||
stream,
|
||||
|
||||
@@ -616,11 +616,13 @@ impl BufferDiffInner {
|
||||
secondary: Option<&'a Self>,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let range = range.to_offset(buffer);
|
||||
println!(" >>> range = {range:?}");
|
||||
|
||||
let mut cursor = self
|
||||
.hunks
|
||||
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
||||
let summary_range = summary.buffer_range.to_offset(buffer);
|
||||
println!(" >>> summary_range = {:?}", summary_range);
|
||||
let before_start = summary_range.end < range.start;
|
||||
let after_end = summary_range.start > range.end;
|
||||
!before_start && !after_end
|
||||
@@ -2155,7 +2157,7 @@ mod tests {
|
||||
let range = diff_1.inner.compare(&empty_diff.inner, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
|
||||
|
||||
// Edit does affects the diff because it recalculates word diffs.
|
||||
// Edit does not affect the diff.
|
||||
buffer.edit_via_marked_text(
|
||||
&"
|
||||
one
|
||||
@@ -2170,14 +2172,7 @@ mod tests {
|
||||
.unindent(),
|
||||
);
|
||||
let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
|
||||
assert_eq!(
|
||||
Point::new(4, 0)..Point::new(5, 0),
|
||||
diff_2
|
||||
.inner
|
||||
.compare(&diff_1.inner, &buffer)
|
||||
.unwrap()
|
||||
.to_point(&buffer)
|
||||
);
|
||||
assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer));
|
||||
|
||||
// Edit turns a deletion hunk into a modification.
|
||||
buffer.edit_via_marked_text(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
|
||||
use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
|
||||
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{App, Context, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
@@ -300,6 +300,16 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate {
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_cursor_position: Anchor,
|
||||
_direction: Direction,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
// Codestral doesn't support multiple completions, so cycling does nothing
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut Context<Self>) {
|
||||
log::debug!("Codestral: Completion accepted");
|
||||
self.pending_request = None;
|
||||
|
||||
@@ -4,7 +4,6 @@ use collections::{HashMap, HashSet};
|
||||
|
||||
use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
|
||||
use debugger_ui::debugger_panel::DebugPanel;
|
||||
use editor::{Editor, EditorMode, MultiBuffer};
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
use futures::StreamExt as _;
|
||||
@@ -13,30 +12,22 @@ use http_client::BlockedHttpClient;
|
||||
use language::{
|
||||
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
|
||||
language_settings::{Formatter, FormatterList, language_settings},
|
||||
rust_lang, tree_sitter_typescript,
|
||||
tree_sitter_typescript,
|
||||
};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::{
|
||||
ProjectPath,
|
||||
debugger::session::ThreadId,
|
||||
lsp_store::{FormatTrigger, LspFormatTarget},
|
||||
trusted_worktrees::{PathTrust, TrustedWorktrees},
|
||||
};
|
||||
use remote::RemoteClient;
|
||||
use remote_server::{HeadlessAppState, HeadlessProject};
|
||||
use rpc::proto;
|
||||
use serde_json::json;
|
||||
use settings::{
|
||||
InlayHintSettingsContent, LanguageServerFormatterSpecifier, PrettierSettingsContent,
|
||||
SettingsStore,
|
||||
};
|
||||
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::{path, rel_path::rel_path};
|
||||
@@ -99,14 +90,13 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/code/project1"), client_ssh, false, cx_a)
|
||||
.build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
@@ -260,14 +250,13 @@ async fn test_ssh_collaboration_git_branches(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project("/project", client_ssh, false, cx_a)
|
||||
.build_ssh_project("/project", client_ssh, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
@@ -465,14 +454,13 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/project"), client_ssh, false, cx_a)
|
||||
.build_ssh_project(path!("/project"), client_ssh, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
@@ -627,7 +615,6 @@ async fn test_remote_server_debugger(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -640,7 +627,7 @@ async fn test_remote_server_debugger(
|
||||
command_palette_hooks::init(cx);
|
||||
});
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
|
||||
.await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
@@ -736,7 +723,6 @@ async fn test_slow_adapter_startup_retries(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -749,7 +735,7 @@ async fn test_slow_adapter_startup_retries(
|
||||
command_palette_hooks::init(cx);
|
||||
});
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
|
||||
.await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
@@ -852,261 +838,3 @@ async fn test_slow_adapter_startup_retries(
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||
use project::trusted_worktrees::RemoteHostLocation;
|
||||
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
project::trusted_worktrees::init(HashMap::default(), None, None, cx);
|
||||
});
|
||||
server_cx.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
project::trusted_worktrees::init(HashMap::default(), None, None, cx);
|
||||
});
|
||||
|
||||
let mut server = TestServer::start(cx_a.executor().clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
|
||||
let server_name = "override-rust-analyzer";
|
||||
let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
path!("/projects"),
|
||||
json!({
|
||||
"project_a": {
|
||||
".zed": {
|
||||
"settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
|
||||
},
|
||||
"main.rs": "fn main() {}"
|
||||
},
|
||||
"project_b": { "lib.rs": "pub fn lib() {}" }
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
server_cx.update(HeadlessProject::init);
|
||||
let remote_http_client = Arc::new(BlockedHttpClient);
|
||||
let node = NodeRuntime::unavailable();
|
||||
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
||||
languages.add(rust_lang());
|
||||
|
||||
let capabilities = lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
let mut fake_language_servers = languages.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: server_name,
|
||||
capabilities: capabilities.clone(),
|
||||
initializer: Some(Box::new({
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
move |fake_server| {
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
|
||||
move |_params, _| {
|
||||
lsp_inlay_hint_request_count.fetch_add(1, Ordering::Release);
|
||||
async move {
|
||||
Ok(Some(vec![lsp::InlayHint {
|
||||
position: lsp::Position::new(0, 0),
|
||||
label: lsp::InlayHintLabel::String("hint".to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
}]))
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
let _headless_project = server_cx.new(|cx| {
|
||||
HeadlessProject::new(
|
||||
HeadlessAppState {
|
||||
session: server_ssh,
|
||||
fs: remote_fs.clone(),
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id_a) = client_a
|
||||
.build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a)
|
||||
.await;
|
||||
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
let language_settings = &mut settings.project.all_languages.defaults;
|
||||
language_settings.inlay_hints = Some(InlayHintSettingsContent {
|
||||
enabled: Some(true),
|
||||
..InlayHintSettingsContent::default()
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.languages().add(rust_lang());
|
||||
project.languages().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: server_name,
|
||||
capabilities,
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
project.find_or_create_worktree(path!("/projects/project_b"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_a.run_until_parked();
|
||||
|
||||
let worktree_ids = project_a.read_with(cx_a, |project, cx| {
|
||||
project
|
||||
.worktrees(cx)
|
||||
.map(|wt| wt.read(cx).id())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(worktree_ids.len(), 2);
|
||||
|
||||
let remote_host = project_a.read_with(cx_a, |project, cx| {
|
||||
project
|
||||
.remote_connection_options(cx)
|
||||
.map(RemoteHostLocation::from)
|
||||
});
|
||||
|
||||
let trusted_worktrees =
|
||||
cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(!can_trust_a, "project_a should be restricted initially");
|
||||
assert!(!can_trust_b, "project_b should be restricted initially");
|
||||
|
||||
let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
|
||||
let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(has_restricted, "should have restricted worktrees");
|
||||
|
||||
let buffer_before_approval = project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.open_buffer((worktree_id_a, rel_path("main.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (editor, cx_a) = cx_a.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::full(),
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
|
||||
Some(project_a.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
let fake_language_server = fake_language_servers.next();
|
||||
|
||||
cx_a.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||
["...".to_string()],
|
||||
"remote .zed/settings.json must not sync before trust approval"
|
||||
)
|
||||
});
|
||||
|
||||
editor.update_in(cx_a, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
cx_a.executor().advance_clock(Duration::from_secs(1));
|
||||
assert_eq!(
|
||||
lsp_inlay_hint_request_count.load(Ordering::Acquire),
|
||||
0,
|
||||
"inlay hints must not be queried before trust approval"
|
||||
);
|
||||
|
||||
trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.trust(
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
|
||||
remote_host.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
|
||||
cx_a.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||
["override-rust-analyzer".to_string()],
|
||||
"remote .zed/settings.json should sync after trust approval"
|
||||
)
|
||||
});
|
||||
let _fake_language_server = fake_language_server.await.unwrap();
|
||||
editor.update_in(cx_a, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
cx_a.executor().advance_clock(Duration::from_secs(1));
|
||||
assert!(
|
||||
lsp_inlay_hint_request_count.load(Ordering::Acquire) > 0,
|
||||
"inlay hints should be queried after trust approval"
|
||||
);
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(can_trust_a, "project_a should be trusted after trust()");
|
||||
assert!(!can_trust_b, "project_b should still be restricted");
|
||||
|
||||
trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.trust(
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
|
||||
remote_host.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(can_trust_a, "project_a should remain trusted");
|
||||
assert!(can_trust_b, "project_b should now be trusted");
|
||||
|
||||
let has_restricted_after = trusted_worktrees.read_with(cx_a, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(
|
||||
!has_restricted_after,
|
||||
"should have no restricted worktrees after trusting both"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -761,7 +761,6 @@ impl TestClient {
|
||||
&self,
|
||||
root_path: impl AsRef<Path>,
|
||||
ssh: Entity<RemoteClient>,
|
||||
init_worktree_trust: bool,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Project>, WorktreeId) {
|
||||
let project = cx.update(|cx| {
|
||||
@@ -772,7 +771,6 @@ impl TestClient {
|
||||
self.app_state.user_store.clone(),
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
init_worktree_trust,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -841,7 +839,6 @@ impl TestClient {
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1252,7 +1252,7 @@ impl CollabPanel {
|
||||
context_menu
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
@@ -1424,7 +1424,7 @@ impl CollabPanel {
|
||||
context_menu
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
@@ -1487,7 +1487,7 @@ impl CollabPanel {
|
||||
})
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
@@ -1521,9 +1521,9 @@ impl CollabPanel {
|
||||
if cx.stop_active_drag(window) {
|
||||
return;
|
||||
} else if self.take_editing_state(window, cx) {
|
||||
window.focus(&self.filter_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.filter_editor.focus_handle(cx));
|
||||
} else if !self.reset_filter_editor_text(window, cx) {
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
}
|
||||
|
||||
if self.context_menu.is_some() {
|
||||
@@ -1826,7 +1826,7 @@ impl CollabPanel {
|
||||
});
|
||||
self.update_entries(false, cx);
|
||||
self.select_channel_editor();
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1851,7 +1851,7 @@ impl CollabPanel {
|
||||
});
|
||||
self.update_entries(false, cx);
|
||||
self.select_channel_editor();
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1900,7 +1900,7 @@ impl CollabPanel {
|
||||
editor.set_text(channel.name.clone(), window, cx);
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
});
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx));
|
||||
self.update_entries(false, cx);
|
||||
self.select_channel_editor();
|
||||
}
|
||||
|
||||
@@ -642,7 +642,7 @@ impl ChannelModalDelegate {
|
||||
});
|
||||
menu
|
||||
});
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
|
||||
@@ -588,7 +588,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let action = command.action;
|
||||
window.focus(&self.previous_focus_handle, cx);
|
||||
window.focus(&self.previous_focus_handle);
|
||||
self.dismissed(window, cx);
|
||||
window.dispatch_action(action, cx);
|
||||
}
|
||||
@@ -784,7 +784,7 @@ mod tests {
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
@@ -855,7 +855,7 @@ mod tests {
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
|
||||
});
|
||||
|
||||
// Test normalize (trimming whitespace and double colons)
|
||||
|
||||
@@ -29,7 +29,6 @@ schemars.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
slotmap.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
|
||||
@@ -6,7 +6,6 @@ use parking_lot::Mutex;
|
||||
use postage::barrier;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use serde_json::{Value, value::RawValue};
|
||||
use slotmap::SlotMap;
|
||||
use smol::channel;
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -51,7 +50,7 @@ pub(crate) struct Client {
|
||||
next_id: AtomicI32,
|
||||
outbound_tx: channel::Sender<String>,
|
||||
name: Arc<str>,
|
||||
subscription_set: Arc<Mutex<NotificationSubscriptionSet>>,
|
||||
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
||||
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[allow(dead_code)]
|
||||
@@ -192,20 +191,21 @@ impl Client {
|
||||
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
|
||||
let (output_done_tx, output_done_rx) = barrier::channel();
|
||||
|
||||
let subscription_set = Arc::new(Mutex::new(NotificationSubscriptionSet::default()));
|
||||
let notification_handlers =
|
||||
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
|
||||
let response_handlers =
|
||||
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
|
||||
let request_handlers = Arc::new(Mutex::new(HashMap::<_, RequestHandler>::default()));
|
||||
|
||||
let receive_input_task = cx.spawn({
|
||||
let subscription_set = subscription_set.clone();
|
||||
let notification_handlers = notification_handlers.clone();
|
||||
let response_handlers = response_handlers.clone();
|
||||
let request_handlers = request_handlers.clone();
|
||||
let transport = transport.clone();
|
||||
async move |cx| {
|
||||
Self::handle_input(
|
||||
transport,
|
||||
subscription_set,
|
||||
notification_handlers,
|
||||
request_handlers,
|
||||
response_handlers,
|
||||
cx,
|
||||
@@ -236,7 +236,7 @@ impl Client {
|
||||
|
||||
Ok(Self {
|
||||
server_id,
|
||||
subscription_set,
|
||||
notification_handlers,
|
||||
response_handlers,
|
||||
name: server_name,
|
||||
next_id: Default::default(),
|
||||
@@ -257,7 +257,7 @@ impl Client {
|
||||
/// to pending requests) and notifications (which trigger registered handlers).
|
||||
async fn handle_input(
|
||||
transport: Arc<dyn Transport>,
|
||||
subscription_set: Arc<Mutex<NotificationSubscriptionSet>>,
|
||||
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
||||
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
|
||||
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -282,11 +282,10 @@ impl Client {
|
||||
handler(Ok(message.to_string()));
|
||||
}
|
||||
} else if let Ok(notification) = serde_json::from_str::<AnyNotification>(&message) {
|
||||
subscription_set.lock().notify(
|
||||
¬ification.method,
|
||||
notification.params.unwrap_or(Value::Null),
|
||||
cx,
|
||||
)
|
||||
let mut notification_handlers = notification_handlers.lock();
|
||||
if let Some(handler) = notification_handlers.get_mut(notification.method.as_str()) {
|
||||
handler(notification.params.unwrap_or(Value::Null), cx.clone());
|
||||
}
|
||||
} else {
|
||||
log::error!("Unhandled JSON from context_server: {}", message);
|
||||
}
|
||||
@@ -452,18 +451,12 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn on_notification(
|
||||
&self,
|
||||
method: &'static str,
|
||||
f: Box<dyn 'static + Send + FnMut(Value, AsyncApp)>,
|
||||
) -> NotificationSubscription {
|
||||
let mut notification_subscriptions = self.subscription_set.lock();
|
||||
|
||||
NotificationSubscription {
|
||||
id: notification_subscriptions.add_handler(method, f),
|
||||
set: self.subscription_set.clone(),
|
||||
}
|
||||
) {
|
||||
self.notification_handlers.lock().insert(method, f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,73 +485,3 @@ impl fmt::Debug for Client {
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
slotmap::new_key_type! {
|
||||
struct NotificationSubscriptionId;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NotificationSubscriptionSet {
|
||||
// we have very few subscriptions at the moment
|
||||
methods: Vec<(&'static str, Vec<NotificationSubscriptionId>)>,
|
||||
handlers: SlotMap<NotificationSubscriptionId, NotificationHandler>,
|
||||
}
|
||||
|
||||
impl NotificationSubscriptionSet {
|
||||
#[must_use]
|
||||
fn add_handler(
|
||||
&mut self,
|
||||
method: &'static str,
|
||||
handler: NotificationHandler,
|
||||
) -> NotificationSubscriptionId {
|
||||
let id = self.handlers.insert(handler);
|
||||
if let Some((_, handler_ids)) = self
|
||||
.methods
|
||||
.iter_mut()
|
||||
.find(|(probe_method, _)| method == *probe_method)
|
||||
{
|
||||
debug_assert!(
|
||||
handler_ids.len() < 20,
|
||||
"Too many MCP handlers for {}. Consider using a different data structure.",
|
||||
method
|
||||
);
|
||||
|
||||
handler_ids.push(id);
|
||||
} else {
|
||||
self.methods.push((method, vec![id]));
|
||||
};
|
||||
id
|
||||
}
|
||||
|
||||
fn notify(&mut self, method: &str, payload: Value, cx: &mut AsyncApp) {
|
||||
let Some((_, handler_ids)) = self
|
||||
.methods
|
||||
.iter_mut()
|
||||
.find(|(probe_method, _)| method == *probe_method)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
for handler_id in handler_ids {
|
||||
if let Some(handler) = self.handlers.get_mut(*handler_id) {
|
||||
handler(payload.clone(), cx.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NotificationSubscription {
|
||||
id: NotificationSubscriptionId,
|
||||
set: Arc<Mutex<NotificationSubscriptionSet>>,
|
||||
}
|
||||
|
||||
impl Drop for NotificationSubscription {
|
||||
fn drop(&mut self) {
|
||||
let mut set = self.set.lock();
|
||||
set.handlers.remove(self.id);
|
||||
set.methods.retain_mut(|(_, handler_ids)| {
|
||||
handler_ids.retain(|id| *id != self.id);
|
||||
!handler_ids.is_empty()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,22 @@ impl ContextServer {
|
||||
self.initialize(self.new_client(cx)?).await
|
||||
}
|
||||
|
||||
/// Starts the context server, making sure handlers are registered before initialization happens
|
||||
pub async fn start_with_handlers(
|
||||
&self,
|
||||
notification_handlers: Vec<(
|
||||
&'static str,
|
||||
Box<dyn 'static + Send + FnMut(serde_json::Value, AsyncApp)>,
|
||||
)>,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<()> {
|
||||
let client = self.new_client(cx)?;
|
||||
for (method, handler) in notification_handlers {
|
||||
client.on_notification(method, handler);
|
||||
}
|
||||
self.initialize(client).await
|
||||
}
|
||||
|
||||
fn new_client(&self, cx: &AsyncApp) -> Result<Client> {
|
||||
Ok(match &self.configuration {
|
||||
ContextServerTransport::Stdio(command, working_directory) => Client::stdio(
|
||||
|
||||
@@ -12,7 +12,7 @@ use futures::channel::oneshot;
|
||||
use gpui::AsyncApp;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::client::{Client, NotificationSubscription};
|
||||
use crate::client::Client;
|
||||
use crate::types::{self, Notification, Request};
|
||||
|
||||
pub struct ModelContextProtocol {
|
||||
@@ -119,7 +119,7 @@ impl InitializedContextServerProtocol {
|
||||
&self,
|
||||
method: &'static str,
|
||||
f: Box<dyn 'static + Send + FnMut(Value, AsyncApp)>,
|
||||
) -> NotificationSubscription {
|
||||
self.inner.on_notification(method, f)
|
||||
) {
|
||||
self.inner.on_notification(method, f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ pub struct PromptMessage {
|
||||
pub content: MessageContent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
User,
|
||||
|
||||
@@ -4,7 +4,6 @@ pub mod copilot_responses;
|
||||
pub mod request;
|
||||
mod sign_in;
|
||||
|
||||
use crate::request::NextEditSuggestions;
|
||||
use crate::sign_in::initiate_sign_out;
|
||||
use ::fs::Fs;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -19,7 +18,7 @@ use http_client::HttpClient;
|
||||
use language::language_settings::CopilotSettings;
|
||||
use language::{
|
||||
Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,
|
||||
language_settings::{EditPredictionProvider, all_language_settings},
|
||||
language_settings::{EditPredictionProvider, all_language_settings, language_settings},
|
||||
point_from_lsp, point_to_lsp,
|
||||
};
|
||||
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
|
||||
@@ -41,7 +40,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::Dimensions;
|
||||
use util::{ResultExt, fs::remove_matching};
|
||||
use util::{ResultExt, fs::remove_matching, rel_path::RelPath};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;
|
||||
@@ -316,15 +315,6 @@ struct GlobalCopilot(Entity<Copilot>);
|
||||
|
||||
impl Global for GlobalCopilot {}
|
||||
|
||||
/// Copilot's NextEditSuggestion response, with coordinates converted to Anchors.
|
||||
struct CopilotEditPrediction {
|
||||
buffer: Entity<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
text: String,
|
||||
command: Option<lsp::Command>,
|
||||
snapshot: BufferSnapshot,
|
||||
}
|
||||
|
||||
impl Copilot {
|
||||
pub fn global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<GlobalCopilot>()
|
||||
@@ -883,19 +873,101 @@ impl Copilot {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn completions(
|
||||
pub fn completions<T>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: Anchor,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<CopilotEditPrediction>>> {
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
|
||||
}
|
||||
|
||||
pub fn completions_cycling<T>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
|
||||
}
|
||||
|
||||
pub fn accept_completion(
|
||||
&mut self,
|
||||
completion: &Completion,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let request =
|
||||
server
|
||||
.lsp
|
||||
.request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
|
||||
uuid: completion.uuid.clone(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify accepted")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn discard_completions(
|
||||
&mut self,
|
||||
completions: &[Completion],
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(_) => return Task::ready(Ok(())),
|
||||
};
|
||||
let request =
|
||||
server
|
||||
.lsp
|
||||
.request::<request::NotifyRejected>(request::NotifyRejectedParams {
|
||||
uuids: completions
|
||||
.iter()
|
||||
.map(|completion| completion.uuid.clone())
|
||||
.collect(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify rejected")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn request_completions<R, T>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
R: 'static
|
||||
+ lsp::request::Request<
|
||||
Params = request::GetCompletionsParams,
|
||||
Result = request::GetCompletionsResult,
|
||||
>,
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
self.register_buffer(buffer, cx);
|
||||
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let buffer_entity = buffer.clone();
|
||||
let lsp = server.lsp.clone();
|
||||
let registered_buffer = server
|
||||
.registered_buffers
|
||||
@@ -905,31 +977,46 @@ impl Copilot {
|
||||
let buffer = buffer.read(cx);
|
||||
let uri = registered_buffer.uri.clone();
|
||||
let position = position.to_point_utf16(buffer);
|
||||
let settings = language_settings(
|
||||
buffer.language_at(position).map(|l| l.name()),
|
||||
buffer.file(),
|
||||
cx,
|
||||
);
|
||||
let tab_size = settings.tab_size;
|
||||
let hard_tabs = settings.hard_tabs;
|
||||
let relative_path = buffer
|
||||
.file()
|
||||
.map_or(RelPath::empty().into(), |file| file.path().clone());
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let (version, snapshot) = snapshot.await?;
|
||||
let result = lsp
|
||||
.request::<NextEditSuggestions>(request::NextEditSuggestionsParams {
|
||||
text_document: lsp::VersionedTextDocumentIdentifier { uri, version },
|
||||
position: point_to_lsp(position),
|
||||
.request::<R>(request::GetCompletionsParams {
|
||||
doc: request::GetCompletionsDocument {
|
||||
uri,
|
||||
tab_size: tab_size.into(),
|
||||
indent_size: 1,
|
||||
insert_spaces: !hard_tabs,
|
||||
relative_path: relative_path.to_proto(),
|
||||
position: point_to_lsp(position),
|
||||
version: version.try_into().unwrap(),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: get completions")?;
|
||||
let completions = result
|
||||
.edits
|
||||
.completions
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
let start = snapshot
|
||||
.clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
|
||||
let end =
|
||||
snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
|
||||
CopilotEditPrediction {
|
||||
buffer: buffer_entity.clone(),
|
||||
Completion {
|
||||
uuid: completion.uuid,
|
||||
range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
text: completion.text,
|
||||
command: completion.command,
|
||||
snapshot: snapshot.clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -937,35 +1024,6 @@ impl Copilot {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn accept_completion(
|
||||
&mut self,
|
||||
completion: &CopilotEditPrediction,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
if let Some(command) = &completion.command {
|
||||
let request = server
|
||||
.lsp
|
||||
.request::<lsp::ExecuteCommand>(lsp::ExecuteCommandParams {
|
||||
command: command.command.clone(),
|
||||
arguments: command.arguments.clone().unwrap_or_default(),
|
||||
..Default::default()
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify accepted")?;
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Status {
|
||||
match &self.server {
|
||||
CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
|
||||
@@ -1188,10 +1246,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
|
||||
.await;
|
||||
if should_install {
|
||||
node_runtime
|
||||
.npm_install_packages(
|
||||
paths::copilot_dir(),
|
||||
&[(PACKAGE_NAME, &latest_version.to_string())],
|
||||
)
|
||||
.npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)])
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -1202,11 +1257,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use util::{
|
||||
path,
|
||||
paths::PathStyle,
|
||||
rel_path::{RelPath, rel_path},
|
||||
};
|
||||
use util::{path, paths::PathStyle, rel_path::rel_path};
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_management(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
use crate::{Copilot, CopilotEditPrediction};
|
||||
use crate::{Completion, Copilot};
|
||||
use anyhow::Result;
|
||||
use edit_prediction_types::{EditPrediction, EditPredictionDelegate, interpolate_edits};
|
||||
use gpui::{App, Context, Entity, Task};
|
||||
use language::{Anchor, Buffer, EditPreview, OffsetRangeExt};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
|
||||
use gpui::{App, Context, Entity, EntityId, Task};
|
||||
use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
|
||||
pub struct CopilotEditPredictionDelegate {
|
||||
completion: Option<(CopilotEditPrediction, EditPreview)>,
|
||||
cycled: bool,
|
||||
buffer_id: Option<EntityId>,
|
||||
completions: Vec<Completion>,
|
||||
active_completion_index: usize,
|
||||
file_extension: Option<String>,
|
||||
pending_refresh: Option<Task<Result<()>>>,
|
||||
pending_cycling_refresh: Option<Task<Result<()>>>,
|
||||
copilot: Entity<Copilot>,
|
||||
}
|
||||
|
||||
impl CopilotEditPredictionDelegate {
|
||||
pub fn new(copilot: Entity<Copilot>) -> Self {
|
||||
Self {
|
||||
completion: None,
|
||||
cycled: false,
|
||||
buffer_id: None,
|
||||
completions: Vec::new(),
|
||||
active_completion_index: 0,
|
||||
file_extension: None,
|
||||
pending_refresh: None,
|
||||
pending_cycling_refresh: None,
|
||||
copilot,
|
||||
}
|
||||
}
|
||||
|
||||
fn active_completion(&self) -> Option<&(CopilotEditPrediction, EditPreview)> {
|
||||
self.completion.as_ref()
|
||||
fn active_completion(&self) -> Option<&Completion> {
|
||||
self.completions.get(self.active_completion_index)
|
||||
}
|
||||
|
||||
fn push_completion(&mut self, new_completion: Completion) {
|
||||
for completion in &self.completions {
|
||||
if completion.text == new_completion.text && completion.range == new_completion.range {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.completions.push(new_completion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +64,12 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_jump_to_edit() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_refreshing(&self, _cx: &App) -> bool {
|
||||
self.pending_refresh.is_some() && self.completion.is_none()
|
||||
self.pending_refresh.is_some() && self.completions.is_empty()
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
@@ -78,96 +102,160 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
if let Some(mut completion) = completions.into_iter().next()
|
||||
&& let Some(trimmed_completion) = cx
|
||||
.update(|cx| trim_completion(&completion, cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
let preview = buffer
|
||||
.update(cx, |this, cx| {
|
||||
this.preview_edits(Arc::from(std::slice::from_ref(&trimmed_completion)), cx)
|
||||
})?
|
||||
.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
if !completions.is_empty() {
|
||||
this.cycled = false;
|
||||
this.pending_refresh = None;
|
||||
completion.range = trimmed_completion.0;
|
||||
completion.text = trimmed_completion.1.to_string();
|
||||
this.completion = Some((completion, preview));
|
||||
this.pending_cycling_refresh = None;
|
||||
this.completions.clear();
|
||||
this.active_completion_index = 0;
|
||||
this.buffer_id = Some(buffer.entity_id());
|
||||
this.file_extension = buffer.read(cx).file().and_then(|file| {
|
||||
Some(
|
||||
Path::new(file.file_name(cx))
|
||||
.extension()?
|
||||
.to_str()?
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
|
||||
for completion in completions {
|
||||
this.push_completion(completion);
|
||||
}
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.cycled {
|
||||
match direction {
|
||||
Direction::Prev => {
|
||||
self.active_completion_index = if self.active_completion_index == 0 {
|
||||
self.completions.len().saturating_sub(1)
|
||||
} else {
|
||||
self.active_completion_index - 1
|
||||
};
|
||||
}
|
||||
Direction::Next => {
|
||||
if self.completions.is_empty() {
|
||||
self.active_completion_index = 0
|
||||
} else {
|
||||
self.active_completion_index =
|
||||
(self.active_completion_index + 1) % self.completions.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
let copilot = self.copilot.clone();
|
||||
self.pending_cycling_refresh = Some(cx.spawn(async move |this, cx| {
|
||||
let completions = copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.completions_cycling(&buffer, cursor_position, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.cycled = true;
|
||||
this.file_extension = buffer.read(cx).file().and_then(|file| {
|
||||
Some(
|
||||
Path::new(file.file_name(cx))
|
||||
.extension()?
|
||||
.to_str()?
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
for completion in completions {
|
||||
this.push_completion(completion);
|
||||
}
|
||||
this.cycle(buffer, cursor_position, direction, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some((completion, _)) = self.active_completion() {
|
||||
if let Some(completion) = self.active_completion() {
|
||||
self.copilot
|
||||
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn discard(&mut self, _: &mut Context<Self>) {}
|
||||
fn discard(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
let copilot_enabled = settings.show_edit_predictions(None, cx);
|
||||
|
||||
if !copilot_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.discard_completions(&self.completions, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn suggest(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
_: language::Anchor,
|
||||
cursor_position: language::Anchor,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<EditPrediction> {
|
||||
let buffer_id = buffer.entity_id();
|
||||
let buffer = buffer.read(cx);
|
||||
let (completion, edit_preview) = self.active_completion()?;
|
||||
|
||||
if Some(buffer_id) != Some(completion.buffer.entity_id())
|
||||
let completion = self.active_completion()?;
|
||||
if Some(buffer_id) != self.buffer_id
|
||||
|| !completion.range.start.is_valid(buffer)
|
||||
|| !completion.range.end.is_valid(buffer)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let edits = vec![(
|
||||
completion.range.clone(),
|
||||
Arc::from(completion.text.as_ref()),
|
||||
)];
|
||||
let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits)
|
||||
.filter(|edits| !edits.is_empty())?;
|
||||
|
||||
Some(EditPrediction::Local {
|
||||
id: None,
|
||||
edits,
|
||||
edit_preview: Some(edit_preview.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
let mut completion_range = completion.range.to_offset(buffer);
|
||||
let prefix_len = common_prefix(
|
||||
buffer.chars_for_range(completion_range.clone()),
|
||||
completion.text.chars(),
|
||||
);
|
||||
completion_range.start += prefix_len;
|
||||
let suffix_len = common_prefix(
|
||||
buffer.reversed_chars_for_range(completion_range.clone()),
|
||||
completion.text[prefix_len..].chars().rev(),
|
||||
);
|
||||
completion_range.end = completion_range.end.saturating_sub(suffix_len);
|
||||
|
||||
fn trim_completion(
|
||||
completion: &CopilotEditPrediction,
|
||||
cx: &mut App,
|
||||
) -> Option<(Range<Anchor>, Arc<str>)> {
|
||||
let buffer = completion.buffer.read(cx);
|
||||
let mut completion_range = completion.range.to_offset(buffer);
|
||||
let prefix_len = common_prefix(
|
||||
buffer.chars_for_range(completion_range.clone()),
|
||||
completion.text.chars(),
|
||||
);
|
||||
completion_range.start += prefix_len;
|
||||
let suffix_len = common_prefix(
|
||||
buffer.reversed_chars_for_range(completion_range.clone()),
|
||||
completion.text[prefix_len..].chars().rev(),
|
||||
);
|
||||
completion_range.end = completion_range.end.saturating_sub(suffix_len);
|
||||
let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
|
||||
if completion_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
let completion_range =
|
||||
buffer.anchor_after(completion_range.start)..buffer.anchor_after(completion_range.end);
|
||||
|
||||
Some((completion_range, Arc::from(completion_text)))
|
||||
if completion_range.is_empty()
|
||||
&& completion_range.start == cursor_position.to_offset(buffer)
|
||||
{
|
||||
let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
|
||||
if completion_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
let position = cursor_position.bias_right(buffer);
|
||||
Some(EditPrediction::Local {
|
||||
id: None,
|
||||
edits: vec![(position..position, completion_text.into())],
|
||||
edit_preview: None,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +282,6 @@ mod tests {
|
||||
Point,
|
||||
language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode},
|
||||
};
|
||||
use lsp::Uri;
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use settings::{AllLanguageSettingsContent, SettingsStore};
|
||||
@@ -250,15 +337,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -299,15 +383,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -331,15 +412,12 @@ mod tests {
|
||||
// After debouncing, new Copilot completions should be requested.
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot2".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -401,6 +479,45 @@ mod tests {
|
||||
assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.set_text("fn foo() {\n \n}", window, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
|
||||
});
|
||||
});
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: " let x = 4;".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
|
||||
// Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
|
||||
editor.tab(&Default::default(), window, cx);
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
|
||||
// Using AcceptEditPrediction again accepts the suggestion.
|
||||
editor.accept_edit_prediction(&Default::default(), window, cx);
|
||||
assert!(!editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -453,15 +570,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -500,15 +614,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.123. copilot\n 456".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -575,18 +686,15 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_edit_prediction(&Default::default(), window, cx)
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -595,22 +703,15 @@ mod tests {
|
||||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\nt\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\n\nthree\n");
|
||||
|
||||
// Deleting across the original suggestion range invalidates it.
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
assert!(!editor.has_active_edit_prediction());
|
||||
@@ -652,7 +753,7 @@ mod tests {
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
use gpui::Focusable;
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
})
|
||||
.unwrap();
|
||||
let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
|
||||
@@ -664,22 +765,19 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "b = 2 + a".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Ensure copilot suggestions are shown for the first excerpt.
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
|
||||
});
|
||||
editor.show_edit_prediction(&Default::default(), window, cx);
|
||||
editor.next_edit_prediction(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
_ = editor.update(cx, |editor, _, cx| {
|
||||
@@ -693,15 +791,12 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "d = 4 + c".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Move to another excerpt, ensuring the suggestion gets cleared.
|
||||
@@ -778,18 +873,15 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_edit_prediction(&Default::default(), window, cx)
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -811,15 +903,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -841,15 +930,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -914,7 +1000,7 @@ mod tests {
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
use gpui::Focusable;
|
||||
window.focus(&editor.focus_handle(cx), cx)
|
||||
window.focus(&editor.focus_handle(cx))
|
||||
})
|
||||
.unwrap();
|
||||
let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
|
||||
@@ -925,20 +1011,16 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let mut copilot_requests = copilot_lsp
|
||||
.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
|
||||
.set_request_handler::<crate::request::GetCompletions, _, _>(
|
||||
move |_params, _cx| async move {
|
||||
Ok(crate::request::NextEditSuggestionsResult {
|
||||
edits: vec![crate::request::NextEditSuggestion {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: vec![crate::request::Completion {
|
||||
text: "next line".into(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(1, 0),
|
||||
lsp::Position::new(1, 0),
|
||||
),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
})
|
||||
},
|
||||
@@ -967,14 +1049,23 @@ mod tests {
|
||||
|
||||
fn handle_copilot_completion_request(
|
||||
lsp: &lsp::FakeLanguageServer,
|
||||
completions: Vec<crate::request::NextEditSuggestion>,
|
||||
completions: Vec<crate::request::Completion>,
|
||||
completions_cycling: Vec<crate::request::Completion>,
|
||||
) {
|
||||
lsp.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
|
||||
lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions.clone(),
|
||||
})
|
||||
}
|
||||
});
|
||||
lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
|
||||
move |_params, _cx| {
|
||||
let completions = completions.clone();
|
||||
let completions_cycling = completions_cycling.clone();
|
||||
async move {
|
||||
Ok(crate::request::NextEditSuggestionsResult {
|
||||
edits: completions.clone(),
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions_cycling.clone(),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use lsp::VersionedTextDocumentIdentifier;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub enum CheckStatus {}
|
||||
@@ -89,6 +88,72 @@ impl lsp::request::Request for SignOut {
|
||||
const METHOD: &'static str = "signOut";
|
||||
}
|
||||
|
||||
pub enum GetCompletions {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsParams {
|
||||
pub doc: GetCompletionsDocument,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsDocument {
|
||||
pub tab_size: u32,
|
||||
pub indent_size: u32,
|
||||
pub insert_spaces: bool,
|
||||
pub uri: lsp::Uri,
|
||||
pub relative_path: String,
|
||||
pub position: lsp::Position,
|
||||
pub version: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsResult {
|
||||
pub completions: Vec<Completion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Completion {
|
||||
pub text: String,
|
||||
pub position: lsp::Position,
|
||||
pub uuid: String,
|
||||
pub range: lsp::Range,
|
||||
pub display_text: String,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for GetCompletions {
|
||||
type Params = GetCompletionsParams;
|
||||
type Result = GetCompletionsResult;
|
||||
const METHOD: &'static str = "getCompletions";
|
||||
}
|
||||
|
||||
pub enum GetCompletionsCycling {}
|
||||
|
||||
impl lsp::request::Request for GetCompletionsCycling {
|
||||
type Params = GetCompletionsParams;
|
||||
type Result = GetCompletionsResult;
|
||||
const METHOD: &'static str = "getCompletionsCycling";
|
||||
}
|
||||
|
||||
pub enum LogMessage {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogMessageParams {
|
||||
pub level: u8,
|
||||
pub message: String,
|
||||
pub metadata_str: String,
|
||||
pub extra: Vec<String>,
|
||||
}
|
||||
|
||||
impl lsp::notification::Notification for LogMessage {
|
||||
type Params = LogMessageParams;
|
||||
const METHOD: &'static str = "LogMessage";
|
||||
}
|
||||
|
||||
pub enum StatusNotification {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -158,36 +223,3 @@ impl lsp::request::Request for NotifyRejected {
|
||||
type Result = String;
|
||||
const METHOD: &'static str = "notifyRejected";
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestions;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestionsParams {
|
||||
pub(crate) text_document: VersionedTextDocumentIdentifier,
|
||||
pub(crate) position: lsp::Position,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestion {
|
||||
pub text: String,
|
||||
pub text_document: VersionedTextDocumentIdentifier,
|
||||
pub range: lsp::Range,
|
||||
pub command: Option<lsp::Command>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestionsResult {
|
||||
pub edits: Vec<NextEditSuggestion>,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for NextEditSuggestions {
|
||||
type Params = NextEditSuggestionsParams;
|
||||
type Result = NextEditSuggestionsResult;
|
||||
|
||||
const METHOD: &'static str = "textDocument/copilotInlineEdit";
|
||||
}
|
||||
|
||||
@@ -435,8 +435,8 @@ impl Render for CopilotCodeVerification {
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
window.focus(&this.focus_handle, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _| {
|
||||
window.focus(&this.focus_handle);
|
||||
}))
|
||||
.child(
|
||||
Vector::new(VectorName::ZedXCopilot, rems(8.), rems(4.))
|
||||
|
||||
@@ -577,7 +577,7 @@ impl DebugPanel {
|
||||
menu
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
|
||||
this.context_menu.take();
|
||||
cx.notify();
|
||||
@@ -1052,7 +1052,7 @@ impl DebugPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
debug_assert!(self.sessions_with_children.contains_key(&session_item));
|
||||
session_item.focus_handle(cx).focus(window, cx);
|
||||
session_item.focus_handle(cx).focus(window);
|
||||
session_item.update(cx, |this, cx| {
|
||||
this.running_state().update(cx, |this, cx| {
|
||||
this.go_to_selected_stack_frame(window, cx);
|
||||
|
||||
@@ -574,7 +574,7 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Launch => NewProcessMode::Task,
|
||||
};
|
||||
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
}))
|
||||
.on_action(
|
||||
cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
|
||||
@@ -585,7 +585,7 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Launch => NewProcessMode::Attach,
|
||||
};
|
||||
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
@@ -602,7 +602,7 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Task.to_string(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewProcessMode::Task;
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
@@ -611,7 +611,7 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Debug.to_string(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewProcessMode::Debug;
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
@@ -629,7 +629,7 @@ impl Render for NewProcessModal {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
@@ -638,7 +638,7 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Launch.to_string(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewProcessMode::Launch;
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
@@ -840,17 +840,17 @@ impl ConfigureMode {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus_next(cx);
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_next();
|
||||
}
|
||||
|
||||
fn on_tab_prev(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
window.focus_prev(cx);
|
||||
window.focus_prev();
|
||||
}
|
||||
|
||||
fn render(
|
||||
@@ -923,7 +923,7 @@ impl AttachMode {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
window.focus(&modal.focus_handle(cx), cx);
|
||||
window.focus(&modal.focus_handle(cx));
|
||||
|
||||
modal
|
||||
});
|
||||
|
||||
@@ -83,8 +83,8 @@ impl Render for DebuggerOnboardingModal {
|
||||
debugger_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -604,7 +604,7 @@ impl DebugTerminal {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let focus_subscription = cx.on_focus(&focus_handle, window, |this, window, cx| {
|
||||
if let Some(terminal) = this.terminal.as_ref() {
|
||||
terminal.focus_handle(cx).focus(window, cx);
|
||||
terminal.focus_handle(cx).focus(window);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ impl BreakpointList {
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
} else if self.strip_mode.is_some() {
|
||||
self.strip_mode.take();
|
||||
cx.notify();
|
||||
@@ -364,9 +364,9 @@ impl BreakpointList {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
} else {
|
||||
handle.focus(window, cx);
|
||||
handle.focus(window);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -627,7 +627,7 @@ impl BreakpointList {
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_, window, cx| {
|
||||
focus_handle.focus(window, cx);
|
||||
focus_handle.focus(window);
|
||||
window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
|
||||
}
|
||||
}),
|
||||
@@ -654,7 +654,7 @@ impl BreakpointList {
|
||||
)
|
||||
.on_click({
|
||||
move |_, window, cx| {
|
||||
focus_handle.focus(window, cx);
|
||||
focus_handle.focus(window);
|
||||
window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -105,7 +105,7 @@ impl Console {
|
||||
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
|
||||
cx.on_focus(&focus_handle, window, |console, window, cx| {
|
||||
if console.is_running(cx) {
|
||||
console.query_bar.focus_handle(cx).focus(window, cx);
|
||||
console.query_bar.focus_handle(cx).focus(window);
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -403,7 +403,7 @@ impl MemoryView {
|
||||
this.set_placeholder_text("Write to Selected Memory Range", window, cx);
|
||||
});
|
||||
self.is_writing_memory = true;
|
||||
self.query_editor.focus_handle(cx).focus(window, cx);
|
||||
self.query_editor.focus_handle(cx).focus(window);
|
||||
} else {
|
||||
self.query_editor.update(cx, |this, cx| {
|
||||
this.clear(window, cx);
|
||||
|
||||
@@ -529,7 +529,7 @@ impl VariableList {
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.edited_path.take();
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,7 @@ impl VariableList {
|
||||
editor.select_all(&editor::actions::SelectAll, window, cx);
|
||||
editor
|
||||
});
|
||||
editor.focus_handle(cx).focus(window, cx);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
editor
|
||||
}
|
||||
|
||||
|
||||
@@ -103,9 +103,8 @@ impl Model {
|
||||
|
||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||
match self {
|
||||
// Their API treats this max against the context window, which means we hit the limit a lot
|
||||
// Using the default value of None in the API instead
|
||||
Self::Chat | Self::Reasoner => None,
|
||||
Self::Chat => Some(8_192),
|
||||
Self::Reasoner => Some(64_000),
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => *max_output_tokens,
|
||||
|
||||
@@ -175,7 +175,7 @@ impl BufferDiagnosticsEditor {
|
||||
// `BufferDiagnosticsEditor` instance.
|
||||
EditorEvent::Focused => {
|
||||
if buffer_diagnostics_editor.multibuffer.read(cx).is_empty() {
|
||||
window.focus(&buffer_diagnostics_editor.focus_handle, cx);
|
||||
window.focus(&buffer_diagnostics_editor.focus_handle);
|
||||
}
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
@@ -517,7 +517,7 @@ impl BufferDiagnosticsEditor {
|
||||
.editor
|
||||
.read(cx)
|
||||
.focus_handle(cx)
|
||||
.focus(window, cx);
|
||||
.focus(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -617,7 +617,7 @@ impl BufferDiagnosticsEditor {
|
||||
// not empty, focus on the editor instead, which will allow the user to
|
||||
// start interacting and editing the buffer's contents.
|
||||
if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||
self.editor.focus_handle(cx).focus(window, cx)
|
||||
self.editor.focus_handle(cx).focus(window)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -315,6 +315,6 @@ impl DiagnosticBlock {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges([range.start..range.start]);
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ impl ProjectDiagnosticsEditor {
|
||||
match event {
|
||||
EditorEvent::Focused => {
|
||||
if this.multibuffer.read(cx).is_empty() {
|
||||
window.focus(&this.focus_handle, cx);
|
||||
window.focus(&this.focus_handle);
|
||||
}
|
||||
}
|
||||
EditorEvent::Blurred => this.close_diagnosticless_buffers(cx, false),
|
||||
@@ -434,7 +434,7 @@ impl ProjectDiagnosticsEditor {
|
||||
|
||||
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||
self.editor.focus_handle(cx).focus(window, cx)
|
||||
self.editor.focus_handle(cx).focus(window)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,7 +650,7 @@ impl ProjectDiagnosticsEditor {
|
||||
})
|
||||
});
|
||||
if this.focus_handle.is_focused(window) {
|
||||
this.editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
this.editor.read(cx).focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
|
||||
@@ -300,19 +300,14 @@ pub const MERCURY_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
|
||||
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
|
||||
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
|
||||
|
||||
struct GlobalMercuryApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalMercuryApiKey {}
|
||||
pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
let entity =
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
|
||||
cx.set_global(GlobalMercuryApiKey(entity.clone()));
|
||||
entity
|
||||
MERCURY_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
|
||||
@@ -131,8 +131,8 @@ impl Render for ZedPredictModal {
|
||||
onboarding_event!("Cancelled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{Point, ToOffset as _};
|
||||
@@ -272,19 +272,14 @@ pub const SWEEP_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://autocomplete.sweep.dev");
|
||||
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
|
||||
pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
|
||||
|
||||
struct GlobalSweepApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalSweepApiKey {}
|
||||
pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
if let Some(global) = cx.try_global::<GlobalSweepApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
let entity =
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
|
||||
cx.set_global(GlobalSweepApiKey(entity.clone()));
|
||||
entity
|
||||
SWEEP_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{cmp, sync::Arc};
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use cloud_llm_client::EditPredictionRejectReason;
|
||||
use edit_prediction_types::{DataCollectionState, EditPredictionDelegate};
|
||||
use edit_prediction_types::{DataCollectionState, Direction, EditPredictionDelegate};
|
||||
use gpui::{App, Entity, prelude::*};
|
||||
use language::{Buffer, ToPoint as _};
|
||||
use project::Project;
|
||||
@@ -139,6 +139,15 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
|
||||
});
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: Direction,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
self.store.update(cx, |store, cx| {
|
||||
store.accept_current_prediction(&self.project, cx);
|
||||
|
||||
@@ -8,7 +8,8 @@ use gpui_tokio::Tokio;
|
||||
use language::LanguageRegistry;
|
||||
use language_extension::LspAccess;
|
||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||
use project::{Project, project_settings::ProjectSettings};
|
||||
use project::Project;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use release_channel::{AppCommitSha, AppVersion};
|
||||
use reqwest_client::ReqwestClient;
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
||||
@@ -179,7 +179,6 @@ async fn setup_project(
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -95,6 +95,13 @@ pub trait EditPredictionDelegate: 'static + Sized {
|
||||
debounce: bool,
|
||||
cx: &mut Context<Self>,
|
||||
);
|
||||
fn cycle(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut Context<Self>,
|
||||
);
|
||||
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>) {}
|
||||
@@ -129,6 +136,13 @@ pub trait EditPredictionDelegateHandle {
|
||||
debounce: bool,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn cycle(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn did_show(&self, cx: &mut App);
|
||||
fn accept(&self, cx: &mut App);
|
||||
fn discard(&self, cx: &mut App);
|
||||
@@ -201,6 +215,18 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.cycle(buffer, cursor_position, direction, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn accept(&self, cx: &mut App) {
|
||||
self.update(cx, |this, cx| this.accept(cx))
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ impl RatePredictionsModal {
|
||||
&& prediction.id == prev_prediction.prediction.id
|
||||
{
|
||||
if focus {
|
||||
window.focus(&prev_prediction.feedback_editor.focus_handle(cx), cx);
|
||||
window.focus(&prev_prediction.feedback_editor.focus_handle(cx));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ fn editor_input_with_1000_cursors(bencher: &mut Bencher<'_>, cx: &TestAppContext
|
||||
);
|
||||
editor
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ fn open_editor_with_one_long_line(bencher: &mut Bencher<'_>, args: &(String, Tes
|
||||
editor.set_style(editor::EditorStyle::default(), window, cx);
|
||||
editor
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor
|
||||
});
|
||||
});
|
||||
@@ -100,7 +100,7 @@ fn editor_render(bencher: &mut Bencher<'_>, cx: &TestAppContext) {
|
||||
editor.set_style(editor::EditorStyle::default(), window, cx);
|
||||
editor
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor
|
||||
});
|
||||
|
||||
|
||||
@@ -348,61 +348,6 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.colorize_brackets = Some(true);
|
||||
});
|
||||
|
||||
let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
|
||||
language_registry.add(markdown_lang());
|
||||
language_registry.add(rust_lang());
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language_registry(language_registry.clone());
|
||||
buffer.set_language(Some(markdown_lang()), cx);
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {r#"
|
||||
fn main() {
|
||||
let v: Vec<Stringˇ> = vec![];
|
||||
}
|
||||
"#});
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
r#"fn main«1()1» «1{
|
||||
let v: Vec<String> = vec!«2[]2»;
|
||||
}1»
|
||||
|
||||
1 hsla(207.80, 16.20%, 69.19%, 1.00)
|
||||
2 hsla(29.00, 54.00%, 65.88%, 1.00)
|
||||
"#,
|
||||
&bracket_colors_markup(&mut cx),
|
||||
"Markdown does not colorize <> brackets"
|
||||
);
|
||||
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language(Some(rust_lang()), cx);
|
||||
});
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
r#"fn main«1()1» «1{
|
||||
let v: Vec«2<String>2» = vec!«2[]2»;
|
||||
}1»
|
||||
|
||||
1 hsla(207.80, 16.20%, 69.19%, 1.00)
|
||||
2 hsla(29.00, 54.00%, 65.88%, 1.00)
|
||||
"#,
|
||||
&bracket_colors_markup(&mut cx),
|
||||
"After switching to Rust, <> brackets are now colorized"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
|
||||
@@ -14,57 +14,8 @@
|
||||
//! - [`DisplayMap`] that adds background highlights to the regions of text.
|
||||
//! Each one of those builds on top of preceding map.
|
||||
//!
|
||||
//! ## Structure of the display map layers
|
||||
//!
|
||||
//! Each layer in the map (and the multibuffer itself to some extent) has a few
|
||||
//! structures that are used to implement the public API available to the layer
|
||||
//! above:
|
||||
//! - a `Transform` type - this represents a region of text that the layer in
|
||||
//! question is "managing", that it transforms into a more "processed" text
|
||||
//! for the layer above. For example, the inlay map has an `enum Transform`
|
||||
//! that has two variants:
|
||||
//! - `Isomorphic`, representing a region of text that has no inlay hints (i.e.
|
||||
//! is passed through the map transparently)
|
||||
//! - `Inlay`, representing a location where an inlay hint is to be inserted.
|
||||
//! - a `TransformSummary` type, which is usually a struct with two fields:
|
||||
//! [`input: TextSummary`][`TextSummary`] and [`output: TextSummary`][`TextSummary`]. Here,
|
||||
//! `input` corresponds to "text in the layer below", and `output` corresponds to the text
|
||||
//! exposed to the layer above. So in the inlay map case, a `Transform::Isomorphic`'s summary is
|
||||
//! just `input = output = summary`, where `summary` is the [`TextSummary`] stored in that
|
||||
//! variant. Conversely, a `Transform::Inlay` always has an empty `input` summary, because it's
|
||||
//! not "replacing" any text that exists on disk. The `output` is the summary of the inlay text
|
||||
//! to be injected. - Various newtype wrappers for co-ordinate spaces (e.g. [`WrapRow`]
|
||||
//! represents a row index, after soft-wrapping (and all lower layers)).
|
||||
//! - A `Snapshot` type (e.g. [`InlaySnapshot`]) that captures the state of a layer at a specific
|
||||
//! point in time.
|
||||
//! - various APIs which drill through the layers below to work with the underlying text. Notably:
|
||||
//! - `fn text_summary_for_offset()` returns a [`TextSummary`] for the range in the co-ordinate
|
||||
//! space that the map in question is responsible for.
|
||||
//! - `fn <A>_point_to_<B>_point()` converts a point in co-ordinate space `A` into co-ordinate
|
||||
//! space `B`.
|
||||
//! - A [`RowInfo`] iterator (e.g. [`InlayBufferRows`]) and a [`Chunk`] iterator
|
||||
//! (e.g. [`InlayChunks`])
|
||||
//! - A `sync` function (e.g. [`InlayMap::sync`]) that takes a snapshot and list of [`Edit<T>`]s,
|
||||
//! and returns a new snapshot and a list of transformed [`Edit<S>`]s. Note that the generic
|
||||
//! parameter on `Edit` changes, since these methods take in edits in the co-ordinate space of
|
||||
//! the lower layer, and return edits in their own co-ordinate space. The term "edit" is
|
||||
//! slightly misleading, since an [`Edit<T>`] doesn't tell you what changed - rather it can be
|
||||
//! thought of as a "region to invalidate". In theory, it would be correct to always use a
|
||||
//! single edit that covers the entire range. However, this would lead to lots of unnecessary
|
||||
//! recalculation.
|
||||
//!
|
||||
//! See the docs for the [`inlay_map`] module for a more in-depth explanation of how a single layer
|
||||
//! works.
|
||||
//!
|
||||
//! [Editor]: crate::Editor
|
||||
//! [EditorElement]: crate::element::EditorElement
|
||||
//! [`TextSummary`]: multi_buffer::MBTextSummary
|
||||
//! [`WrapRow`]: wrap_map::WrapRow
|
||||
//! [`InlayBufferRows`]: inlay_map::InlayBufferRows
|
||||
//! [`InlayChunks`]: inlay_map::InlayChunks
|
||||
//! [`Edit<T>`]: text::Edit
|
||||
//! [`Edit<S>`]: text::Edit
|
||||
//! [`Chunk`]: language::Chunk
|
||||
|
||||
#[macro_use]
|
||||
mod dimensions;
|
||||
|
||||
@@ -545,7 +545,7 @@ impl BlockMap {
|
||||
{
|
||||
let max_point = wrap_snapshot.max_point();
|
||||
let edit_start = wrap_snapshot.prev_row_boundary(max_point);
|
||||
let edit_end = max_point.row() + WrapRow(1); // this is end of file
|
||||
let edit_end = max_point.row() + WrapRow(1);
|
||||
edits = edits.compose([WrapEdit {
|
||||
old: edit_start..edit_end,
|
||||
new: edit_start..edit_end,
|
||||
@@ -715,7 +715,6 @@ impl BlockMap {
|
||||
let placement = block.placement.to_wrap_row(wrap_snapshot)?;
|
||||
if let BlockPlacement::Above(row) = placement
|
||||
&& row < new_start
|
||||
// this will be true more often now
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
//! The inlay map. See the [`display_map`][super] docs for an overview of how the inlay map fits
|
||||
//! into the rest of the [`DisplayMap`][super::DisplayMap]. Much of the documentation for this
|
||||
//! module generalizes to other layers.
|
||||
//!
|
||||
//! The core of this module is the [`InlayMap`] struct, which maintains a vec of [`Inlay`]s, and
|
||||
//! [`InlaySnapshot`], which holds a sum tree of [`Transform`]s.
|
||||
|
||||
use crate::{
|
||||
ChunkRenderer, HighlightStyles,
|
||||
inlays::{Inlay, InlayContent},
|
||||
@@ -76,9 +69,7 @@ impl sum_tree::Item for Transform {
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct TransformSummary {
|
||||
/// Summary of the text before inlays have been applied.
|
||||
input: MBTextSummary,
|
||||
/// Summary of the text after inlays have been applied.
|
||||
output: MBTextSummary,
|
||||
}
|
||||
|
||||
|
||||
@@ -840,62 +840,35 @@ impl WrapSnapshot {
|
||||
self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
|
||||
}
|
||||
|
||||
/// Try to find a TabRow start that is also a WrapRow start
|
||||
/// Every TabRow start is a WrapRow start
|
||||
#[ztracing::instrument(skip_all, fields(point=?point))]
|
||||
pub fn prev_row_boundary(&self, point: WrapPoint) -> WrapRow {
|
||||
#[ztracing::instrument(skip_all, fields(point=?point, ret))]
|
||||
pub fn prev_row_boundary(&self, mut point: WrapPoint) -> WrapRow {
|
||||
if self.transforms.is_empty() {
|
||||
return WrapRow(0);
|
||||
}
|
||||
|
||||
let point = WrapPoint::new(point.row(), 0);
|
||||
*point.column_mut() = 0;
|
||||
|
||||
let mut cursor = self
|
||||
.transforms
|
||||
.cursor::<Dimensions<WrapPoint, TabPoint>>(());
|
||||
|
||||
// start
|
||||
cursor.seek(&point, Bias::Right);
|
||||
// end
|
||||
if cursor.item().is_none() {
|
||||
cursor.prev();
|
||||
}
|
||||
|
||||
// real newline fake fake
|
||||
// text: helloworldasldlfjasd\njdlasfalsk\naskdjfasdkfj\n
|
||||
// dimensions v v v v v
|
||||
// transforms |-------|-----NW----|-----W------|-----W------|
|
||||
// cursor ^ ^^^^^^^^^^^^^ ^
|
||||
// (^) ^^^^^^^^^^^^^^
|
||||
// point: ^
|
||||
// point(col_zero): (^)
|
||||
|
||||
// start
|
||||
while let Some(transform) = cursor.item() {
|
||||
if transform.is_isomorphic() {
|
||||
// this transform only has real linefeeds
|
||||
let tab_summary = &transform.summary.input;
|
||||
// is the wrap just before the end of the transform a tab row?
|
||||
// thats only if this transform has at least one newline
|
||||
//
|
||||
// "this wrap row is a tab row" <=> self.to_tab_point(WrapPoint::new(wrap_row, 0)).column() == 0
|
||||
|
||||
// Note on comparison:
|
||||
// We have code that relies on this to be row > 1
|
||||
// It should work with row >= 1 but it does not :(
|
||||
//
|
||||
// That means that if every line is wrapped we walk back all the
|
||||
// way to the start. Which invalidates the entire state triggering
|
||||
// a full re-render.
|
||||
if tab_summary.lines.row > 1 {
|
||||
let wrap_point_at_end = cursor.end().0.row();
|
||||
return cmp::min(wrap_point_at_end - RowDelta(1), point.row());
|
||||
} else if cursor.start().1.column() == 0 {
|
||||
return cmp::min(cursor.end().0.row(), point.row());
|
||||
}
|
||||
if transform.is_isomorphic() && cursor.start().1.column() == 0 {
|
||||
return cmp::min(cursor.end().0.row(), point.row());
|
||||
} else {
|
||||
cursor.prev();
|
||||
}
|
||||
|
||||
cursor.prev();
|
||||
}
|
||||
// end
|
||||
|
||||
WrapRow(0)
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[ztracing::instrument(skip_all)]
|
||||
@@ -918,11 +891,13 @@ impl WrapSnapshot {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ztracing::instrument(skip_all)]
|
||||
pub fn text(&self) -> String {
|
||||
self.text_chunks(WrapRow(0)).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ztracing::instrument(skip_all)]
|
||||
pub fn text_chunks(&self, wrap_row: WrapRow) -> impl Iterator<Item = &str> {
|
||||
self.chunks(
|
||||
wrap_row..self.max_point().row() + WrapRow(1),
|
||||
@@ -1323,71 +1298,6 @@ mod tests {
|
||||
use text::Rope;
|
||||
use theme::LoadThemes;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prev_row_boundary(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
fn test_wrap_snapshot(
|
||||
text: &str,
|
||||
soft_wrap_every: usize, // font size multiple
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> WrapSnapshot {
|
||||
let text_system = cx.read(|cx| cx.text_system().clone());
|
||||
let tab_size = 4.try_into().unwrap();
|
||||
let font = test_font();
|
||||
let _font_id = text_system.resolve_font(&font);
|
||||
let font_size = px(14.0);
|
||||
// this is very much an estimate to try and get the wrapping to
|
||||
// occur at `soft_wrap_every` we check that it pans out for every test case
|
||||
let soft_wrapping = Some(font_size * soft_wrap_every * 0.6);
|
||||
|
||||
let buffer = cx.new(|cx| language::Buffer::local(text, cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
|
||||
let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size);
|
||||
let tabs_snapshot = tab_map.set_max_expansion_column(32);
|
||||
let (_wrap_map, wrap_snapshot) =
|
||||
cx.update(|cx| WrapMap::new(tabs_snapshot, font, font_size, soft_wrapping, cx));
|
||||
|
||||
wrap_snapshot
|
||||
}
|
||||
|
||||
// These two should pass but dont, see the comparison note in
|
||||
// prev_row_boundary about why.
|
||||
//
|
||||
// // 0123 4567 wrap_rows
|
||||
// let wrap_snapshot = test_wrap_snapshot("1234\n5678", 1, cx);
|
||||
// assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8");
|
||||
// let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
|
||||
// assert_eq!(row.0, 3);
|
||||
|
||||
// // 012 345 678 wrap_rows
|
||||
// let wrap_snapshot = test_wrap_snapshot("123\n456\n789", 1, cx);
|
||||
// assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8\n9");
|
||||
// let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
|
||||
// assert_eq!(row.0, 5);
|
||||
|
||||
// 012345678 wrap_rows
|
||||
let wrap_snapshot = test_wrap_snapshot("123456789", 1, cx);
|
||||
assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8\n9");
|
||||
let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
|
||||
assert_eq!(row.0, 0);
|
||||
|
||||
// 111 2222 44 wrap_rows
|
||||
let wrap_snapshot = test_wrap_snapshot("123\n4567\n\n89", 4, cx);
|
||||
assert_eq!(wrap_snapshot.text(), "123\n4567\n\n89");
|
||||
let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
|
||||
assert_eq!(row.0, 2);
|
||||
|
||||
// 11 2223 wrap_rows
|
||||
let wrap_snapshot = test_wrap_snapshot("12\n3456\n\n", 3, cx);
|
||||
assert_eq!(wrap_snapshot.text(), "12\n345\n6\n\n");
|
||||
let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
|
||||
assert_eq!(row.0, 3);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
|
||||
// todo this test is flaky
|
||||
|
||||
@@ -485,6 +485,15 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate {
|
||||
) {
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: gpui::Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: edit_prediction_types::Direction,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
@@ -552,6 +561,15 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate {
|
||||
) {
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: gpui::Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: edit_prediction_types::Direction,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
@@ -73,7 +73,11 @@ pub use multi_buffer::{
|
||||
pub use split::SplittableEditor;
|
||||
pub use text::Bias;
|
||||
|
||||
use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
|
||||
use ::git::{
|
||||
Restore,
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
status::FileStatus,
|
||||
};
|
||||
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use blink_manager::BlinkManager;
|
||||
@@ -120,9 +124,8 @@ use language::{
|
||||
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
|
||||
BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
|
||||
DiagnosticEntryRef, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
|
||||
IndentSize, Language, LanguageName, LanguageRegistry, LanguageScope, OffsetRangeExt,
|
||||
OutlineItem, Point, Runnable, Selection, SelectionGoal, TextObject, TransactionId,
|
||||
TreeSitterOptions, WordsQuery,
|
||||
IndentSize, Language, LanguageName, LanguageRegistry, OffsetRangeExt, OutlineItem, Point,
|
||||
Runnable, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
|
||||
language_settings::{
|
||||
self, LanguageSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
|
||||
all_language_settings, language_settings,
|
||||
@@ -2060,34 +2063,46 @@ impl Editor {
|
||||
})
|
||||
});
|
||||
});
|
||||
let edited_buffers_already_open = {
|
||||
let other_editors: Vec<Entity<Editor>> = workspace
|
||||
.read(cx)
|
||||
.panes()
|
||||
.iter()
|
||||
.flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
|
||||
.filter(|editor| editor.entity_id() != cx.entity_id())
|
||||
.collect();
|
||||
|
||||
Self::open_transaction_for_hidden_buffers(
|
||||
workspace,
|
||||
transaction.clone(),
|
||||
"Rename".to_string(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
project::Event::WorkspaceEditApplied(transaction) => {
|
||||
let Some(workspace) = editor.workspace() else {
|
||||
return;
|
||||
};
|
||||
let Some(active_editor) = workspace.read(cx).active_item_as::<Self>(cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if active_editor.entity_id() == cx.entity_id() {
|
||||
Self::open_transaction_for_hidden_buffers(
|
||||
workspace,
|
||||
transaction.clone(),
|
||||
"LSP Edit".to_string(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
transaction.0.keys().all(|buffer| {
|
||||
other_editors.iter().any(|editor| {
|
||||
let multi_buffer = editor.read(cx).buffer();
|
||||
multi_buffer.read(cx).is_singleton()
|
||||
&& multi_buffer.read(cx).as_singleton().map_or(
|
||||
false,
|
||||
|singleton| {
|
||||
singleton.entity_id() == buffer.entity_id()
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
};
|
||||
if !edited_buffers_already_open {
|
||||
let workspace = workspace.downgrade();
|
||||
let transaction = transaction.clone();
|
||||
cx.defer_in(window, move |_, window, cx| {
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
Self::open_project_transaction(
|
||||
&editor,
|
||||
workspace,
|
||||
transaction,
|
||||
"Rename".to_string(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,7 +3827,7 @@ impl Editor {
|
||||
) {
|
||||
if !self.focus_handle.is_focused(window) {
|
||||
self.last_focused_descendant = None;
|
||||
window.focus(&self.focus_handle, cx);
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
@@ -3917,7 +3932,7 @@ impl Editor {
|
||||
) {
|
||||
if !self.focus_handle.is_focused(window) {
|
||||
self.last_focused_descendant = None;
|
||||
window.focus(&self.focus_handle, cx);
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
@@ -4787,51 +4802,205 @@ impl Editor {
|
||||
let end = selection.end;
|
||||
let selection_is_empty = start == end;
|
||||
let language_scope = buffer.language_scope_at(start);
|
||||
let (comment_delimiter, doc_delimiter, newline_formatting) =
|
||||
if let Some(language) = &language_scope {
|
||||
let mut newline_formatting =
|
||||
NewlineFormatting::new(&buffer, start..end, language);
|
||||
let (
|
||||
comment_delimiter,
|
||||
doc_delimiter,
|
||||
insert_extra_newline,
|
||||
indent_on_newline,
|
||||
indent_on_extra_newline,
|
||||
) = if let Some(language) = &language_scope {
|
||||
let mut insert_extra_newline =
|
||||
insert_extra_newline_brackets(&buffer, start..end, language)
|
||||
|| insert_extra_newline_tree_sitter(&buffer, start..end);
|
||||
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
let delimiters = language.line_comment_prefixes();
|
||||
let max_len_of_delimiter =
|
||||
delimiters.iter().map(|delimiter| delimiter.len()).max()?;
|
||||
let (snapshot, range) =
|
||||
buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
let comment_candidate = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_len_of_delimiter)
|
||||
.collect::<String>();
|
||||
let (delimiter, trimmed_len) = delimiters
|
||||
.iter()
|
||||
.filter_map(|delimiter| {
|
||||
let prefix = delimiter.trim_end();
|
||||
if comment_candidate.starts_with(prefix) {
|
||||
Some((delimiter, prefix.len()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.max_by_key(|(_, len)| *len)?;
|
||||
|
||||
if let Some(BlockCommentConfig {
|
||||
start: block_start, ..
|
||||
}) = language.block_comment()
|
||||
{
|
||||
let block_start_trimmed = block_start.trim_end();
|
||||
if block_start_trimmed.starts_with(delimiter.trim_end()) {
|
||||
let line_content = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(num_of_whitespaces)
|
||||
.take(block_start_trimmed.len())
|
||||
.collect::<String>();
|
||||
|
||||
if line_content.starts_with(block_start_trimmed) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_is_placed_after_comment_marker =
|
||||
num_of_whitespaces + trimmed_len <= start_point.column as usize;
|
||||
if cursor_is_placed_after_comment_marker {
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let mut indent_on_newline = IndentSize::spaces(0);
|
||||
let mut indent_on_extra_newline = IndentSize::spaces(0);
|
||||
|
||||
let doc_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
let BlockCommentConfig {
|
||||
start: start_tag,
|
||||
end: end_tag,
|
||||
prefix: delimiter,
|
||||
tab_size: len,
|
||||
} = language.documentation_comment()?;
|
||||
let is_within_block_comment = buffer
|
||||
.language_scope_at(start_point)
|
||||
.is_some_and(|scope| scope.override_name() == Some("comment"));
|
||||
if !is_within_block_comment {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (snapshot, range) =
|
||||
buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
|
||||
// It is safe to use a column from MultiBufferPoint in context of a single buffer ranges, because we're only ever looking at a single line at a time.
|
||||
let column = start_point.column;
|
||||
let cursor_is_after_start_tag = {
|
||||
let start_tag_len = start_tag.len();
|
||||
let start_tag_line = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(start_tag_len)
|
||||
.collect::<String>();
|
||||
if start_tag_line.starts_with(start_tag.as_ref()) {
|
||||
num_of_whitespaces + start_tag_len <= column as usize
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let cursor_is_after_delimiter = {
|
||||
let delimiter_trim = delimiter.trim_end();
|
||||
let delimiter_line = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(delimiter_trim.len())
|
||||
.collect::<String>();
|
||||
if delimiter_line.starts_with(delimiter_trim) {
|
||||
num_of_whitespaces + delimiter_trim.len() <= column as usize
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let cursor_is_before_end_tag_if_exists = {
|
||||
let mut char_position = 0u32;
|
||||
let mut end_tag_offset = None;
|
||||
|
||||
'outer: for chunk in snapshot.text_for_range(range) {
|
||||
if let Some(byte_pos) = chunk.find(&**end_tag) {
|
||||
let chars_before_match =
|
||||
chunk[..byte_pos].chars().count() as u32;
|
||||
end_tag_offset =
|
||||
Some(char_position + chars_before_match);
|
||||
break 'outer;
|
||||
}
|
||||
char_position += chunk.chars().count() as u32;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline
|
||||
{
|
||||
return None;
|
||||
if let Some(end_tag_offset) = end_tag_offset {
|
||||
let cursor_is_before_end_tag = column <= end_tag_offset;
|
||||
if cursor_is_after_start_tag {
|
||||
if cursor_is_before_end_tag {
|
||||
insert_extra_newline = true;
|
||||
}
|
||||
let cursor_is_at_start_of_end_tag =
|
||||
column == end_tag_offset;
|
||||
if cursor_is_at_start_of_end_tag {
|
||||
indent_on_extra_newline.len = *len;
|
||||
}
|
||||
}
|
||||
cursor_is_before_end_tag
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
return comment_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
);
|
||||
});
|
||||
|
||||
let doc_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
if (cursor_is_after_start_tag || cursor_is_after_delimiter)
|
||||
&& cursor_is_before_end_tag_if_exists
|
||||
{
|
||||
if cursor_is_after_start_tag {
|
||||
indent_on_newline.len = *len;
|
||||
}
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
return documentation_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
&mut newline_formatting,
|
||||
);
|
||||
});
|
||||
|
||||
(comment_delimiter, doc_delimiter, newline_formatting)
|
||||
} else {
|
||||
(None, None, NewlineFormatting::default())
|
||||
};
|
||||
(
|
||||
comment_delimiter,
|
||||
doc_delimiter,
|
||||
insert_extra_newline,
|
||||
indent_on_newline,
|
||||
indent_on_extra_newline,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
IndentSize::default(),
|
||||
IndentSize::default(),
|
||||
)
|
||||
};
|
||||
|
||||
let prevent_auto_indent = doc_delimiter.is_some();
|
||||
let delimiter = comment_delimiter.or(doc_delimiter);
|
||||
@@ -4841,28 +5010,28 @@ impl Editor {
|
||||
let mut new_text = String::with_capacity(
|
||||
1 + capacity_for_delimiter
|
||||
+ existing_indent.len as usize
|
||||
+ newline_formatting.indent_on_newline.len as usize
|
||||
+ newline_formatting.indent_on_extra_newline.len as usize,
|
||||
+ indent_on_newline.len as usize
|
||||
+ indent_on_extra_newline.len as usize,
|
||||
);
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(newline_formatting.indent_on_newline.chars());
|
||||
new_text.extend(indent_on_newline.chars());
|
||||
|
||||
if let Some(delimiter) = &delimiter {
|
||||
new_text.push_str(delimiter);
|
||||
}
|
||||
|
||||
if newline_formatting.insert_extra_newline {
|
||||
if insert_extra_newline {
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(newline_formatting.indent_on_extra_newline.chars());
|
||||
new_text.extend(indent_on_extra_newline.chars());
|
||||
}
|
||||
|
||||
let anchor = buffer.anchor_after(end);
|
||||
let new_selection = selection.map(|_| anchor);
|
||||
(
|
||||
((start..end, new_text), prevent_auto_indent),
|
||||
(newline_formatting.insert_extra_newline, new_selection),
|
||||
(insert_extra_newline, new_selection),
|
||||
)
|
||||
})
|
||||
.unzip()
|
||||
@@ -6503,52 +6672,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn open_transaction_for_hidden_buffers(
|
||||
workspace: Entity<Workspace>,
|
||||
transaction: ProjectTransaction,
|
||||
title: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if transaction.0.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let edited_buffers_already_open = {
|
||||
let other_editors: Vec<Entity<Editor>> = workspace
|
||||
.read(cx)
|
||||
.panes()
|
||||
.iter()
|
||||
.flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
|
||||
.filter(|editor| editor.entity_id() != cx.entity_id())
|
||||
.collect();
|
||||
|
||||
transaction.0.keys().all(|buffer| {
|
||||
other_editors.iter().any(|editor| {
|
||||
let multi_buffer = editor.read(cx).buffer();
|
||||
multi_buffer.read(cx).is_singleton()
|
||||
&& multi_buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.map_or(false, |singleton| {
|
||||
singleton.entity_id() == buffer.entity_id()
|
||||
})
|
||||
})
|
||||
})
|
||||
};
|
||||
if !edited_buffers_already_open {
|
||||
let workspace = workspace.downgrade();
|
||||
cx.defer_in(window, move |_, window, cx| {
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
Self::open_project_transaction(&editor, workspace, transaction, title, cx)
|
||||
.await
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn open_project_transaction(
|
||||
editor: &WeakEntity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -6708,7 +6831,7 @@ impl Editor {
|
||||
})
|
||||
})
|
||||
.on_click(cx.listener(move |editor, _: &ClickEvent, window, cx| {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.toggle_code_actions(
|
||||
&crate::actions::ToggleCodeActions {
|
||||
deployed_from: Some(crate::actions::CodeActionSource::Indicator(
|
||||
@@ -7464,6 +7587,26 @@ impl Editor {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn cycle_edit_prediction(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
let provider = self.edit_prediction_provider()?;
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
let (buffer, cursor_buffer_position) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||
if self.edit_predictions_hidden_for_vim_mode || !self.should_show_edit_predictions() {
|
||||
return None;
|
||||
}
|
||||
|
||||
provider.cycle(buffer, cursor_buffer_position, direction, cx);
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn show_edit_prediction(
|
||||
&mut self,
|
||||
_: &ShowEditPrediction,
|
||||
@@ -7501,6 +7644,42 @@ impl Editor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn next_edit_prediction(
|
||||
&mut self,
|
||||
_: &NextEditPrediction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.has_active_edit_prediction() {
|
||||
self.cycle_edit_prediction(Direction::Next, window, cx);
|
||||
} else {
|
||||
let is_copilot_disabled = self
|
||||
.refresh_edit_prediction(false, true, window, cx)
|
||||
.is_none();
|
||||
if is_copilot_disabled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_edit_prediction(
|
||||
&mut self,
|
||||
_: &PreviousEditPrediction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.has_active_edit_prediction() {
|
||||
self.cycle_edit_prediction(Direction::Prev, window, cx);
|
||||
} else {
|
||||
let is_copilot_disabled = self
|
||||
.refresh_edit_prediction(false, true, window, cx)
|
||||
.is_none();
|
||||
if is_copilot_disabled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept_partial_edit_prediction(
|
||||
&mut self,
|
||||
granularity: EditPredictionGranularity,
|
||||
@@ -8545,7 +8724,7 @@ impl Editor {
|
||||
BreakpointEditAction::Toggle
|
||||
};
|
||||
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
position,
|
||||
breakpoint.as_ref().clone(),
|
||||
@@ -8737,7 +8916,7 @@ impl Editor {
|
||||
ClickEvent::Mouse(e) => e.down.button == MouseButton::Left,
|
||||
};
|
||||
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.toggle_code_actions(
|
||||
&ToggleCodeActions {
|
||||
deployed_from: Some(CodeActionSource::RunMenu(row)),
|
||||
@@ -11152,7 +11331,7 @@ impl Editor {
|
||||
}];
|
||||
|
||||
let focus_handle = bp_prompt.focus_handle(cx);
|
||||
window.focus(&focus_handle, cx);
|
||||
window.focus(&focus_handle);
|
||||
|
||||
let block_ids = self.insert_blocks(blocks, None, cx);
|
||||
bp_prompt.update(cx, |prompt, _| {
|
||||
@@ -15355,9 +15534,10 @@ impl Editor {
|
||||
I: IntoIterator<Item = P>,
|
||||
P: AsRef<[u8]>,
|
||||
{
|
||||
let case_sensitive = self
|
||||
.select_next_is_case_sensitive
|
||||
.unwrap_or_else(|| EditorSettings::get_global(cx).search.case_sensitive);
|
||||
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);
|
||||
@@ -17979,7 +18159,7 @@ impl Editor {
|
||||
cx,
|
||||
);
|
||||
let rename_focus_handle = rename_editor.focus_handle(cx);
|
||||
window.focus(&rename_focus_handle, cx);
|
||||
window.focus(&rename_focus_handle);
|
||||
let block_id = this.insert_blocks(
|
||||
[BlockProperties {
|
||||
style: BlockStyle::Flex,
|
||||
@@ -18093,7 +18273,7 @@ impl Editor {
|
||||
) -> Option<RenameState> {
|
||||
let rename = self.pending_rename.take()?;
|
||||
if rename.editor.focus_handle(cx).is_focused(window) {
|
||||
window.focus(&self.focus_handle, cx);
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
self.remove_blocks(
|
||||
@@ -19914,8 +20094,10 @@ impl Editor {
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
println!("\n\nin toggle_diff_hunks_in_ranges");
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
let expand = !buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx);
|
||||
println!("expand={expand}\n\n");
|
||||
buffer.expand_or_collapse_diff_hunks(ranges, expand, cx);
|
||||
})
|
||||
}
|
||||
@@ -22663,7 +22845,7 @@ impl Editor {
|
||||
.take()
|
||||
.and_then(|descendant| descendant.upgrade())
|
||||
{
|
||||
window.focus(&descendant, cx);
|
||||
window.focus(&descendant);
|
||||
} else {
|
||||
if let Some(blame) = self.blame.as_ref() {
|
||||
blame.update(cx, GitBlame::focus)
|
||||
@@ -23294,256 +23476,76 @@ struct CompletionEdit {
|
||||
snippet: Option<Snippet>,
|
||||
}
|
||||
|
||||
fn comment_delimiter_for_newline(
|
||||
start_point: &Point,
|
||||
fn insert_extra_newline_brackets(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
) -> Option<Arc<str>> {
|
||||
let delimiters = language.line_comment_prefixes();
|
||||
let max_len_of_delimiter = delimiters.iter().map(|delimiter| delimiter.len()).max()?;
|
||||
let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
range: Range<MultiBufferOffset>,
|
||||
language: &language::LanguageScope,
|
||||
) -> bool {
|
||||
let leading_whitespace_len = buffer
|
||||
.reversed_chars_at(range.start)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let trailing_whitespace_len = buffer
|
||||
.chars_at(range.end)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
let comment_candidate = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_len_of_delimiter)
|
||||
.collect::<String>();
|
||||
let (delimiter, trimmed_len) = delimiters
|
||||
.iter()
|
||||
.filter_map(|delimiter| {
|
||||
let prefix = delimiter.trim_end();
|
||||
if comment_candidate.starts_with(prefix) {
|
||||
Some((delimiter, prefix.len()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.max_by_key(|(_, len)| *len)?;
|
||||
language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
|
||||
if let Some(BlockCommentConfig {
|
||||
start: block_start, ..
|
||||
}) = language.block_comment()
|
||||
{
|
||||
let block_start_trimmed = block_start.trim_end();
|
||||
if block_start_trimmed.starts_with(delimiter.trim_end()) {
|
||||
let line_content = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(num_of_whitespaces)
|
||||
.take(block_start_trimmed.len())
|
||||
.collect::<String>();
|
||||
|
||||
if line_content.starts_with(block_start_trimmed) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_is_placed_after_comment_marker =
|
||||
num_of_whitespaces + trimmed_len <= start_point.column as usize;
|
||||
if cursor_is_placed_after_comment_marker {
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer.contains_str_at(range.end, pair_end)
|
||||
&& buffer.contains_str_at(
|
||||
range.start.saturating_sub_usize(pair_start.len()),
|
||||
pair_start,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn documentation_delimiter_for_newline(
|
||||
start_point: &Point,
|
||||
fn insert_extra_newline_tree_sitter(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
newline_formatting: &mut NewlineFormatting,
|
||||
) -> Option<Arc<str>> {
|
||||
let BlockCommentConfig {
|
||||
start: start_tag,
|
||||
end: end_tag,
|
||||
prefix: delimiter,
|
||||
tab_size: len,
|
||||
} = language.documentation_comment()?;
|
||||
let is_within_block_comment = buffer
|
||||
.language_scope_at(*start_point)
|
||||
.is_some_and(|scope| scope.override_name() == Some("comment"));
|
||||
if !is_within_block_comment {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
|
||||
// It is safe to use a column from MultiBufferPoint in context of a single buffer ranges, because we're only ever looking at a single line at a time.
|
||||
let column = start_point.column;
|
||||
let cursor_is_after_start_tag = {
|
||||
let start_tag_len = start_tag.len();
|
||||
let start_tag_line = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(start_tag_len)
|
||||
.collect::<String>();
|
||||
if start_tag_line.starts_with(start_tag.as_ref()) {
|
||||
num_of_whitespaces + start_tag_len <= column as usize
|
||||
} else {
|
||||
false
|
||||
}
|
||||
range: Range<MultiBufferOffset>,
|
||||
) -> bool {
|
||||
let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() {
|
||||
[(buffer, range, _)] => (*buffer, range.clone()),
|
||||
_ => return false,
|
||||
};
|
||||
let pair = {
|
||||
let mut result: Option<BracketMatch<usize>> = None;
|
||||
|
||||
let cursor_is_after_delimiter = {
|
||||
let delimiter_trim = delimiter.trim_end();
|
||||
let delimiter_line = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(delimiter_trim.len())
|
||||
.collect::<String>();
|
||||
if delimiter_line.starts_with(delimiter_trim) {
|
||||
num_of_whitespaces + delimiter_trim.len() <= column as usize
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
for pair in buffer
|
||||
.all_bracket_ranges(range.start.0..range.end.0)
|
||||
.filter(move |pair| {
|
||||
pair.open_range.start <= range.start.0 && pair.close_range.end >= range.end.0
|
||||
})
|
||||
{
|
||||
let len = pair.close_range.end - pair.open_range.start;
|
||||
|
||||
let cursor_is_before_end_tag_if_exists = {
|
||||
let mut char_position = 0u32;
|
||||
let mut end_tag_offset = None;
|
||||
|
||||
'outer: for chunk in snapshot.text_for_range(range) {
|
||||
if let Some(byte_pos) = chunk.find(&**end_tag) {
|
||||
let chars_before_match = chunk[..byte_pos].chars().count() as u32;
|
||||
end_tag_offset = Some(char_position + chars_before_match);
|
||||
break 'outer;
|
||||
}
|
||||
char_position += chunk.chars().count() as u32;
|
||||
}
|
||||
|
||||
if let Some(end_tag_offset) = end_tag_offset {
|
||||
let cursor_is_before_end_tag = column <= end_tag_offset;
|
||||
if cursor_is_after_start_tag {
|
||||
if cursor_is_before_end_tag {
|
||||
newline_formatting.insert_extra_newline = true;
|
||||
}
|
||||
let cursor_is_at_start_of_end_tag = column == end_tag_offset;
|
||||
if cursor_is_at_start_of_end_tag {
|
||||
newline_formatting.indent_on_extra_newline.len = *len;
|
||||
if let Some(existing) = &result {
|
||||
let existing_len = existing.close_range.end - existing.open_range.start;
|
||||
if len > existing_len {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
cursor_is_before_end_tag
|
||||
} else {
|
||||
true
|
||||
|
||||
result = Some(pair);
|
||||
}
|
||||
|
||||
result
|
||||
};
|
||||
|
||||
if (cursor_is_after_start_tag || cursor_is_after_delimiter)
|
||||
&& cursor_is_before_end_tag_if_exists
|
||||
{
|
||||
if cursor_is_after_start_tag {
|
||||
newline_formatting.indent_on_newline.len = *len;
|
||||
}
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct NewlineFormatting {
|
||||
insert_extra_newline: bool,
|
||||
indent_on_newline: IndentSize,
|
||||
indent_on_extra_newline: IndentSize,
|
||||
}
|
||||
|
||||
impl NewlineFormatting {
|
||||
fn new(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
language: &LanguageScope,
|
||||
) -> Self {
|
||||
Self {
|
||||
insert_extra_newline: Self::insert_extra_newline_brackets(
|
||||
buffer,
|
||||
range.clone(),
|
||||
language,
|
||||
) || Self::insert_extra_newline_tree_sitter(buffer, range),
|
||||
indent_on_newline: IndentSize::spaces(0),
|
||||
indent_on_extra_newline: IndentSize::spaces(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_extra_newline_brackets(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
language: &language::LanguageScope,
|
||||
) -> bool {
|
||||
let leading_whitespace_len = buffer
|
||||
.reversed_chars_at(range.start)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let trailing_whitespace_len = buffer
|
||||
.chars_at(range.end)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len;
|
||||
|
||||
language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer.contains_str_at(range.end, pair_end)
|
||||
&& buffer.contains_str_at(
|
||||
range.start.saturating_sub_usize(pair_start.len()),
|
||||
pair_start,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_extra_newline_tree_sitter(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
) -> bool {
|
||||
let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() {
|
||||
[(buffer, range, _)] => (*buffer, range.clone()),
|
||||
_ => return false,
|
||||
};
|
||||
let pair = {
|
||||
let mut result: Option<BracketMatch<usize>> = None;
|
||||
|
||||
for pair in buffer
|
||||
.all_bracket_ranges(range.start.0..range.end.0)
|
||||
.filter(move |pair| {
|
||||
pair.open_range.start <= range.start.0 && pair.close_range.end >= range.end.0
|
||||
})
|
||||
{
|
||||
let len = pair.close_range.end - pair.open_range.start;
|
||||
|
||||
if let Some(existing) = &result {
|
||||
let existing_len = existing.close_range.end - existing.open_range.start;
|
||||
if len > existing_len {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result = Some(pair);
|
||||
}
|
||||
|
||||
result
|
||||
};
|
||||
let Some(pair) = pair else {
|
||||
return false;
|
||||
};
|
||||
pair.newline_only
|
||||
&& buffer
|
||||
.chars_for_range(pair.open_range.end..range.start.0)
|
||||
.chain(buffer.chars_for_range(range.end.0..pair.close_range.start))
|
||||
.all(|c| c.is_whitespace() && c != '\n')
|
||||
}
|
||||
let Some(pair) = pair else {
|
||||
return false;
|
||||
};
|
||||
pair.newline_only
|
||||
&& buffer
|
||||
.chars_for_range(pair.open_range.end..range.start.0)
|
||||
.chain(buffer.chars_for_range(range.end.0..pair.close_range.start))
|
||||
.all(|c| c.is_whitespace() && c != '\n')
|
||||
}
|
||||
|
||||
fn update_uncommitted_diff_for_buffer(
|
||||
@@ -25909,7 +25911,7 @@ impl BreakpointPromptEditor {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.remove_blocks(self.block_ids.clone(), None, cx);
|
||||
window.focus(&editor.focus_handle, cx);
|
||||
window.focus(&editor.focus_handle);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -41,16 +41,14 @@ use multi_buffer::{
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::{
|
||||
FakeFs, Project,
|
||||
FakeFs,
|
||||
debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
|
||||
project_settings::LspSettings,
|
||||
trusted_worktrees::{PathTrust, TrustedWorktrees},
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use settings::{
|
||||
AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
|
||||
IndentGuideColoring, InlayHintSettingsContent, ProjectSettingsContent, SearchSettingsContent,
|
||||
SettingsStore,
|
||||
IndentGuideColoring, ProjectSettingsContent, SearchSettingsContent,
|
||||
};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
|
||||
use std::{
|
||||
@@ -69,6 +67,7 @@ use util::{
|
||||
use workspace::{
|
||||
CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
|
||||
OpenOptions, ViewId,
|
||||
invalid_item_view::InvalidItemView,
|
||||
item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
|
||||
register_project_item,
|
||||
};
|
||||
@@ -18200,7 +18199,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
editor_handle.update_in(cx, |editor, window, cx| {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
|
||||
});
|
||||
@@ -20880,36 +20879,6 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_up(&MoveUp, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
|
||||
});
|
||||
cx.assert_state_with_diff(
|
||||
indoc! { "
|
||||
ˇone
|
||||
- two
|
||||
three
|
||||
five
|
||||
"}
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
|
||||
});
|
||||
cx.assert_state_with_diff(
|
||||
indoc! { "
|
||||
one
|
||||
- two
|
||||
ˇthree
|
||||
- four
|
||||
five
|
||||
"}
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.set_state(indoc! { "
|
||||
one
|
||||
ˇTWO
|
||||
@@ -20949,66 +20918,6 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggling_adjacent_diff_hunks_2(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let diff_base = r#"
|
||||
lineA
|
||||
lineB
|
||||
lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
cx.set_state(
|
||||
&r#"
|
||||
ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.set_head_text(&diff_base);
|
||||
executor.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
- lineA
|
||||
+ ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.move_right(&MoveRight, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
- lineA
|
||||
+ lineA1
|
||||
lˇineB
|
||||
- lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edits_around_expanded_deletion_hunks(
|
||||
executor: BackgroundExecutor,
|
||||
@@ -25669,7 +25578,6 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestApp
|
||||
ˇ log('for else')
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
ˇfor item in items:
|
||||
@@ -25689,7 +25597,6 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestApp
|
||||
// test relative indent is preserved when tab
|
||||
// for `if`, `elif`, `else`, `while`, `with` and `for`
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
ˇfor item in items:
|
||||
@@ -25723,7 +25630,6 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestApp
|
||||
ˇ return 0
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
ˇtry:
|
||||
@@ -25740,7 +25646,6 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestApp
|
||||
// test relative indent is preserved when tab
|
||||
// for `try`, `except`, `else`, `finally`, `match` and `def`
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
ˇtry:
|
||||
@@ -25774,7 +25679,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("else:", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
if i == 2:
|
||||
@@ -25792,7 +25696,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("except:", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
@@ -25812,7 +25715,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("else:", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
@@ -25836,7 +25738,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("finally:", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
@@ -25861,7 +25762,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("else:", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
@@ -25887,7 +25787,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("finally:", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
@@ -25913,7 +25812,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("except:", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
@@ -25937,7 +25835,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("except:", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
try:
|
||||
@@ -25959,7 +25856,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("else:", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def main():
|
||||
for i in range(10):
|
||||
@@ -25976,7 +25872,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("a", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
def f() -> list[str]:
|
||||
aˇ
|
||||
@@ -25990,7 +25885,6 @@ async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(":", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
match 1:
|
||||
case:ˇ
|
||||
@@ -26014,7 +25908,6 @@ async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
# COMMENT:
|
||||
ˇ
|
||||
@@ -26027,7 +25920,7 @@ async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
{
|
||||
ˇ
|
||||
@@ -26061,48 +25954,6 @@ async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
|
||||
let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
|
||||
language_registry.add(markdown_lang());
|
||||
language_registry.add(python_lang);
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language_registry(language_registry);
|
||||
buffer.set_language(Some(markdown_lang()), cx);
|
||||
});
|
||||
|
||||
// Test that `else:` correctly outdents to match `if:` inside the Python code block
|
||||
cx.set_state(indoc! {"
|
||||
# Heading
|
||||
|
||||
```python
|
||||
def main():
|
||||
if condition:
|
||||
pass
|
||||
ˇ
|
||||
```
|
||||
"});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("else:", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
# Heading
|
||||
|
||||
```python
|
||||
def main():
|
||||
if condition:
|
||||
pass
|
||||
else:ˇ
|
||||
```
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -26129,7 +25980,6 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppCo
|
||||
ˇ}
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
function main() {
|
||||
ˇfor item in $items; do
|
||||
@@ -26147,7 +25997,6 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppCo
|
||||
"});
|
||||
// test relative indent is preserved when tab
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
function main() {
|
||||
ˇfor item in $items; do
|
||||
@@ -26182,7 +26031,6 @@ async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppCo
|
||||
ˇ}
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
function handle() {
|
||||
ˇcase \"$1\" in
|
||||
@@ -26225,7 +26073,6 @@ async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
|
||||
ˇ}
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
function main() {
|
||||
#ˇ for item in $items; do
|
||||
@@ -26260,7 +26107,6 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("else", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
if [ \"$1\" = \"test\" ]; then
|
||||
echo \"foo bar\"
|
||||
@@ -26276,7 +26122,6 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("elif", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
if [ \"$1\" = \"test\" ]; then
|
||||
echo \"foo bar\"
|
||||
@@ -26294,7 +26139,6 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("fi", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
if [ \"$1\" = \"test\" ]; then
|
||||
echo \"foo bar\"
|
||||
@@ -26312,7 +26156,6 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("done", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
while read line; do
|
||||
echo \"$line\"
|
||||
@@ -26328,7 +26171,6 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("done", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
for file in *.txt; do
|
||||
cat \"$file\"
|
||||
@@ -26349,7 +26191,6 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("esac", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
case \"$1\" in
|
||||
start)
|
||||
@@ -26372,7 +26213,6 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("*)", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
case \"$1\" in
|
||||
start)
|
||||
@@ -26392,7 +26232,6 @@ async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("fi", window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
if [ \"$1\" = \"test\" ]; then
|
||||
echo \"outer if\"
|
||||
@@ -26419,7 +26258,6 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
# COMMENT:
|
||||
ˇ
|
||||
@@ -26433,7 +26271,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
if [ \"$1\" = \"test\" ]; then
|
||||
@@ -26448,7 +26286,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
if [ \"$1\" = \"test\" ]; then
|
||||
else
|
||||
@@ -26463,7 +26301,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
if [ \"$1\" = \"test\" ]; then
|
||||
elif
|
||||
@@ -26477,7 +26315,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
for file in *.txt; do
|
||||
ˇ
|
||||
@@ -26491,7 +26329,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
case \"$1\" in
|
||||
start)
|
||||
@@ -26508,7 +26346,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
case \"$1\" in
|
||||
start)
|
||||
@@ -26524,7 +26362,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
function test() {
|
||||
ˇ
|
||||
@@ -26538,7 +26376,7 @@ async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
echo \"test\";
|
||||
ˇ
|
||||
@@ -27756,10 +27594,11 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
// The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
|
||||
// Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
|
||||
// With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
|
||||
assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
|
||||
|
||||
assert_eq!(
|
||||
handle.to_any_view().entity_type(),
|
||||
TypeId::of::<InvalidItemView>()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -29498,200 +29337,188 @@ async fn test_find_references_single_case(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_local_worktree_trust(cx: &mut TestAppContext) {
|
||||
async fn test_toggle_selected_diff_hunks_neighbor(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), None, None, cx));
|
||||
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.all_languages.defaults.inlay_hints =
|
||||
Some(InlayHintSettingsContent {
|
||||
enabled: Some(true),
|
||||
..InlayHintSettingsContent::default()
|
||||
});
|
||||
});
|
||||
});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let diff_base = r#"
|
||||
line0
|
||||
lineA
|
||||
lineB
|
||||
lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
cx.set_state(
|
||||
&r#"
|
||||
line0
|
||||
ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.set_head_text(&diff_base);
|
||||
executor.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".zed": {
|
||||
"settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
|
||||
},
|
||||
"main.rs": "fn main() {}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
|
||||
let server_name = "override-rust-analyzer";
|
||||
let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(rust_lang());
|
||||
|
||||
let capabilities = lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
let mut fake_language_servers = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: server_name,
|
||||
capabilities,
|
||||
initializer: Some(Box::new({
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
move |fake_server| {
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
|
||||
move |_params, _| {
|
||||
lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
|
||||
async move {
|
||||
Ok(Some(vec![lsp::InlayHint {
|
||||
position: lsp::Position::new(0, 0),
|
||||
label: lsp::InlayHintLabel::String("hint".to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
}]))
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
line0
|
||||
- lineA
|
||||
+ ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let worktree_id = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.map(|wt| wt.read(cx).id())
|
||||
.expect("should have a worktree")
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.move_right(&MoveRight, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
|
||||
let trusted_worktrees =
|
||||
cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
|
||||
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
|
||||
assert!(!can_trust, "worktree should be restricted initially");
|
||||
|
||||
let buffer_before_approval = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, rel_path("main.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::full(),
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let fake_language_server = fake_language_servers.next();
|
||||
|
||||
cx.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language::language_settings::language_settings(Some("Rust".into()), file, cx)
|
||||
.language_servers,
|
||||
["...".to_string()],
|
||||
"local .zed/settings.json must not apply before trust approval"
|
||||
)
|
||||
});
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(1));
|
||||
assert_eq!(
|
||||
lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
|
||||
0,
|
||||
"inlay hints must not be queried before trust approval"
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
line0
|
||||
- lineA
|
||||
+ lineA1
|
||||
lˇineB
|
||||
- lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language::language_settings::language_settings(Some("Rust".into()), file, cx)
|
||||
.language_servers,
|
||||
["override-rust-analyzer".to_string()],
|
||||
"local .zed/settings.json should apply after trust approval"
|
||||
)
|
||||
});
|
||||
let _fake_language_server = fake_language_server.await.unwrap();
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.executor()
|
||||
.advance_clock(std::time::Duration::from_secs(1));
|
||||
assert!(
|
||||
lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
|
||||
"inlay hints should be queried after trust approval"
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
line0
|
||||
- lineA
|
||||
+ lineA1
|
||||
lˇineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
let can_trust_after =
|
||||
trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
|
||||
assert!(can_trust_after, "worktree should be trusted after trust()");
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_up(&MoveUp, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
line0
|
||||
lˇineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
|
||||
// This test reproduces a bug where drawing an editor at a position above the viewport
|
||||
// (simulating what happens when an AutoHeight editor inside a List is scrolled past)
|
||||
// causes an infinite loop in blocks_in_range.
|
||||
//
|
||||
// The issue: when the editor's bounds.origin.y is very negative (above the viewport),
|
||||
// the content mask intersection produces visible_bounds with origin at the viewport top.
|
||||
// This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
|
||||
// When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
|
||||
// but the while loop after seek never terminates because cursor.next() is a no-op at end.
|
||||
async fn test_toggle_selected_diff_hunks_neighbor_edge_case(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let window = cx.add_window(|_, _| gpui::Empty);
|
||||
let mut cx = VisualTestContext::from_window(*window, cx);
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
|
||||
let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
|
||||
let diff_base = r#"
|
||||
line0
|
||||
lineA
|
||||
lineB
|
||||
lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
// Simulate a small viewport (500x500 pixels at origin 0,0)
|
||||
cx.simulate_resize(gpui::size(px(500.), px(500.)));
|
||||
cx.set_state(
|
||||
&r#"
|
||||
line0
|
||||
ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.set_head_text(&diff_base);
|
||||
executor.run_until_parked();
|
||||
|
||||
// Draw the editor at a very negative Y position, simulating an editor that's been
|
||||
// scrolled way above the visible viewport (like in a List that has scrolled past it).
|
||||
// The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
|
||||
// This should NOT hang - it should just render nothing.
|
||||
cx.draw(
|
||||
gpui::point(px(0.), px(-10000.)),
|
||||
gpui::size(px(500.), px(3000.)),
|
||||
|_, _| editor.clone(),
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
line0
|
||||
- lineA
|
||||
+ ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
// If we get here without hanging, the test passes
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
line0
|
||||
- lineA
|
||||
+ lineA1
|
||||
ˇlineB
|
||||
- lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
line0
|
||||
- lineA
|
||||
+ lineA1
|
||||
ˇlineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_up(&MoveUp, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
line0
|
||||
ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ use crate::{
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use file_icons::FileIcons;
|
||||
use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
|
||||
use git::{
|
||||
Oid,
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
status::FileStatus,
|
||||
};
|
||||
use gpui::{
|
||||
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
|
||||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||
@@ -590,6 +594,8 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::show_signature_help);
|
||||
register_action(editor, window, Editor::signature_help_prev);
|
||||
register_action(editor, window, Editor::signature_help_next);
|
||||
register_action(editor, window, Editor::next_edit_prediction);
|
||||
register_action(editor, window, Editor::previous_edit_prediction);
|
||||
register_action(editor, window, Editor::show_edit_prediction);
|
||||
register_action(editor, window, Editor::context_menu_first);
|
||||
register_action(editor, window, Editor::context_menu_prev);
|
||||
@@ -9225,11 +9231,9 @@ impl Element for EditorElement {
|
||||
// the top of the window in terms of display rows.
|
||||
// We add clipped_top_in_lines to skip rows that are clipped by parent containers,
|
||||
// but we don't modify scroll_position itself since the parent handles positioning.
|
||||
let start_row =
|
||||
DisplayRow((scroll_position.y + clipped_top_in_lines).floor() as u32);
|
||||
let max_row = snapshot.max_point().row();
|
||||
let start_row = cmp::min(
|
||||
DisplayRow((scroll_position.y + clipped_top_in_lines).floor() as u32),
|
||||
max_row,
|
||||
);
|
||||
let end_row = cmp::min(
|
||||
(scroll_position.y + clipped_top_in_lines + visible_height_in_lines).ceil()
|
||||
as u32,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user