Compare commits

..

1 Commits

Author SHA1 Message Date
Jakub Konka
350e355b15 editor: Toggle diff hunks WIP 2025-12-17 15:04:30 +01:00
294 changed files with 3907 additions and 14966 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

@@ -74,12 +74,6 @@ jobs:
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
@@ -478,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

@@ -77,15 +77,6 @@ jobs:
- name: ./script/prettier
run: ./script/prettier
shell: bash -euxo pipefail {0}
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=false
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: ./script/check-todos
run: ./script/check-todos
shell: bash -euxo pipefail {0}
@@ -96,6 +87,9 @@ jobs:
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
with:
config: ./typos.toml
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
timeout-minutes: 60
run_tests_windows:
needs:
@@ -166,12 +160,6 @@ jobs:
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large

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>

396
Cargo.lock generated
View File

@@ -111,15 +111,6 @@ dependencies = [
"workspace",
]
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli 0.31.1",
]
[[package]]
name = "addr2line"
version = "0.25.1"
@@ -301,7 +292,6 @@ dependencies = [
name = "agent_settings"
version = "0.1.0"
dependencies = [
"agent-client-protocol",
"anyhow",
"cloud_llm_client",
"collections",
@@ -1441,9 +1431,9 @@ dependencies = [
[[package]]
name = "aws-config"
version = "1.8.10"
version = "1.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1856b1b48b65f71a4dd940b1c0931f9a7b646d4a924b9828ffefc1454714668a"
checksum = "37cf2b6af2a95a20e266782b4f76f1a5e12bf412a9db2de9c1e9123b9d8c0ad8"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1507,9 +1497,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
version = "1.5.13"
version = "1.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f2402da1a5e16868ba98725e5d73f26b8116eaa892e56f2cd0bf5eec7985f70"
checksum = "bfa006bb32360ed90ac51203feafb9d02e3d21046e1fd3a450a404b90ea73e5d"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@@ -1532,9 +1522,9 @@ dependencies = [
[[package]]
name = "aws-sdk-bedrockruntime"
version = "1.112.0"
version = "1.109.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06c037e6823696d752702ec2bad758d3cf95d1b92b712c8ac7e93824b5e2391"
checksum = "fbfdfd941dcb253c17bf70baddbf1e5b22f19e29d313d2e049bad4b1dadb2011"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1614,9 +1604,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.88.0"
version = "1.86.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05b276777560aa9a196dbba2e3aada4d8006d3d7eeb3ba7fe0c317227d933c4"
checksum = "4a0abbfab841446cce6e87af853a3ba2cc1bc9afcd3f3550dd556c43d434c86d"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1636,9 +1626,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.90.0"
version = "1.88.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9be14d6d9cd761fac3fd234a0f47f7ed6c0df62d83c0eeb7012750e4732879b"
checksum = "9a68d675582afea0e94d38b6ca9c5aaae4ca14f1d36faa6edb19b42e687e70d7"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1658,9 +1648,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.90.0"
version = "1.88.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98a862d704c817d865c8740b62d8bbeb5adcb30965e93b471df8a5bcefa20a80"
checksum = "d30990923f4f675523c51eb1c0dec9b752fb267b36a61e83cbc219c9d86da715"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1681,9 +1671,9 @@ dependencies = [
[[package]]
name = "aws-sigv4"
version = "1.3.6"
version = "1.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c35452ec3f001e1f2f6db107b6373f1f48f05ec63ba2c5c9fa91f07dad32af11"
checksum = "bffc03068fbb9c8dd5ce1c6fb240678a5cffb86fb2b7b1985c999c4b83c8df68"
dependencies = [
"aws-credential-types",
"aws-smithy-eventstream",
@@ -1740,9 +1730,9 @@ dependencies = [
[[package]]
name = "aws-smithy-eventstream"
version = "0.60.13"
version = "0.60.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e29a304f8319781a39808847efb39561351b1bb76e933da7aa90232673638658"
checksum = "9656b85088f8d9dc7ad40f9a6c7228e1e8447cdf4b046c87e152e0805dea02fa"
dependencies = [
"aws-smithy-types",
"bytes 1.10.1",
@@ -1751,9 +1741,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http"
version = "0.62.5"
version = "0.62.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445d5d720c99eed0b4aa674ed00d835d9b1427dd73e04adaf2f94c6b2d6f9fca"
checksum = "3feafd437c763db26aa04e0cc7591185d0961e64c61885bece0fb9d50ceac671"
dependencies = [
"aws-smithy-eventstream",
"aws-smithy-runtime-api",
@@ -1761,7 +1751,6 @@ dependencies = [
"bytes 1.10.1",
"bytes-utils",
"futures-core",
"futures-util",
"http 0.2.12",
"http 1.3.1",
"http-body 0.4.6",
@@ -1773,9 +1762,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http-client"
version = "1.1.4"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "623254723e8dfd535f566ee7b2381645f8981da086b5c4aa26c0c41582bb1d2c"
checksum = "1053b5e587e6fa40ce5a79ea27957b04ba660baa02b28b7436f64850152234f1"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -1803,9 +1792,9 @@ dependencies = [
[[package]]
name = "aws-smithy-json"
version = "0.61.7"
version = "0.61.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2db31f727935fc63c6eeae8b37b438847639ec330a9161ece694efba257e0c54"
checksum = "cff418fc8ec5cadf8173b10125f05c2e7e1d46771406187b2c878557d4503390"
dependencies = [
"aws-smithy-types",
]
@@ -1831,9 +1820,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.9.4"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bbe9d018d646b96c7be063dd07987849862b0e6d07c778aad7d93d1be6c1ef0"
checksum = "40ab99739082da5347660c556689256438defae3bcefd66c52b095905730e404"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -1855,9 +1844,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.9.2"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7204f9fd94749a7c53b26da1b961b4ac36bf070ef1e0b94bb09f79d4f6c193"
checksum = "3683c5b152d2ad753607179ed71988e8cfd52964443b4f74fd8e552d0bbfeb46"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@@ -1872,9 +1861,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.3.4"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f535879a207fce0db74b679cfc3e91a3159c8144d717d55f5832aea9eef46e"
checksum = "9f5b3a7486f6690ba25952cabf1e7d75e34d69eaff5081904a47bc79074d6457"
dependencies = [
"base64-simd",
"bytes 1.10.1",
@@ -1898,18 +1887,18 @@ dependencies = [
[[package]]
name = "aws-smithy-xml"
version = "0.60.12"
version = "0.60.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab77cdd036b11056d2a30a7af7b775789fb024bf216acc13884c6c97752ae56"
checksum = "e9c34127e8c624bc2999f3b657e749c1393bedc9cd97b92a804db8ced4d2e163"
dependencies = [
"xmlparser",
]
[[package]]
name = "aws-types"
version = "1.3.10"
version = "1.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d79fb68e3d7fe5d4833ea34dc87d2e97d26d3086cb3da660bb6b1f76d98680b6"
checksum = "e2fd329bf0e901ff3f60425691410c69094dc2a1f34b331f37bfc4e9ac1565a1"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
@@ -2008,7 +1997,7 @@ version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
dependencies = [
"addr2line 0.25.1",
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
@@ -2896,17 +2885,6 @@ dependencies = [
"util",
]
[[package]]
name = "chardetng"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
dependencies = [
"cfg-if",
"encoding_rs",
"memchr",
]
[[package]]
name = "chrono"
version = "0.4.42"
@@ -3635,7 +3613,6 @@ dependencies = [
"serde",
"serde_json",
"settings",
"slotmap",
"smol",
"tempfile",
"terminal",
@@ -3947,38 +3924,20 @@ dependencies = [
"libc",
]
[[package]]
name = "cranelift-assembler-x64"
version = "0.120.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5023e06632d8f351c2891793ccccfe4aef957954904392434038745fb6f1f68"
dependencies = [
"cranelift-assembler-x64-meta",
]
[[package]]
name = "cranelift-assembler-x64-meta"
version = "0.120.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c4012b4c8c1f6eb05c0a0a540e3e1ee992631af51aa2bbb3e712903ce4fd65"
dependencies = [
"cranelift-srcgen",
]
[[package]]
name = "cranelift-bforest"
version = "0.120.2"
version = "0.116.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d6d883b4942ef3a7104096b8bc6f2d1a41393f159ac8de12aed27b25d67f895"
checksum = "e15d04a0ce86cb36ead88ad68cf693ffd6cda47052b9e0ac114bc47fd9cd23c4"
dependencies = [
"cranelift-entity",
]
[[package]]
name = "cranelift-bitset"
version = "0.120.2"
version = "0.116.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db7b2ee9eec6ca8a716d900d5264d678fb2c290c58c46c8da7f94ee268175d17"
checksum = "7c6e3969a7ce267259ce244b7867c5d3bc9e65b0a87e81039588dfdeaede9f34"
dependencies = [
"serde",
"serde_derive",
@@ -3986,12 +3945,11 @@ dependencies = [
[[package]]
name = "cranelift-codegen"
version = "0.120.2"
version = "0.116.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aeda0892577afdce1ac2e9a983a55f8c5b87a59334e1f79d8f735a2d7ba4f4b4"
checksum = "2c22032c4cb42558371cf516bb47f26cdad1819d3475c133e93c49f50ebf304e"
dependencies = [
"bumpalo",
"cranelift-assembler-x64",
"cranelift-bforest",
"cranelift-bitset",
"cranelift-codegen-meta",
@@ -4000,10 +3958,9 @@ dependencies = [
"cranelift-entity",
"cranelift-isle",
"gimli 0.31.1",
"hashbrown 0.15.5",
"hashbrown 0.14.5",
"log",
"postcard",
"pulley-interpreter",
"regalloc2",
"rustc-hash 2.1.1",
"serde",
@@ -4015,36 +3972,33 @@ dependencies = [
[[package]]
name = "cranelift-codegen-meta"
version = "0.120.2"
version = "0.116.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e461480d87f920c2787422463313326f67664e68108c14788ba1676f5edfcd15"
checksum = "c904bc71c61b27fc57827f4a1379f29de64fe95653b620a3db77d59655eee0b8"
dependencies = [
"cranelift-assembler-x64-meta",
"cranelift-codegen-shared",
"cranelift-srcgen",
"pulley-interpreter",
]
[[package]]
name = "cranelift-codegen-shared"
version = "0.120.2"
version = "0.116.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976584d09f200c6c84c4b9ff7af64fc9ad0cb64dffa5780991edd3fe143a30a1"
checksum = "40180f5497572f644ce88c255480981ae2ec1d7bb4d8e0c0136a13b87a2f2ceb"
[[package]]
name = "cranelift-control"
version = "0.120.2"
version = "0.116.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46d43d70f4e17c545aa88dbf4c84d4200755d27c6e3272ebe4de65802fa6a955"
checksum = "26d132c6d0bd8a489563472afc171759da0707804a65ece7ceb15a8c6d7dd5ef"
dependencies = [
"arbitrary",
]
[[package]]
name = "cranelift-entity"
version = "0.120.2"
version = "0.116.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75418674520cb400c8772bfd6e11a62736c78fc1b6e418195696841d1bf91f1"
checksum = "4b2d0d9618275474fbf679dd018ac6e009acbd6ae6850f6a67be33fb3b00b323"
dependencies = [
"cranelift-bitset",
"serde",
@@ -4053,9 +4007,9 @@ dependencies = [
[[package]]
name = "cranelift-frontend"
version = "0.120.2"
version = "0.116.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c8b1a91c86687a344f3c52dd6dfb6e50db0dfa7f2e9c7711b060b3623e1fdeb"
checksum = "4fac41e16729107393174b0c9e3730fb072866100e1e64e80a1a963b2e484d57"
dependencies = [
"cranelift-codegen",
"log",
@@ -4065,27 +4019,21 @@ dependencies = [
[[package]]
name = "cranelift-isle"
version = "0.120.2"
version = "0.116.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711baa4e3432d4129295b39ec2b4040cc1b558874ba0a37d08e832e857db7285"
checksum = "1ca20d576e5070044d0a72a9effc2deacf4d6aa650403189d8ea50126483944d"
[[package]]
name = "cranelift-native"
version = "0.120.2"
version = "0.116.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41c83e8666e3bcc5ffeaf6f01f356f0e1f9dcd69ce5511a1efd7ca5722001a3f"
checksum = "b8dee82f3f1f2c4cba9177f1cc5e350fe98764379bcd29340caa7b01f85076c7"
dependencies = [
"cranelift-codegen",
"libc",
"target-lexicon 0.13.3",
]
[[package]]
name = "cranelift-srcgen"
version = "0.120.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e3f4d783a55c64266d17dc67d2708852235732a100fc40dd9f1051adc64d7b"
[[package]]
name = "crash-context"
version = "0.6.3"
@@ -8808,7 +8756,6 @@ dependencies = [
"ctor",
"diffy",
"ec4rs",
"encoding_rs",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -8827,7 +8774,6 @@ dependencies = [
"regex",
"rpc",
"schemars",
"semver",
"serde",
"serde_json",
"settings",
@@ -9062,7 +9008,6 @@ dependencies = [
"regex",
"rope",
"rust-embed",
"semver",
"serde",
"serde_json",
"serde_json_lenient",
@@ -12476,8 +12421,6 @@ dependencies = [
"context_server",
"dap",
"dap_adapters",
"db",
"encoding_rs",
"extension",
"fancy-regex",
"fs",
@@ -12851,12 +12794,13 @@ checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3"
[[package]]
name = "pulley-interpreter"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "986beaef947a51d17b42b0ea18ceaa88450d35b6994737065ed505c39172db71"
checksum = "62d95f8575df49a2708398182f49a888cf9dc30210fb1fd2df87c889edcee75d"
dependencies = [
"cranelift-bitset",
"log",
"sptr",
"wasmtime-math",
]
@@ -13355,9 +13299,9 @@ dependencies = [
[[package]]
name = "regalloc2"
version = "0.12.2"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734"
checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a"
dependencies = [
"allocator-api2",
"bumpalo",
@@ -17366,9 +17310,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.26.3"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "974d205cc395652cfa8b37daa053fe56eebd429acf8dc055503fee648dae981e"
checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87"
dependencies = [
"cc",
"regex",
@@ -18455,16 +18399,6 @@ dependencies = [
"wasmparser 0.227.1",
]
[[package]]
name = "wasm-encoder"
version = "0.229.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38ba1d491ecacb085a2552025c10a675a6fddcbd03b1fc9b36c536010ce265d2"
dependencies = [
"leb128fmt",
"wasmparser 0.229.0",
]
[[package]]
name = "wasm-metadata"
version = "0.201.0"
@@ -18549,37 +18483,23 @@ dependencies = [
"semver",
]
[[package]]
name = "wasmparser"
version = "0.229.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc3b1f053f5d41aa55640a1fa9b6d1b8a9e4418d118ce308d20e24ff3575a8c"
dependencies = [
"bitflags 2.9.4",
"hashbrown 0.15.5",
"indexmap",
"semver",
"serde",
]
[[package]]
name = "wasmprinter"
version = "0.229.0"
version = "0.221.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d25dac01892684a99b8fbfaf670eb6b56edea8a096438c75392daeb83156ae2e"
checksum = "7343c42a97f2926c7819ff81b64012092ae954c5d83ddd30c9fcdefd97d0b283"
dependencies = [
"anyhow",
"termcolor",
"wasmparser 0.229.0",
"wasmparser 0.221.3",
]
[[package]]
name = "wasmtime"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57373e1d8699662fb791270ac5dfac9da5c14f618ecf940cdb29dc3ad9472a3c"
checksum = "11976a250672556d1c4c04c6d5d7656ac9192ac9edc42a4587d6c21460010e69"
dependencies = [
"addr2line 0.24.2",
"anyhow",
"async-trait",
"bitflags 2.9.4",
@@ -18587,7 +18507,7 @@ dependencies = [
"cc",
"cfg-if",
"encoding_rs",
"hashbrown 0.15.5",
"hashbrown 0.14.5",
"indexmap",
"libc",
"log",
@@ -18595,11 +18515,12 @@ dependencies = [
"memfd",
"object 0.36.7",
"once_cell",
"paste",
"postcard",
"psm",
"pulley-interpreter",
"rayon",
"rustix 1.1.2",
"rustix 0.38.44",
"semver",
"serde",
"serde_derive",
@@ -18607,7 +18528,7 @@ dependencies = [
"sptr",
"target-lexicon 0.13.3",
"trait-variant",
"wasmparser 0.229.0",
"wasmparser 0.221.3",
"wasmtime-asm-macros",
"wasmtime-component-macro",
"wasmtime-component-util",
@@ -18624,18 +18545,18 @@ dependencies = [
[[package]]
name = "wasmtime-asm-macros"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0fc91372865167a695dc98d0d6771799a388a7541d3f34e939d0539d6583de"
checksum = "1f178b0d125201fbe9f75beaf849bd3e511891f9e45ba216a5b620802ccf64f2"
dependencies = [
"cfg-if",
]
[[package]]
name = "wasmtime-c-api-impl"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46db556f1dccdd88e0672bd407162ab0036b72e5eccb0f4398d8251cba32dba1"
checksum = "ea30cef3608f2de5797c7bbb94c1ba4f3676d9a7f81ae86ced1b512e2766ed0c"
dependencies = [
"anyhow",
"log",
@@ -18646,9 +18567,9 @@ dependencies = [
[[package]]
name = "wasmtime-c-api-macros"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "315cc6bc8cdc66f296accb26d7625ae64c1c7b6da6f189e8a72ce6594bf7bd36"
checksum = "022a79ebe1124d5d384d82463d7e61c6b4dd857d81f15cb8078974eeb86db65b"
dependencies = [
"proc-macro2",
"quote",
@@ -18656,9 +18577,9 @@ dependencies = [
[[package]]
name = "wasmtime-component-macro"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25c9c7526675ff9a9794b115023c4af5128e3eb21389bfc3dc1fd344d549258f"
checksum = "d74de6592ed945d0a602f71243982a304d5d02f1e501b638addf57f42d57dfaf"
dependencies = [
"anyhow",
"proc-macro2",
@@ -18666,20 +18587,20 @@ dependencies = [
"syn 2.0.106",
"wasmtime-component-util",
"wasmtime-wit-bindgen",
"wit-parser 0.229.0",
"wit-parser 0.221.3",
]
[[package]]
name = "wasmtime-component-util"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc42ec8b078875804908d797cb4950fec781d9add9684c9026487fd8eb3f6291"
checksum = "707dc7b3c112ab5a366b30cfe2fb5b2f8e6a0f682f16df96a5ec582bfe6f056e"
[[package]]
name = "wasmtime-cranelift"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2bd72f0a6a0ffcc6a184ec86ac35c174e48ea0e97bbae277c8f15f8bf77a566"
checksum = "366be722674d4bf153290fbcbc4d7d16895cc82fb3e869f8d550ff768f9e9e87"
dependencies = [
"anyhow",
"cfg-if",
@@ -18689,23 +18610,22 @@ dependencies = [
"cranelift-frontend",
"cranelift-native",
"gimli 0.31.1",
"itertools 0.14.0",
"itertools 0.12.1",
"log",
"object 0.36.7",
"pulley-interpreter",
"smallvec",
"target-lexicon 0.13.3",
"thiserror 2.0.17",
"wasmparser 0.229.0",
"thiserror 1.0.69",
"wasmparser 0.221.3",
"wasmtime-environ",
"wasmtime-versioned-export-macros",
]
[[package]]
name = "wasmtime-environ"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6187bb108a23eb25d2a92aa65d6c89fb5ed53433a319038a2558567f3011ff2"
checksum = "cdadc1af7097347aa276a4f008929810f726b5b46946971c660b6d421e9994ad"
dependencies = [
"anyhow",
"cpp_demangle",
@@ -18722,22 +18642,22 @@ dependencies = [
"serde_derive",
"smallvec",
"target-lexicon 0.13.3",
"wasm-encoder 0.229.0",
"wasmparser 0.229.0",
"wasm-encoder 0.221.3",
"wasmparser 0.221.3",
"wasmprinter",
"wasmtime-component-util",
]
[[package]]
name = "wasmtime-fiber"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc8965d2128c012329f390e24b8b2758dd93d01bf67e1a1a0dd3d8fd72f56873"
checksum = "ccba90d4119f081bca91190485650730a617be1fff5228f8c4757ce133d21117"
dependencies = [
"anyhow",
"cc",
"cfg-if",
"rustix 1.1.2",
"rustix 0.38.44",
"wasmtime-asm-macros",
"wasmtime-versioned-export-macros",
"windows-sys 0.59.0",
@@ -18745,9 +18665,9 @@ dependencies = [
[[package]]
name = "wasmtime-jit-icache-coherence"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7af0e940cb062a45c0b3f01a926f77da5947149e99beb4e3dd9846d5b8f11619"
checksum = "ec5e8552e01692e6c2e5293171704fed8abdec79d1a6995a0870ab190e5747d1"
dependencies = [
"anyhow",
"cfg-if",
@@ -18757,24 +18677,24 @@ dependencies = [
[[package]]
name = "wasmtime-math"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acfca360e719dda9a27e26944f2754ff2fd5bad88e21919c42c5a5f38ddd93cb"
checksum = "29210ec2aa25e00f4d54605cedaf080f39ec01a872c5bd520ad04c67af1dde17"
dependencies = [
"libm",
]
[[package]]
name = "wasmtime-slab"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e240559cada55c4b24af979d5f6c95e0029f5772f32027ec3c62b258aaff65"
checksum = "fcb5821a96fa04ac14bc7b158bb3d5cd7729a053db5a74dad396cd513a5e5ccf"
[[package]]
name = "wasmtime-versioned-export-macros"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0963c1438357a3d8c0efe152b4ef5259846c1cf8b864340270744fe5b3bae5e"
checksum = "86ff86db216dc0240462de40c8290887a613dddf9685508eb39479037ba97b5b"
dependencies = [
"proc-macro2",
"quote",
@@ -18783,9 +18703,9 @@ dependencies = [
[[package]]
name = "wasmtime-wasi"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae951b72c7c6749a1c15dcdfb6d940a2614c932b4a54f474636e78e2c744b4c"
checksum = "8d1be69bfcab1bdac74daa7a1f9695ab992b9c8e21b9b061e7d66434097e0ca4"
dependencies = [
"anyhow",
"async-trait",
@@ -18800,43 +18720,30 @@ dependencies = [
"futures 0.3.31",
"io-extras",
"io-lifetimes",
"rustix 1.1.2",
"rustix 0.38.44",
"system-interface",
"thiserror 2.0.17",
"thiserror 1.0.69",
"tokio",
"tracing",
"trait-variant",
"url",
"wasmtime",
"wasmtime-wasi-io",
"wiggle",
"windows-sys 0.59.0",
]
[[package]]
name = "wasmtime-wasi-io"
version = "33.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a835790dcecc3d7051ec67da52ba9e04af25e1bc204275b9391e3f0042b10797"
dependencies = [
"anyhow",
"async-trait",
"bytes 1.10.1",
"futures 0.3.31",
"wasmtime",
]
[[package]]
name = "wasmtime-winch"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc3b117d03d6eeabfa005a880c5c22c06503bb8820f3aa2e30f0e8d87b6752f"
checksum = "fdbabfb8f20502d5e1d81092b9ead3682ae59988487aafcd7567387b7a43cf8f"
dependencies = [
"anyhow",
"cranelift-codegen",
"gimli 0.31.1",
"object 0.36.7",
"target-lexicon 0.13.3",
"wasmparser 0.229.0",
"wasmparser 0.221.3",
"wasmtime-cranelift",
"wasmtime-environ",
"winch-codegen",
@@ -18844,14 +18751,14 @@ dependencies = [
[[package]]
name = "wasmtime-wit-bindgen"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1382f4f09390eab0d75d4994d0c3b0f6279f86a571807ec67a8253c87cf6a145"
checksum = "8358319c2dd1e4db79e3c1c5d3a5af84956615343f9f89f4e4996a36816e06e6"
dependencies = [
"anyhow",
"heck 0.5.0",
"indexmap",
"wit-parser 0.229.0",
"wit-parser 0.221.3",
]
[[package]]
@@ -19133,20 +19040,6 @@ dependencies = [
"winsafe",
]
[[package]]
name = "which_key"
version = "0.1.0"
dependencies = [
"command_palette",
"gpui",
"serde",
"settings",
"theme",
"ui",
"util",
"workspace",
]
[[package]]
name = "whoami"
version = "1.6.1"
@@ -19159,14 +19052,14 @@ dependencies = [
[[package]]
name = "wiggle"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "649c1aca13ef9e9dccf2d5efbbebf12025bc5521c3fb7754355ef60f5eb810be"
checksum = "4b9af35bc9629c52c261465320a9a07959164928b4241980ba1cf923b9e6751d"
dependencies = [
"anyhow",
"async-trait",
"bitflags 2.9.4",
"thiserror 2.0.17",
"thiserror 1.0.69",
"tracing",
"wasmtime",
"wiggle-macro",
@@ -19174,23 +19067,24 @@ dependencies = [
[[package]]
name = "wiggle-generate"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "164870fc34214ee42bd81b8ce9e7c179800fa1a7d4046d17a84e7f7bf422c8ad"
checksum = "2cf267dd05673912c8138f4b54acabe6bd53407d9d1536f0fadb6520dd16e101"
dependencies = [
"anyhow",
"heck 0.5.0",
"proc-macro2",
"quote",
"shellexpand 2.1.2",
"syn 2.0.106",
"witx",
]
[[package]]
name = "wiggle-macro"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d873bb5b59ca703b5e41562e96a4796d1af61bf4cf80bf8a7abda755a380ec1c"
checksum = "08c5c473d4198e6c2d377f3809f713ff0c110cab88a0805ae099a82119ee250c"
dependencies = [
"proc-macro2",
"quote",
@@ -19231,19 +19125,18 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "winch-codegen"
version = "33.0.2"
version = "29.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7914c296fbcef59d1b89a15e82384d34dc9669bc09763f2ef068a28dd3a64ebf"
checksum = "2f849ef2c5f46cb0a20af4b4487aaa239846e52e2c03f13fa3c784684552859c"
dependencies = [
"anyhow",
"cranelift-assembler-x64",
"cranelift-codegen",
"gimli 0.31.1",
"regalloc2",
"smallvec",
"target-lexicon 0.13.3",
"thiserror 2.0.17",
"wasmparser 0.229.0",
"thiserror 1.0.69",
"wasmparser 0.221.3",
"wasmtime-cranelift",
"wasmtime-environ",
]
@@ -20139,6 +20032,24 @@ dependencies = [
"wasmparser 0.201.0",
]
[[package]]
name = "wit-parser"
version = "0.221.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "896112579ed56b4a538b07a3d16e562d101ff6265c46b515ce0c701eef16b2ac"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser 0.221.3",
]
[[package]]
name = "wit-parser"
version = "0.227.1"
@@ -20157,24 +20068,6 @@ dependencies = [
"wasmparser 0.227.1",
]
[[package]]
name = "wit-parser"
version = "0.229.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459c6ba62bf511d6b5f2a845a2a736822e38059c1cfa0b644b467bbbfae4efa6"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser 0.229.0",
]
[[package]]
name = "witx"
version = "0.9.1"
@@ -20244,10 +20137,8 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-lock 2.8.0",
"chardetng",
"clock",
"collections",
"encoding_rs",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -20616,7 +20507,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.219.0"
version = "0.218.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -20759,7 +20650,6 @@ dependencies = [
"watch",
"web_search",
"web_search_providers",
"which_key",
"windows 0.61.3",
"winresource",
"workspace",

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" }
@@ -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",
},
},
{
@@ -252,7 +251,6 @@
"ctrl-y": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"ctrl-alt-z": "agent::RejectOnce",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -264,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",
},
},
{
@@ -347,7 +345,6 @@
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -903,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,7 +265,6 @@
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-k l": "agent::OpenRulesLibrary",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -293,7 +291,6 @@
"cmd-y": "agent::AllowOnce",
"cmd-alt-y": "agent::AllowAlways",
"cmd-alt-z": "agent::RejectOnce",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -306,7 +303,7 @@
"context": "AgentPanel > Markdown",
"use_key_equivalents": true,
"bindings": {
"cmd-c": "markdown::CopyAsMarkdown",
"cmd-c": "markdown::Copy",
},
},
{
@@ -388,7 +385,6 @@
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -400,7 +396,6 @@
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -884,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",
@@ -981,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",
},
},
{
@@ -253,7 +252,6 @@
"shift-alt-a": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"shift-alt-z": "agent::RejectOnce",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -267,7 +265,7 @@
"context": "AgentPanel > Markdown",
"use_key_equivalents": true,
"bindings": {
"ctrl-c": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::Copy",
},
},
{
@@ -343,7 +341,6 @@
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -355,7 +352,6 @@
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -908,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),
@@ -1194,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);
@@ -1214,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);
@@ -1232,7 +1210,6 @@ impl AcpThread {
content,
chunks: vec![chunk],
checkpoint: None,
indented,
}),
cx,
);
@@ -1244,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));
@@ -1292,7 +1255,6 @@ impl AcpThread {
self.push_entry(
AgentThreadEntry::AssistantMessage(AssistantMessage {
chunks: vec![chunk],
indented,
}),
cx,
);
@@ -1742,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

@@ -1365,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()
});
@@ -1587,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)
});
@@ -2315,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() {
Arc::new(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: Arc<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,170 +447,6 @@ mod tests {
}
}
fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
Arc::new(
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

@@ -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,7 +253,7 @@ impl ThreadFeedbackState {
editor
});
editor.read(cx).focus_handle(cx).focus(window, cx);
editor.read(cx).focus_handle(cx).focus(window);
editor
}
}
@@ -389,17 +389,6 @@ 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();
@@ -682,7 +671,7 @@ impl AcpThreadView {
})
});
this.message_editor.focus_handle(cx).focus(window, cx);
this.message_editor.focus_handle(cx).focus(window);
cx.notify();
}
@@ -784,7 +773,7 @@ impl AcpThreadView {
_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();
})
@@ -804,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();
}
@@ -1270,7 +1259,7 @@ impl AcpThreadView {
}
})
};
self.focus_handle(cx).focus(window, cx);
self.focus_handle(cx).focus(window);
cx.notify();
}
@@ -1322,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(
@@ -1465,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 => {
@@ -1898,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,
@@ -1962,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
@@ -2004,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()
@@ -2052,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|{
@@ -2149,10 +2112,7 @@ impl AcpThreadView {
)
.into_any()
}
AgentThreadEntry::AssistantMessage(AssistantMessage {
chunks,
indented: _,
}) => {
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
let is_last = entry_ix + 1 == total_entries;
let style = default_markdown_style(false, false, window, cx);
@@ -2186,7 +2146,6 @@ impl AcpThreadView {
v_flex()
.px_5()
.py_1p5()
.when(is_first_indented, |this| this.pt_0p5())
.when(is_last, |this| this.pb_4())
.w_full()
.text_ui(cx)
@@ -2196,48 +2155,19 @@ impl AcpThreadView {
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(rems_from_px(20.0))
.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 {
@@ -4121,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))
@@ -4156,6 +4084,7 @@ impl AcpThreadView {
.relative()
.pr_8()
.w_full()
.overflow_x_scroll()
.child(
h_flex()
.id(("file-name-path", index))
@@ -4167,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| {
@@ -4312,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()
@@ -5079,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)
})
})
@@ -6506,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};
@@ -73,8 +73,6 @@ use workspace::{
};
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
use crate::CycleFavoriteModels;
use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker};
use assistant_text_thread::{
CacheStatus, Content, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
@@ -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);
})
}
}
@@ -2211,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")
@@ -2274,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,
)
@@ -2634,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

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

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

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

@@ -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)])
});
@@ -25579,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:
@@ -25599,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:
@@ -25633,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:
@@ -25650,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:
@@ -25684,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:
@@ -25702,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:
@@ -25722,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:
@@ -25746,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:
@@ -25771,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:
@@ -25797,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:
@@ -25823,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:
@@ -25847,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:
@@ -25869,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):
@@ -25886,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]:
@@ -25900,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:ˇ
@@ -25924,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:
ˇ
@@ -25937,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! {"
{
ˇ
@@ -25971,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, |_| {});
@@ -26039,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
@@ -26057,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
@@ -26092,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
@@ -26135,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
@@ -26170,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\"
@@ -26186,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\"
@@ -26204,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\"
@@ -26222,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\"
@@ -26238,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\"
@@ -26259,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)
@@ -26282,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)
@@ -26302,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\"
@@ -26329,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:
ˇ
@@ -26343,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
@@ -26358,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
@@ -26373,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
@@ -26387,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
ˇ
@@ -26401,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)
@@ -26418,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)
@@ -26434,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() {
ˇ
@@ -26448,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\";
ˇ
@@ -27666,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]
@@ -29408,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,

View File

@@ -3,9 +3,9 @@ use anyhow::{Context as _, Result};
use collections::HashMap;
use git::{
GitHostingProviderRegistry, Oid,
blame::{Blame, BlameEntry},
commit::ParsedCommitMessage,
GitHostingProviderRegistry, GitRemote, Oid,
blame::{Blame, BlameEntry, ParsedCommitMessage},
parse_git_remote_url,
};
use gpui::{
AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task,
@@ -525,7 +525,12 @@ impl GitBlame {
.git_store()
.read(cx)
.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
.and_then(|(repo, _)| repo.read(cx).default_remote_url());
.and_then(|(repo, _)| {
repo.read(cx)
.remote_upstream_url
.clone()
.or(repo.read(cx).remote_origin_url.clone())
});
let blame_buffer = project
.update(cx, |project, cx| project.blame_buffer(&buffer, None, cx));
Ok(async move {
@@ -549,19 +554,13 @@ impl GitBlame {
entries,
snapshot.max_point().row,
);
let commit_details = messages
.into_iter()
.map(|(oid, message)| {
let parsed_commit_message =
ParsedCommitMessage::parse(
oid.to_string(),
message,
remote_url.as_deref(),
Some(provider_registry.clone()),
);
(oid, parsed_commit_message)
})
.collect();
let commit_details = parse_commit_messages(
messages,
remote_url,
provider_registry.clone(),
)
.await;
res.push((
id,
snapshot,
@@ -681,6 +680,55 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
entries
}
async fn parse_commit_messages(
messages: impl IntoIterator<Item = (Oid, String)>,
remote_url: Option<String>,
provider_registry: Arc<GitHostingProviderRegistry>,
) -> HashMap<Oid, ParsedCommitMessage> {
let mut commit_details = HashMap::default();
let parsed_remote_url = remote_url
.as_deref()
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
for (oid, message) in messages {
let permalink = if let Some((provider, git_remote)) = parsed_remote_url.as_ref() {
Some(provider.build_commit_permalink(
git_remote,
git::BuildCommitPermalinkParams {
sha: oid.to_string().as_str(),
},
))
} else {
None
};
let remote = parsed_remote_url
.as_ref()
.map(|(provider, remote)| GitRemote {
host: provider.clone(),
owner: remote.owner.clone().into(),
repo: remote.repo.clone().into(),
});
let pull_request = parsed_remote_url
.as_ref()
.and_then(|(provider, remote)| provider.extract_pull_request(remote, &message));
commit_details.insert(
oid,
ParsedCommitMessage {
message: message.into(),
permalink,
remote,
pull_request,
},
);
}
commit_details
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -218,7 +218,7 @@ impl Editor {
self.hide_hovered_link(cx);
if !hovered_link_state.links.is_empty() {
if !self.focus_handle.is_focused(window) {
window.focus(&self.focus_handle, cx);
window.focus(&self.focus_handle);
}
// exclude links pointing back to the current anchor

View File

@@ -19,7 +19,7 @@ pub struct JsxTagCompletionState {
/// that corresponds to the tag name
/// Note that this is not configurable, i.e. we assume the first
/// named child of a tag node is the tag name
const TS_NODE_TAG_NAME_CHILD_INDEX: u32 = 0;
const TS_NODE_TAG_NAME_CHILD_INDEX: usize = 0;
/// Maximum number of parent elements to walk back when checking if an open tag
/// is already closed.

View File

@@ -90,8 +90,8 @@ impl MouseContextMenu {
// `true` when the `ContextMenu` is focused.
let focus_handle = context_menu_focus.clone();
cx.on_next_frame(window, move |_, window, cx| {
cx.on_next_frame(window, move |_, window, cx| {
window.focus(&focus_handle, cx);
cx.on_next_frame(window, move |_, window, _cx| {
window.focus(&focus_handle);
});
});
@@ -100,7 +100,7 @@ impl MouseContextMenu {
move |editor, _, _event: &DismissEvent, window, cx| {
editor.mouse_context_menu.take();
if context_menu_focus.contains_focused(window, cx) {
window.focus(&editor.focus_handle(cx), cx);
window.focus(&editor.focus_handle(cx));
}
}
});
@@ -127,7 +127,7 @@ impl MouseContextMenu {
}
editor.mouse_context_menu.take();
if context_menu_focus.contains_focused(window, cx) {
window.focus(&editor.focus_handle(cx), cx);
window.focus(&editor.focus_handle(cx));
}
},
);
@@ -161,7 +161,7 @@ pub fn deploy_context_menu(
cx: &mut Context<Editor>,
) {
if !editor.is_focused(window) {
window.focus(&editor.focus_handle(cx), cx);
window.focus(&editor.focus_handle(cx));
}
// Don't show context menu for inline editors

View File

@@ -126,7 +126,7 @@ impl EditorLspTestContext {
.read(cx)
.nav_history_for_item(&cx.entity());
editor.set_nav_history(Some(nav_history));
window.focus(&editor.focus_handle(cx), cx)
window.focus(&editor.focus_handle(cx))
});
let lsp = fake_servers.next().await.unwrap();

View File

@@ -78,7 +78,7 @@ impl EditorTestContext {
cx,
);
window.focus(&editor.focus_handle(cx), cx);
window.focus(&editor.focus_handle(cx));
editor
});
let editor_view = editor.root(cx).unwrap();
@@ -139,7 +139,7 @@ impl EditorTestContext {
let editor = cx.add_window(|window, cx| {
let editor = build_editor(buffer, window, cx);
window.focus(&editor.focus_handle(cx), cx);
window.focus(&editor.focus_handle(cx));
editor
});
@@ -305,12 +305,6 @@ impl EditorTestContext {
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
}
pub async fn wait_for_autoindent_applied(&mut self) {
if let Some(fut) = self.update_buffer(|buffer, _| buffer.wait_for_autoindent_applied()) {
fut.await.ok();
}
}
pub fn set_head_text(&mut self, diff_base: &str) {
self.cx.run_until_parked();
let fs =

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