Compare commits

..

1 Commits

Author SHA1 Message Date
Jakub Konka
350e355b15 editor: Toggle diff hunks WIP 2025-12-17 15:04:30 +01:00
319 changed files with 4446 additions and 16822 deletions

View File

@@ -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}

View File

@@ -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 }}

View File

@@ -34,7 +34,6 @@ jobs:
CharlesChen0823
chbk
cppcoffee
davidbarsky
davewa
ddoemonn
djsauble

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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 . .

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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}}.

View File

@@ -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",

View File

@@ -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,
);

View File

@@ -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")]

View File

@@ -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(&params.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)
}
}
}

View File

@@ -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)?

View File

@@ -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"
);
});
}

View File

@@ -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;
};

View File

@@ -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)
})
}

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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)
});

View File

@@ -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![

View File

@@ -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,
)

View File

@@ -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(&registry, 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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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);
}
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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(),

View File

@@ -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,
})
}
}

View File

@@ -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<_>>()

View File

@@ -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);
}
});
}

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -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"],
);
}
}

View File

@@ -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()
}

View File

@@ -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()

View File

@@ -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(

View File

@@ -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(

View File

@@ -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),
)
}))
}
}

View File

@@ -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()

View File

@@ -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"] }

View File

@@ -1 +1 @@
../../LICENSE-GPL
LICENSE-GPL

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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(

View File

@@ -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;

View File

@@ -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"
);
}

View File

@@ -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,
)
})

View File

@@ -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();
}

View File

@@ -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,

View File

@@ -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)

View File

@@ -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"] }

View File

@@ -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(
&notification.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()
});
}
}

View File

@@ -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(

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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(),
})
}
},

View File

@@ -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";
}

View File

@@ -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.))

View File

@@ -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);

View File

@@ -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
});

View File

@@ -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()

View File

@@ -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);
}
});

View File

@@ -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)
}
}),

View File

@@ -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);
}
}),
];

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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>> {

View File

@@ -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()

View File

@@ -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>> {

View File

@@ -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);

View File

@@ -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};

View File

@@ -179,7 +179,6 @@ async fn setup_project(
app_state.languages.clone(),
app_state.fs.clone(),
None,
false,
cx,
)
})?;

View File

@@ -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))
}

View File

@@ -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;
}

View File

@@ -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
});

View File

@@ -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| {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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>) {}

View File

@@ -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();
}

View File

@@ -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]:
@@ -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(),
);
}

View File

@@ -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