Compare commits
1 Commits
git-clone
...
editor/tog
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
350e355b15 |
81
.github/workflows/autofix_pr.yml
vendored
81
.github/workflows/autofix_pr.yml
vendored
@@ -9,23 +9,26 @@ on:
|
||||
description: pr_number
|
||||
required: true
|
||||
type: string
|
||||
run_clippy:
|
||||
description: run_clippy
|
||||
type: boolean
|
||||
default: 'true'
|
||||
jobs:
|
||||
run_autofix:
|
||||
runs-on: namespace-profile-16x32-ubuntu-2204
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
- id: get-app-token
|
||||
name: autofix_pr::run_autofix::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: steps::checkout_repo_with_token
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
token: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: autofix_pr::run_autofix::checkout_pr
|
||||
run: gh pr checkout ${{ inputs.pr_number }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: steps::setup_cargo_config
|
||||
run: |
|
||||
mkdir -p ./../.cargo
|
||||
@@ -55,74 +58,26 @@ jobs:
|
||||
run: cargo fmt --all
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: autofix_pr::run_autofix::run_clippy_fix
|
||||
if: ${{ inputs.run_clippy }}
|
||||
run: cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: create-patch
|
||||
name: autofix_pr::run_autofix::create_patch
|
||||
- name: autofix_pr::run_autofix::commit_and_push
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "No changes to commit"
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
git diff > autofix.patch
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
git add -A
|
||||
git commit -m "Autofix"
|
||||
git push
|
||||
fi
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: upload artifact autofix-patch
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
with:
|
||||
name: autofix-patch
|
||||
path: autofix.patch
|
||||
if-no-files-found: ignore
|
||||
retention-days: '1'
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
has_changes: ${{ steps.create-patch.outputs.has_changes }}
|
||||
commit_changes:
|
||||
needs:
|
||||
- run_autofix
|
||||
if: needs.run_autofix.outputs.has_changes == 'true'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: steps::checkout_repo_with_token
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
token: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: autofix_pr::commit_changes::checkout_pr
|
||||
run: gh pr checkout ${{ inputs.pr_number }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
- name: autofix_pr::download_patch_artifact
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
|
||||
with:
|
||||
name: autofix-patch
|
||||
- name: autofix_pr::commit_changes::apply_patch
|
||||
run: git apply autofix.patch
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: autofix_pr::commit_changes::commit_and_push
|
||||
run: |
|
||||
git commit -am "Autofix"
|
||||
git push
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GIT_COMMITTER_NAME: Zed Zippy
|
||||
GIT_COMMITTER_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
|
||||
GIT_AUTHOR_NAME: Zed Zippy
|
||||
GIT_AUTHOR_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
- name: steps::cleanup_cargo_config
|
||||
if: always()
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
2
.github/workflows/cherry_pick.yml
vendored
2
.github/workflows/cherry_pick.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
name: cherry_pick::run_cherry_pick::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
|
||||
@@ -34,7 +34,6 @@ jobs:
|
||||
CharlesChen0823
|
||||
chbk
|
||||
cppcoffee
|
||||
davidbarsky
|
||||
davewa
|
||||
ddoemonn
|
||||
djsauble
|
||||
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
18
.github/workflows/run_tests.yml
vendored
18
.github/workflows/run_tests.yml
vendored
@@ -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
|
||||
|
||||
3
.mailmap
3
.mailmap
@@ -141,9 +141,6 @@ Uladzislau Kaminski <i@uladkaminski.com>
|
||||
Uladzislau Kaminski <i@uladkaminski.com> <uladzislau_kaminski@epam.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
|
||||
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
|
||||
Yara <davidsk@zed.dev>
|
||||
Yara <git@davidsk.dev>
|
||||
Yara <git@yara.blue>
|
||||
Will Bradley <williambbradley@gmail.com>
|
||||
Will Bradley <williambbradley@gmail.com> <will@zed.dev>
|
||||
WindSoilder <WindSoilder@outlook.com>
|
||||
|
||||
396
Cargo.lock
generated
396
Cargo.lock
generated
@@ -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",
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@@ -192,7 +192,6 @@ members = [
|
||||
"crates/vercel",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/which_key",
|
||||
"crates/watch",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
@@ -416,7 +415,6 @@ util_macros = { path = "crates/util_macros" }
|
||||
vercel = { path = "crates/vercel" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
which_key = { path = "crates/which_key" }
|
||||
|
||||
watch = { path = "crates/watch" }
|
||||
web_search = { path = "crates/web_search" }
|
||||
@@ -457,15 +455,15 @@ async-task = "4.7"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = "0.31.0"
|
||||
async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] }
|
||||
aws-config = { version = "1.8.10", features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { version = "1.2.8", features = [
|
||||
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { version = "1.2.2", features = [
|
||||
"hardcoded-credentials",
|
||||
] }
|
||||
aws-sdk-bedrockruntime = { version = "1.112.0", features = [
|
||||
aws-sdk-bedrockruntime = { version = "1.80.0", features = [
|
||||
"behavior-version-latest",
|
||||
] }
|
||||
aws-smithy-runtime-api = { version = "1.9.2", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.4", features = ["http-body-1-x"] }
|
||||
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.22"
|
||||
bincode = "1.2.1"
|
||||
@@ -478,7 +476,6 @@ bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
cfg-if = "1.0.3"
|
||||
chardetng = "0.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ciborium = "0.2"
|
||||
circular-buffer = "1.0"
|
||||
@@ -502,7 +499,6 @@ dotenvy = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
encoding_rs = "0.8"
|
||||
exec = "0.3.1"
|
||||
fancy-regex = "0.16.0"
|
||||
fork = "0.4.0"
|
||||
@@ -667,7 +663,7 @@ tokio-socks = { version = "0.5.2", default-features = false, features = ["future
|
||||
toml = "0.8"
|
||||
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.26", features = ["wasm"] }
|
||||
tree-sitter = { version = "0.25.10", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.25.1"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
|
||||
@@ -701,7 +697,7 @@ uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
walkdir = "2.5"
|
||||
wasm-encoder = "0.221"
|
||||
wasmparser = "0.221"
|
||||
wasmtime = { version = "33", default-features = false, features = [
|
||||
wasmtime = { version = "29", default-features = false, features = [
|
||||
"async",
|
||||
"demangle",
|
||||
"runtime",
|
||||
@@ -710,7 +706,7 @@ wasmtime = { version = "33", default-features = false, features = [
|
||||
"incremental-cache",
|
||||
"parallel-compilation",
|
||||
] }
|
||||
wasmtime-wasi = "33"
|
||||
wasmtime-wasi = "29"
|
||||
wax = "0.6"
|
||||
which = "6.0.0"
|
||||
windows-core = "0.61"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.92-bookworm as builder
|
||||
FROM rust:1.91.1-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ ai
|
||||
= @rtfeldman
|
||||
|
||||
audio
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
|
||||
crashes
|
||||
= @p1n3appl3
|
||||
@@ -53,7 +53,7 @@ extension
|
||||
git
|
||||
= @cole-miller
|
||||
= @danilo-leal
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
= @kubkon
|
||||
= @Anthony-Eid
|
||||
= @cameron1024
|
||||
@@ -76,7 +76,7 @@ languages
|
||||
|
||||
linux
|
||||
= @cole-miller
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
= @p1n3appl3
|
||||
= @probably-neb
|
||||
= @smitbarmase
|
||||
@@ -92,7 +92,7 @@ multi_buffer
|
||||
= @SomeoneToIgnore
|
||||
|
||||
pickers
|
||||
= @yara-blue
|
||||
= @dvdsk
|
||||
= @p1n3appl3
|
||||
= @SomeoneToIgnore
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"ctrl-alt-z": "edit_prediction::RatePredictions",
|
||||
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-alt-shift-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -14,6 +14,7 @@ The section you'll need to rewrite is marked with <rewrite_this></rewrite_this>
|
||||
The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
|
||||
{{/if}}
|
||||
|
||||
{{#if rewrite_section}}
|
||||
And here's the section to rewrite based on that prompt again for reference:
|
||||
|
||||
<rewrite_this>
|
||||
@@ -32,6 +33,8 @@ Below are the diagnostic errors visible to the user. If the user requests probl
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
|
||||
|
||||
Start at the indentation level in the original file in the rewritten {{content_type}}.
|
||||
|
||||
@@ -1705,12 +1705,7 @@
|
||||
// }
|
||||
//
|
||||
"file_types": {
|
||||
"JSONC": [
|
||||
"**/.zed/*.json",
|
||||
"**/.vscode/**/*.json",
|
||||
"**/{zed,Zed}/{settings,keymap,tasks,debug}.json",
|
||||
"tsconfig*.json",
|
||||
],
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
|
||||
"Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
|
||||
"Shell Script": [".env.*"],
|
||||
},
|
||||
@@ -2067,12 +2062,6 @@
|
||||
//
|
||||
// Default: true
|
||||
"restore_unsaved_buffers": true,
|
||||
// Whether or not to skip worktree trust checks.
|
||||
// When trusted, project settings are synchronized automatically,
|
||||
// language and MCP servers are downloaded and started automatically.
|
||||
//
|
||||
// Default: false
|
||||
"trust_all_worktrees": false,
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// Allows to enable/disable formatting with Prettier
|
||||
@@ -2157,13 +2146,6 @@
|
||||
// The shape can be one of the following: "block", "bar", "underline", "hollow".
|
||||
"cursor_shape": {},
|
||||
},
|
||||
// Which-key popup settings
|
||||
"which_key": {
|
||||
// Whether to show the which-key popup when holding down key combinations.
|
||||
"enabled": false,
|
||||
// Delay in milliseconds before showing the which-key popup.
|
||||
"delay_ms": 1000,
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
"server_url": "https://zed.dev",
|
||||
|
||||
@@ -43,7 +43,6 @@ pub struct UserMessage {
|
||||
pub content: ContentBlock,
|
||||
pub chunks: Vec<acp::ContentBlock>,
|
||||
pub checkpoint: Option<Checkpoint>,
|
||||
pub indented: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -74,7 +73,6 @@ impl UserMessage {
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct AssistantMessage {
|
||||
pub chunks: Vec<AssistantMessageChunk>,
|
||||
pub indented: bool,
|
||||
}
|
||||
|
||||
impl AssistantMessage {
|
||||
@@ -125,14 +123,6 @@ pub enum AgentThreadEntry {
|
||||
}
|
||||
|
||||
impl AgentThreadEntry {
|
||||
pub fn is_indented(&self) -> bool {
|
||||
match self {
|
||||
Self::UserMessage(message) => message.indented,
|
||||
Self::AssistantMessage(message) => message.indented,
|
||||
Self::ToolCall(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_markdown(&self, cx: &App) -> String {
|
||||
match self {
|
||||
Self::UserMessage(message) => message.to_markdown(cx),
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -202,12 +202,6 @@ pub trait AgentModelSelector: 'static {
|
||||
fn should_render_footer(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether this selector supports the favorites feature.
|
||||
/// Only the native agent uses the model ID format that maps to settings.
|
||||
fn supports_favorites(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -245,10 +239,6 @@ impl AgentModelList {
|
||||
AgentModelList::Grouped(groups) => groups.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_flat(&self) -> bool {
|
||||
matches!(self, AgentModelList::Flat(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
|
||||
@@ -5,12 +5,12 @@ mod legacy_thread;
|
||||
mod native_agent_server;
|
||||
pub mod outline;
|
||||
mod templates;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod thread;
|
||||
mod tools;
|
||||
|
||||
use context_server::ContextServerId;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use db::*;
|
||||
pub use history_store::*;
|
||||
pub use native_agent_server::NativeAgentServer;
|
||||
@@ -18,11 +18,11 @@ pub use templates::*;
|
||||
pub use thread::*;
|
||||
pub use tools::*;
|
||||
|
||||
use acp_thread::{AcpThread, AgentModelSelector, UserMessageId};
|
||||
use acp_thread::{AcpThread, AgentModelSelector};
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use collections::{HashSet, IndexMap};
|
||||
use fs::Fs;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::future::Shared;
|
||||
@@ -39,6 +39,7 @@ use prompt_store::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{LanguageModelSelection, update_settings_file};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
@@ -251,24 +252,12 @@ impl NativeAgent {
|
||||
.await;
|
||||
|
||||
cx.new(|cx| {
|
||||
let context_server_store = project.read(cx).context_server_store();
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx));
|
||||
|
||||
let mut subscriptions = vec![
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
cx.subscribe(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
Self::handle_models_updated_event,
|
||||
),
|
||||
cx.subscribe(
|
||||
&context_server_store,
|
||||
Self::handle_context_server_store_updated,
|
||||
),
|
||||
cx.subscribe(
|
||||
&context_server_registry,
|
||||
Self::handle_context_server_registry_event,
|
||||
),
|
||||
];
|
||||
if let Some(prompt_store) = prompt_store.as_ref() {
|
||||
subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event))
|
||||
@@ -277,14 +266,16 @@ impl NativeAgent {
|
||||
let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) =
|
||||
watch::channel(());
|
||||
Self {
|
||||
sessions: HashMap::default(),
|
||||
sessions: HashMap::new(),
|
||||
history,
|
||||
project_context: cx.new(|_| project_context),
|
||||
project_context_needs_refresh: project_context_needs_refresh_tx,
|
||||
_maintain_project_context: cx.spawn(async move |this, cx| {
|
||||
Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await
|
||||
}),
|
||||
context_server_registry,
|
||||
context_server_registry: cx.new(|cx| {
|
||||
ContextServerRegistry::new(project.read(cx).context_server_store(), cx)
|
||||
}),
|
||||
templates,
|
||||
models: LanguageModels::new(cx),
|
||||
project,
|
||||
@@ -353,9 +344,6 @@ impl NativeAgent {
|
||||
pending_save: Task::ready(()),
|
||||
},
|
||||
);
|
||||
|
||||
self.update_available_commands(cx);
|
||||
|
||||
acp_thread
|
||||
}
|
||||
|
||||
@@ -426,7 +414,10 @@ impl NativeAgent {
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(UserRulesContext {
|
||||
uuid: prompt_metadata.id.user_id()?,
|
||||
uuid: match prompt_metadata.id {
|
||||
prompt_store::PromptId::User { uuid } => uuid,
|
||||
prompt_store::PromptId::EditWorkflow => return None,
|
||||
},
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
@@ -620,99 +611,6 @@ impl NativeAgent {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_context_server_store_updated(
|
||||
&mut self,
|
||||
_store: Entity<project::context_server_store::ContextServerStore>,
|
||||
_event: &project::context_server_store::Event,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.update_available_commands(cx);
|
||||
}
|
||||
|
||||
fn handle_context_server_registry_event(
|
||||
&mut self,
|
||||
_registry: Entity<ContextServerRegistry>,
|
||||
event: &ContextServerRegistryEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ContextServerRegistryEvent::ToolsChanged => {}
|
||||
ContextServerRegistryEvent::PromptsChanged => {
|
||||
self.update_available_commands(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_available_commands(&self, cx: &mut Context<Self>) {
|
||||
let available_commands = self.build_available_commands(cx);
|
||||
for session in self.sessions.values() {
|
||||
if let Some(acp_thread) = session.acp_thread.upgrade() {
|
||||
acp_thread.update(cx, |thread, cx| {
|
||||
thread
|
||||
.handle_session_update(
|
||||
acp::SessionUpdate::AvailableCommandsUpdate(
|
||||
acp::AvailableCommandsUpdate::new(available_commands.clone()),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_available_commands(&self, cx: &App) -> Vec<acp::AvailableCommand> {
|
||||
let registry = self.context_server_registry.read(cx);
|
||||
|
||||
let mut prompt_name_counts: HashMap<&str, usize> = HashMap::default();
|
||||
for context_server_prompt in registry.prompts() {
|
||||
*prompt_name_counts
|
||||
.entry(context_server_prompt.prompt.name.as_str())
|
||||
.or_insert(0) += 1;
|
||||
}
|
||||
|
||||
registry
|
||||
.prompts()
|
||||
.flat_map(|context_server_prompt| {
|
||||
let prompt = &context_server_prompt.prompt;
|
||||
|
||||
let should_prefix = prompt_name_counts
|
||||
.get(prompt.name.as_str())
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
> 1;
|
||||
|
||||
let name = if should_prefix {
|
||||
format!("{}.{}", context_server_prompt.server_id, prompt.name)
|
||||
} else {
|
||||
prompt.name.clone()
|
||||
};
|
||||
|
||||
let mut command = acp::AvailableCommand::new(
|
||||
name,
|
||||
prompt.description.clone().unwrap_or_default(),
|
||||
);
|
||||
|
||||
match prompt.arguments.as_deref() {
|
||||
Some([arg]) => {
|
||||
let hint = format!("<{}>", arg.name);
|
||||
|
||||
command = command.input(acp::AvailableCommandInput::Unstructured(
|
||||
acp::UnstructuredCommandInput::new(hint),
|
||||
));
|
||||
}
|
||||
Some([]) | None => {}
|
||||
Some(_) => {
|
||||
// skip >1 argument commands since we don't support them yet
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(command)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn load_thread(
|
||||
&mut self,
|
||||
id: acp::SessionId,
|
||||
@@ -811,102 +709,6 @@ impl NativeAgent {
|
||||
history.update(cx, |history, cx| history.reload(cx)).ok();
|
||||
});
|
||||
}
|
||||
|
||||
fn send_mcp_prompt(
|
||||
&self,
|
||||
message_id: UserMessageId,
|
||||
session_id: agent_client_protocol::SessionId,
|
||||
prompt_name: String,
|
||||
server_id: ContextServerId,
|
||||
arguments: HashMap<String, String>,
|
||||
original_content: Vec<acp::ContentBlock>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let server_store = self.context_server_registry.read(cx).server_store().clone();
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let prompt =
|
||||
crate::get_prompt(&server_store, &server_id, &prompt_name, arguments, cx).await?;
|
||||
|
||||
let (acp_thread, thread) = this.update(cx, |this, _cx| {
|
||||
let session = this
|
||||
.sessions
|
||||
.get(&session_id)
|
||||
.context("Failed to get session")?;
|
||||
anyhow::Ok((session.acp_thread.clone(), session.thread.clone()))
|
||||
})??;
|
||||
|
||||
let mut last_is_user = true;
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_acp_user_block(
|
||||
message_id,
|
||||
original_content.into_iter().skip(1),
|
||||
path_style,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
for message in prompt.messages {
|
||||
let context_server::types::PromptMessage { role, content } = message;
|
||||
let block = mcp_message_content_to_acp_content_block(content);
|
||||
|
||||
match role {
|
||||
context_server::types::Role::User => {
|
||||
let id = acp_thread::UserMessageId::new();
|
||||
|
||||
acp_thread.update(cx, |acp_thread, cx| {
|
||||
acp_thread.push_user_content_block_with_indent(
|
||||
Some(id.clone()),
|
||||
block.clone(),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_acp_user_block(id, [block], path_style, cx);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
}
|
||||
context_server::types::Role::Assistant => {
|
||||
acp_thread.update(cx, |acp_thread, cx| {
|
||||
acp_thread.push_assistant_content_block_with_indent(
|
||||
block.clone(),
|
||||
false,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_acp_agent_block(block, cx);
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
}
|
||||
}
|
||||
|
||||
last_is_user = role == context_server::types::Role::User;
|
||||
}
|
||||
|
||||
let response_stream = thread.update(cx, |thread, cx| {
|
||||
if last_is_user {
|
||||
thread.send_existing(cx)
|
||||
} else {
|
||||
// Resume if MCP prompt did not end with a user message
|
||||
thread.resume(cx)
|
||||
}
|
||||
})??;
|
||||
|
||||
cx.update(|cx| {
|
||||
NativeAgentConnection::handle_thread_events(response_stream, acp_thread, cx)
|
||||
})?
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper struct that implements the AgentConnection trait
|
||||
@@ -1041,39 +843,6 @@ impl NativeAgentConnection {
|
||||
}
|
||||
}
|
||||
|
||||
struct Command<'a> {
|
||||
prompt_name: &'a str,
|
||||
arg_value: &'a str,
|
||||
explicit_server_id: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> Command<'a> {
|
||||
fn parse(prompt: &'a [acp::ContentBlock]) -> Option<Self> {
|
||||
let acp::ContentBlock::Text(text_content) = prompt.first()? else {
|
||||
return None;
|
||||
};
|
||||
let text = text_content.text.trim();
|
||||
let command = text.strip_prefix('/')?;
|
||||
let (command, arg_value) = command
|
||||
.split_once(char::is_whitespace)
|
||||
.unwrap_or((command, ""));
|
||||
|
||||
if let Some((server_id, prompt_name)) = command.split_once('.') {
|
||||
Some(Self {
|
||||
prompt_name,
|
||||
arg_value,
|
||||
explicit_server_id: Some(server_id),
|
||||
})
|
||||
} else {
|
||||
Some(Self {
|
||||
prompt_name: command,
|
||||
arg_value,
|
||||
explicit_server_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NativeAgentModelSelector {
|
||||
session_id: acp::SessionId,
|
||||
connection: NativeAgentConnection,
|
||||
@@ -1164,10 +933,6 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
|
||||
fn should_render_footer(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_favorites(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
@@ -1243,47 +1008,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
let session_id = params.session_id.clone();
|
||||
log::info!("Received prompt request for session: {}", session_id);
|
||||
log::debug!("Prompt blocks count: {}", params.prompt.len());
|
||||
|
||||
if let Some(parsed_command) = Command::parse(¶ms.prompt) {
|
||||
let registry = self.0.read(cx).context_server_registry.read(cx);
|
||||
|
||||
let explicit_server_id = parsed_command
|
||||
.explicit_server_id
|
||||
.map(|server_id| ContextServerId(server_id.into()));
|
||||
|
||||
if let Some(prompt) =
|
||||
registry.find_prompt(explicit_server_id.as_ref(), parsed_command.prompt_name)
|
||||
{
|
||||
let arguments = if !parsed_command.arg_value.is_empty()
|
||||
&& let Some(arg_name) = prompt
|
||||
.prompt
|
||||
.arguments
|
||||
.as_ref()
|
||||
.and_then(|args| args.first())
|
||||
.map(|arg| arg.name.clone())
|
||||
{
|
||||
HashMap::from_iter([(arg_name, parsed_command.arg_value.to_string())])
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let prompt_name = prompt.prompt.name.clone();
|
||||
let server_id = prompt.server_id.clone();
|
||||
|
||||
return self.0.update(cx, |agent, cx| {
|
||||
agent.send_mcp_prompt(
|
||||
id,
|
||||
session_id.clone(),
|
||||
prompt_name,
|
||||
server_id,
|
||||
arguments,
|
||||
params.prompt,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
let path_style = self.0.read(cx).project.read(cx).path_style(cx);
|
||||
|
||||
self.run_turn(session_id, cx, move |thread, cx| {
|
||||
@@ -1880,35 +1604,3 @@ mod internal_tests {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_message_content_to_acp_content_block(
|
||||
content: context_server::types::MessageContent,
|
||||
) -> acp::ContentBlock {
|
||||
match content {
|
||||
context_server::types::MessageContent::Text {
|
||||
text,
|
||||
annotations: _,
|
||||
} => text.into(),
|
||||
context_server::types::MessageContent::Image {
|
||||
data,
|
||||
mime_type,
|
||||
annotations: _,
|
||||
} => acp::ContentBlock::Image(acp::ImageContent::new(data, mime_type)),
|
||||
context_server::types::MessageContent::Audio {
|
||||
data,
|
||||
mime_type,
|
||||
annotations: _,
|
||||
} => acp::ContentBlock::Audio(acp::AudioContent::new(data, mime_type)),
|
||||
context_server::types::MessageContent::Resource {
|
||||
resource,
|
||||
annotations: _,
|
||||
} => {
|
||||
let mut link =
|
||||
acp::ResourceLink::new(resource.uri.to_string(), resource.uri.to_string());
|
||||
if let Some(mime_type) = resource.mime_type {
|
||||
link = link.mime_type(mime_type);
|
||||
}
|
||||
acp::ContentBlock::ResourceLink(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,10 +216,14 @@ impl HistoryStore {
|
||||
}
|
||||
|
||||
pub fn reload(&self, cx: &mut Context<Self>) {
|
||||
let database_connection = ThreadsDatabase::connect(cx);
|
||||
let database_future = ThreadsDatabase::connect(cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let database = database_connection.await;
|
||||
let threads = database.map_err(|err| anyhow!(err))?.list_threads().await?;
|
||||
let threads = database_future
|
||||
.await
|
||||
.map_err(|err| anyhow!(err))?
|
||||
.list_threads()
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES {
|
||||
for thread in threads
|
||||
@@ -340,8 +344,7 @@ impl HistoryStore {
|
||||
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
|
||||
cx.background_spawn(async move {
|
||||
if cfg!(any(feature = "test-support", test)) {
|
||||
log::warn!("history store does not persist in tests");
|
||||
return Ok(VecDeque::new());
|
||||
anyhow::bail!("history store does not persist in tests");
|
||||
}
|
||||
let json = KEY_VALUE_STORE
|
||||
.read_kvp(RECENTLY_OPENED_THREADS_KEY)?
|
||||
|
||||
@@ -2809,181 +2809,3 @@ fn setup_context_server(
|
||||
cx.run_until_parked();
|
||||
mcp_tool_calls_rx
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tokens_before_message(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
// First message
|
||||
let message_1_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_1_id.clone(), ["First message"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Before any response, tokens_before_message should return None for first message
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message should have no tokens before it"
|
||||
);
|
||||
});
|
||||
|
||||
// Complete first message with usage
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 1");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 100,
|
||||
output_tokens: 50,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// First message still has no tokens before it
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message should still have no tokens before it after response"
|
||||
);
|
||||
});
|
||||
|
||||
// Second message
|
||||
let message_2_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_2_id.clone(), ["Second message"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Second message should have first message's input tokens before it
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_2_id),
|
||||
Some(100),
|
||||
"Second message should have 100 tokens before it (from first request)"
|
||||
);
|
||||
});
|
||||
|
||||
// Complete second message
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 2");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 250, // Total for this request (includes previous context)
|
||||
output_tokens: 75,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Third message
|
||||
let message_3_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_3_id.clone(), ["Third message"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Third message should have second message's input tokens (250) before it
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_3_id),
|
||||
Some(250),
|
||||
"Third message should have 250 tokens before it (from second request)"
|
||||
);
|
||||
// Second message should still have 100
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_2_id),
|
||||
Some(100),
|
||||
"Second message should still have 100 tokens before it"
|
||||
);
|
||||
// First message still has none
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message should still have no tokens before it"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tokens_before_message_after_truncate(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
// Set up three messages with responses
|
||||
let message_1_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_1_id.clone(), ["Message 1"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 1");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 100,
|
||||
output_tokens: 50,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
let message_2_id = UserMessageId::new();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(message_2_id.clone(), ["Message 2"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_text_chunk("Response 2");
|
||||
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate(
|
||||
language_model::TokenUsage {
|
||||
input_tokens: 250,
|
||||
output_tokens: 75,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
},
|
||||
));
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify initial state
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(thread.tokens_before_message(&message_2_id), Some(100));
|
||||
});
|
||||
|
||||
// Truncate at message 2 (removes message 2 and everything after)
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.truncate(message_2_id.clone(), cx))
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// After truncation, message_2_id no longer exists, so lookup should return None
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_2_id),
|
||||
None,
|
||||
"After truncation, message 2 no longer exists"
|
||||
);
|
||||
// Message 1 still exists but has no tokens before it
|
||||
assert_eq!(
|
||||
thread.tokens_before_message(&message_1_id),
|
||||
None,
|
||||
"First message still has no tokens before it"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,13 +108,7 @@ impl Message {
|
||||
|
||||
pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
|
||||
match self {
|
||||
Message::User(message) => {
|
||||
if message.content.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
vec![message.to_request()]
|
||||
}
|
||||
}
|
||||
Message::User(message) => vec![message.to_request()],
|
||||
Message::Agent(message) => message.to_request(),
|
||||
Message::Resume => vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
@@ -1095,28 +1089,6 @@ impl Thread {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the total input token count as of the message before the given message.
|
||||
///
|
||||
/// Returns `None` if:
|
||||
/// - `target_id` is the first message (no previous message)
|
||||
/// - The previous message hasn't received a response yet (no usage data)
|
||||
/// - `target_id` is not found in the messages
|
||||
pub fn tokens_before_message(&self, target_id: &UserMessageId) -> Option<u64> {
|
||||
let mut previous_user_message_id: Option<&UserMessageId> = None;
|
||||
|
||||
for message in &self.messages {
|
||||
if let Message::User(user_msg) = message {
|
||||
if &user_msg.id == target_id {
|
||||
let prev_id = previous_user_message_id?;
|
||||
let usage = self.request_token_usage.get(prev_id)?;
|
||||
return Some(usage.input_tokens);
|
||||
}
|
||||
previous_user_message_id = Some(&user_msg.id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Look up the active profile and resolve its preferred model if one is configured.
|
||||
fn resolve_profile_model(
|
||||
profile_id: &AgentProfileId,
|
||||
@@ -1169,6 +1141,11 @@ impl Thread {
|
||||
where
|
||||
T: Into<UserMessageContent>,
|
||||
{
|
||||
let model = self.model().context("No language model configured")?;
|
||||
|
||||
log::info!("Thread::send called with model: {}", model.name().0);
|
||||
self.advance_prompt_id();
|
||||
|
||||
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
|
||||
log::debug!("Thread::send content: {:?}", content);
|
||||
|
||||
@@ -1176,59 +1153,10 @@ impl Thread {
|
||||
.push(Message::User(UserMessage { id, content }));
|
||||
cx.notify();
|
||||
|
||||
self.send_existing(cx)
|
||||
}
|
||||
|
||||
pub fn send_existing(
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
|
||||
let model = self.model().context("No language model configured")?;
|
||||
|
||||
log::info!("Thread::send called with model: {}", model.name().0);
|
||||
self.advance_prompt_id();
|
||||
|
||||
log::debug!("Total messages in thread: {}", self.messages.len());
|
||||
self.run_turn(cx)
|
||||
}
|
||||
|
||||
pub fn push_acp_user_block(
|
||||
&mut self,
|
||||
id: UserMessageId,
|
||||
blocks: impl IntoIterator<Item = acp::ContentBlock>,
|
||||
path_style: PathStyle,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let content = blocks
|
||||
.into_iter()
|
||||
.map(|block| UserMessageContent::from_content_block(block, path_style))
|
||||
.collect::<Vec<_>>();
|
||||
self.messages
|
||||
.push(Message::User(UserMessage { id, content }));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn push_acp_agent_block(&mut self, block: acp::ContentBlock, cx: &mut Context<Self>) {
|
||||
let text = match block {
|
||||
acp::ContentBlock::Text(text_content) => text_content.text,
|
||||
acp::ContentBlock::Image(_) => "[image]".to_string(),
|
||||
acp::ContentBlock::Audio(_) => "[audio]".to_string(),
|
||||
acp::ContentBlock::ResourceLink(resource_link) => resource_link.uri,
|
||||
acp::ContentBlock::Resource(resource) => match resource.resource {
|
||||
acp::EmbeddedResourceResource::TextResourceContents(resource) => resource.uri,
|
||||
acp::EmbeddedResourceResource::BlobResourceContents(resource) => resource.uri,
|
||||
_ => "[resource]".to_string(),
|
||||
},
|
||||
_ => "[unknown]".to_string(),
|
||||
};
|
||||
|
||||
self.messages.push(Message::Agent(AgentMessage {
|
||||
content: vec![AgentMessageContent::Text(text)],
|
||||
..Default::default()
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(feature = "eval")]
|
||||
pub fn proceed(
|
||||
&mut self,
|
||||
@@ -1725,10 +1653,6 @@ impl Thread {
|
||||
self.pending_summary_generation.is_some()
|
||||
}
|
||||
|
||||
pub fn is_generating_title(&self) -> bool {
|
||||
self.pending_title_generation.is_some()
|
||||
}
|
||||
|
||||
pub fn summary(&mut self, cx: &mut Context<Self>) -> Shared<Task<Option<SharedString>>> {
|
||||
if let Some(summary) = self.summary.as_ref() {
|
||||
return Task::ready(Some(summary.clone())).shared();
|
||||
@@ -1796,7 +1720,7 @@ impl Thread {
|
||||
task
|
||||
}
|
||||
|
||||
pub fn generate_title(&mut self, cx: &mut Context<Self>) {
|
||||
fn generate_title(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(model) = self.summarization_model.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -2,24 +2,12 @@ use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream};
|
||||
use agent_client_protocol::ToolKind;
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use context_server::{ContextServerId, client::NotificationSubscription};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
|
||||
use context_server::ContextServerId;
|
||||
use gpui::{App, Context, Entity, SharedString, Task};
|
||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct ContextServerPrompt {
|
||||
pub server_id: ContextServerId,
|
||||
pub prompt: context_server::types::Prompt,
|
||||
}
|
||||
|
||||
pub enum ContextServerRegistryEvent {
|
||||
ToolsChanged,
|
||||
PromptsChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<ContextServerRegistryEvent> for ContextServerRegistry {}
|
||||
|
||||
pub struct ContextServerRegistry {
|
||||
server_store: Entity<ContextServerStore>,
|
||||
registered_servers: HashMap<ContextServerId, RegisteredContextServer>,
|
||||
@@ -28,10 +16,7 @@ pub struct ContextServerRegistry {
|
||||
|
||||
struct RegisteredContextServer {
|
||||
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
|
||||
prompts: BTreeMap<SharedString, ContextServerPrompt>,
|
||||
load_tools: Task<Result<()>>,
|
||||
load_prompts: Task<Result<()>>,
|
||||
_tools_updated_subscription: Option<NotificationSubscription>,
|
||||
}
|
||||
|
||||
impl ContextServerRegistry {
|
||||
@@ -43,7 +28,6 @@ impl ContextServerRegistry {
|
||||
};
|
||||
for server in server_store.read(cx).running_servers() {
|
||||
this.reload_tools_for_server(server.id(), cx);
|
||||
this.reload_prompts_for_server(server.id(), cx);
|
||||
}
|
||||
this
|
||||
}
|
||||
@@ -72,88 +56,6 @@ impl ContextServerRegistry {
|
||||
.map(|(id, server)| (id, &server.tools))
|
||||
}
|
||||
|
||||
pub fn prompts(&self) -> impl Iterator<Item = &ContextServerPrompt> {
|
||||
self.registered_servers
|
||||
.values()
|
||||
.flat_map(|server| server.prompts.values())
|
||||
}
|
||||
|
||||
pub fn find_prompt(
|
||||
&self,
|
||||
server_id: Option<&ContextServerId>,
|
||||
name: &str,
|
||||
) -> Option<&ContextServerPrompt> {
|
||||
if let Some(server_id) = server_id {
|
||||
self.registered_servers
|
||||
.get(server_id)
|
||||
.and_then(|server| server.prompts.get(name))
|
||||
} else {
|
||||
self.registered_servers
|
||||
.values()
|
||||
.find_map(|server| server.prompts.get(name))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_store(&self) -> &Entity<ContextServerStore> {
|
||||
&self.server_store
|
||||
}
|
||||
|
||||
fn get_or_register_server(
|
||||
&mut self,
|
||||
server_id: &ContextServerId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> &mut RegisteredContextServer {
|
||||
self.registered_servers
|
||||
.entry(server_id.clone())
|
||||
.or_insert_with(|| Self::init_registered_server(server_id, &self.server_store, cx))
|
||||
}
|
||||
|
||||
fn init_registered_server(
|
||||
server_id: &ContextServerId,
|
||||
server_store: &Entity<ContextServerStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> RegisteredContextServer {
|
||||
let tools_updated_subscription = server_store
|
||||
.read(cx)
|
||||
.get_running_server(server_id)
|
||||
.and_then(|server| {
|
||||
let client = server.client()?;
|
||||
|
||||
if !client.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let server_id = server.id();
|
||||
let this = cx.entity().downgrade();
|
||||
|
||||
Some(client.on_notification(
|
||||
"notifications/tools/list_changed",
|
||||
Box::new(move |_params, cx: AsyncApp| {
|
||||
let server_id = server_id.clone();
|
||||
let this = this.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
log::info!(
|
||||
"Received tools/list_changed notification for server {}",
|
||||
server_id
|
||||
);
|
||||
this.reload_tools_for_server(server_id, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}),
|
||||
))
|
||||
});
|
||||
|
||||
RegisteredContextServer {
|
||||
tools: BTreeMap::default(),
|
||||
prompts: BTreeMap::default(),
|
||||
load_tools: Task::ready(Ok(())),
|
||||
load_prompts: Task::ready(Ok(())),
|
||||
_tools_updated_subscription: tools_updated_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
|
||||
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
|
||||
return;
|
||||
@@ -161,12 +63,17 @@ impl ContextServerRegistry {
|
||||
let Some(client) = server.client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !client.capable(context_server::protocol::ServerCapability::Tools) {
|
||||
return;
|
||||
}
|
||||
|
||||
let registered_server = self.get_or_register_server(&server_id, cx);
|
||||
let registered_server =
|
||||
self.registered_servers
|
||||
.entry(server_id.clone())
|
||||
.or_insert(RegisteredContextServer {
|
||||
tools: BTreeMap::default(),
|
||||
load_tools: Task::ready(Ok(())),
|
||||
});
|
||||
registered_server.load_tools = cx.spawn(async move |this, cx| {
|
||||
let response = client
|
||||
.request::<context_server::types::requests::ListTools>(())
|
||||
@@ -187,49 +94,6 @@ impl ContextServerRegistry {
|
||||
));
|
||||
registered_server.tools.insert(tool.name(), tool);
|
||||
}
|
||||
cx.emit(ContextServerRegistryEvent::ToolsChanged);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn reload_prompts_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
|
||||
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = server.client() else {
|
||||
return;
|
||||
};
|
||||
if !client.capable(context_server::protocol::ServerCapability::Prompts) {
|
||||
return;
|
||||
}
|
||||
|
||||
let registered_server = self.get_or_register_server(&server_id, cx);
|
||||
|
||||
registered_server.load_prompts = cx.spawn(async move |this, cx| {
|
||||
let response = client
|
||||
.request::<context_server::types::requests::PromptsList>(())
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let Some(registered_server) = this.registered_servers.get_mut(&server_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
registered_server.prompts.clear();
|
||||
if let Some(response) = response.log_err() {
|
||||
for prompt in response.prompts {
|
||||
let name: SharedString = prompt.name.clone().into();
|
||||
registered_server.prompts.insert(
|
||||
name,
|
||||
ContextServerPrompt {
|
||||
server_id: server_id.clone(),
|
||||
prompt,
|
||||
},
|
||||
);
|
||||
}
|
||||
cx.emit(ContextServerRegistryEvent::PromptsChanged);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
@@ -248,17 +112,9 @@ impl ContextServerRegistry {
|
||||
ContextServerStatus::Starting => {}
|
||||
ContextServerStatus::Running => {
|
||||
self.reload_tools_for_server(server_id.clone(), cx);
|
||||
self.reload_prompts_for_server(server_id.clone(), cx);
|
||||
}
|
||||
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
|
||||
if let Some(registered_server) = self.registered_servers.remove(server_id) {
|
||||
if !registered_server.tools.is_empty() {
|
||||
cx.emit(ContextServerRegistryEvent::ToolsChanged);
|
||||
}
|
||||
if !registered_server.prompts.is_empty() {
|
||||
cx.emit(ContextServerRegistryEvent::PromptsChanged);
|
||||
}
|
||||
}
|
||||
self.registered_servers.remove(server_id);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -395,39 +251,3 @@ impl AnyAgentTool for ContextServerTool {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_prompt(
|
||||
server_store: &Entity<ContextServerStore>,
|
||||
server_id: &ContextServerId,
|
||||
prompt_name: &str,
|
||||
arguments: HashMap<String, String>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<context_server::types::PromptsGetResponse>> {
|
||||
let server = match cx.update(|cx| server_store.read(cx).get_running_server(server_id)) {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let Some(server) = server else {
|
||||
return Task::ready(Err(anyhow::anyhow!("Context server not found")));
|
||||
};
|
||||
|
||||
let Some(protocol) = server.client() else {
|
||||
return Task::ready(Err(anyhow::anyhow!("Context server not initialized")));
|
||||
};
|
||||
|
||||
let prompt_name = prompt_name.to_string();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let response = protocol
|
||||
.request::<context_server::types::requests::PromptsGet>(
|
||||
context_server::types::PromptsGetParams {
|
||||
name: prompt_name,
|
||||
arguments: (!arguments.is_empty()).then(|| arguments),
|
||||
meta: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(response)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ workspace = true
|
||||
path = "src/agent_settings.rs"
|
||||
|
||||
[dependencies]
|
||||
agent-client-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
|
||||
@@ -2,8 +2,7 @@ mod agent_profile;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::ModelId;
|
||||
use collections::{HashSet, IndexMap};
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, Pixels, px};
|
||||
use language_model::LanguageModel;
|
||||
use project::DisableAiSettings;
|
||||
@@ -34,7 +33,6 @@ pub struct AgentSettings {
|
||||
pub commit_message_model: Option<LanguageModelSelection>,
|
||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub favorite_models: Vec<LanguageModelSelection>,
|
||||
pub default_profile: AgentProfileId,
|
||||
pub default_view: DefaultAgentView,
|
||||
pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
|
||||
@@ -98,13 +96,6 @@ impl AgentSettings {
|
||||
pub fn set_message_editor_max_lines(&self) -> usize {
|
||||
self.message_editor_min_lines * 2
|
||||
}
|
||||
|
||||
pub fn favorite_model_ids(&self) -> HashSet<ModelId> {
|
||||
self.favorite_models
|
||||
.iter()
|
||||
.map(|sel| ModelId::new(format!("{}/{}", sel.provider.0, sel.model)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
@@ -173,7 +164,6 @@ impl Settings for AgentSettings {
|
||||
commit_message_model: agent.commit_message_model,
|
||||
thread_summary_model: agent.thread_summary_model,
|
||||
inline_alternatives: agent.inline_alternatives.unwrap_or_default(),
|
||||
favorite_models: agent.favorite_models,
|
||||
default_profile: AgentProfileId(agent.default_profile.unwrap()),
|
||||
default_view: agent.default_view.unwrap(),
|
||||
profiles: agent
|
||||
|
||||
@@ -13,7 +13,7 @@ path = "src/agent_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support", "agent/test-support"]
|
||||
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support"]
|
||||
unit-eval = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
@@ -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![
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -446,17 +446,17 @@ impl AddLlmProviderModal {
|
||||
})
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus_next(cx);
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_next();
|
||||
}
|
||||
|
||||
fn on_tab_prev(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
window.focus_prev(cx);
|
||||
window.focus_prev();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +493,7 @@ impl Render for AddLlmProviderModal {
|
||||
.on_action(cx.listener(Self::on_tab))
|
||||
.on_action(cx.listener(Self::on_tab_prev))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.child(
|
||||
Modal::new("configure-context-server", None)
|
||||
|
||||
@@ -831,7 +831,7 @@ impl Render for ConfigureContextServerModal {
|
||||
}),
|
||||
)
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.child(
|
||||
Modal::new("configure-context-server", None)
|
||||
|
||||
@@ -156,7 +156,7 @@ impl ManageProfilesModal {
|
||||
cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
|
||||
if matches!(this.mode, Mode::ChooseProfile(_)) {
|
||||
this.mode = Mode::choose_profile(window, cx);
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
@@ -173,7 +173,7 @@ impl ManageProfilesModal {
|
||||
|
||||
fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mode = Mode::choose_profile(window, cx);
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn new_profile(
|
||||
@@ -191,7 +191,7 @@ impl ManageProfilesModal {
|
||||
name_editor,
|
||||
base_profile_id,
|
||||
});
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
pub fn view_profile(
|
||||
@@ -209,7 +209,7 @@ impl ManageProfilesModal {
|
||||
delete_profile: NavigableEntry::focusable(cx),
|
||||
cancel_item: NavigableEntry::focusable(cx),
|
||||
});
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_default_model(
|
||||
@@ -222,6 +222,7 @@ impl ManageProfilesModal {
|
||||
let profile_id_for_closure = profile_id.clone();
|
||||
|
||||
let model_picker = cx.new(|cx| {
|
||||
let fs = fs.clone();
|
||||
let profile_id = profile_id_for_closure.clone();
|
||||
|
||||
language_model_selector(
|
||||
@@ -249,36 +250,22 @@ impl ManageProfilesModal {
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
let profile_id = profile_id.clone();
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
let profile_id = profile_id.clone();
|
||||
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
let agent_settings = settings.agent.get_or_insert_default();
|
||||
if let Some(profiles) = agent_settings.profiles.as_mut() {
|
||||
if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
|
||||
profile.default_model = Some(LanguageModelSelection {
|
||||
provider: LanguageModelProviderSetting(provider.clone()),
|
||||
model: model_id.clone(),
|
||||
});
|
||||
}
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
let agent_settings = settings.agent.get_or_insert_default();
|
||||
if let Some(profiles) = agent_settings.profiles.as_mut() {
|
||||
if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
|
||||
profile.default_model = Some(LanguageModelSelection {
|
||||
provider: LanguageModelProviderSetting(provider.clone()),
|
||||
model: model_id.clone(),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, should_be_favorite, cx| {
|
||||
crate::favorite_models::toggle_in_settings(
|
||||
model,
|
||||
should_be_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
false, // Do not use popover styles for the model picker
|
||||
self.focus_handle.clone(),
|
||||
@@ -300,7 +287,7 @@ impl ManageProfilesModal {
|
||||
model_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
};
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_mcp_tools(
|
||||
@@ -336,7 +323,7 @@ impl ManageProfilesModal {
|
||||
tool_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
};
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_builtin_tools(
|
||||
@@ -377,7 +364,7 @@ impl ManageProfilesModal {
|
||||
tool_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
};
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -951,7 +938,7 @@ impl Render for ManageProfilesModal {
|
||||
.on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
|
||||
.child(match &self.mode {
|
||||
|
||||
@@ -212,10 +212,10 @@ impl AgentDiffPane {
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx)
|
||||
{
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
} else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.focus_handle(cx).focus(window, cx);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -874,12 +874,12 @@ impl AgentDiffToolbar {
|
||||
match active_item {
|
||||
AgentDiffToolbarItem::Pane(agent_diff) => {
|
||||
if let Some(agent_diff) = agent_diff.upgrade() {
|
||||
agent_diff.focus_handle(cx).focus(window, cx);
|
||||
agent_diff.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
AgentDiffToolbarItem::Editor { editor, .. } => {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
editor.read(cx).focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,39 +29,26 @@ impl AgentModelSelector {
|
||||
|
||||
Self {
|
||||
selector: cx.new(move |cx| {
|
||||
let fs = fs.clone();
|
||||
language_model_selector(
|
||||
{
|
||||
let model_context = model_usage_context.clone();
|
||||
move |cx| model_context.configured_model(cx)
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
match &model_usage_context {
|
||||
ModelUsageContext::InlineAssistant => {
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_inline_assistant_model(provider.clone(), model_id);
|
||||
});
|
||||
}
|
||||
move |model, cx| {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
match &model_usage_context {
|
||||
ModelUsageContext::InlineAssistant => {
|
||||
update_settings_file(fs.clone(), cx, move |settings, _cx| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_inline_assistant_model(provider.clone(), model_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
let fs = fs.clone();
|
||||
move |model, should_be_favorite, cx| {
|
||||
crate::favorite_models::toggle_in_settings(
|
||||
model,
|
||||
should_be_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
true, // Use popover styles for picker
|
||||
focus_handle_clone,
|
||||
window,
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
|
||||
|
||||
use acp_thread::AcpThread;
|
||||
use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore};
|
||||
use agent_servers::AgentServer;
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use project::{
|
||||
ExternalAgentServerName,
|
||||
@@ -288,7 +287,7 @@ impl ActiveView {
|
||||
}
|
||||
}
|
||||
|
||||
fn native_agent(
|
||||
pub fn native_agent(
|
||||
fs: Arc<dyn Fs>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
history_store: Entity<agent::HistoryStore>,
|
||||
@@ -443,7 +442,6 @@ pub struct AgentPanel {
|
||||
pending_serialization: Option<Task<Result<()>>>,
|
||||
onboarding: Entity<AgentPanelOnboarding>,
|
||||
selected_agent: AgentType,
|
||||
show_trust_workspace_message: bool,
|
||||
}
|
||||
|
||||
impl AgentPanel {
|
||||
@@ -694,7 +692,6 @@ impl AgentPanel {
|
||||
history_store,
|
||||
selected_agent: AgentType::default(),
|
||||
loading: false,
|
||||
show_trust_workspace_message: false,
|
||||
};
|
||||
|
||||
// Initial sync of agent servers from extensions
|
||||
@@ -822,7 +819,7 @@ impl AgentPanel {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
text_thread_editor.focus_handle(cx).focus(window, cx);
|
||||
text_thread_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn external_thread(
|
||||
@@ -888,21 +885,36 @@ impl AgentPanel {
|
||||
};
|
||||
|
||||
let server = ext_agent.server(fs, history);
|
||||
this.update_in(cx, |agent_panel, window, cx| {
|
||||
agent_panel._external_thread(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace,
|
||||
project,
|
||||
loading,
|
||||
ext_agent,
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let selected_agent = ext_agent.into();
|
||||
if this.selected_agent != selected_agent {
|
||||
this.selected_agent = selected_agent;
|
||||
this.serialize(cx);
|
||||
}
|
||||
|
||||
let thread_view = cx.new(|cx| {
|
||||
crate::acp::AcpThreadView::new(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace.clone(),
|
||||
project,
|
||||
this.history_store.clone(),
|
||||
this.prompt_store.clone(),
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
this.set_active_view(
|
||||
ActiveView::ExternalAgentThread { thread_view },
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -935,7 +947,7 @@ impl AgentPanel {
|
||||
if let Some(thread_view) = self.active_thread_view() {
|
||||
thread_view.update(cx, |view, cx| {
|
||||
view.expand_message_editor(&ExpandMessageEditor, window, cx);
|
||||
view.focus_handle(cx).focus(window, cx);
|
||||
view.focus_handle(cx).focus(window);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1016,12 +1028,12 @@ impl AgentPanel {
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => {
|
||||
thread_view.focus_handle(cx).focus(window, cx);
|
||||
thread_view.focus_handle(cx).focus(window);
|
||||
}
|
||||
ActiveView::TextThread {
|
||||
text_thread_editor, ..
|
||||
} => {
|
||||
text_thread_editor.focus_handle(cx).focus(window, cx);
|
||||
text_thread_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
@@ -1169,7 +1181,7 @@ impl AgentPanel {
|
||||
Self::handle_agent_configuration_event,
|
||||
));
|
||||
|
||||
configuration.focus_handle(cx).focus(window, cx);
|
||||
configuration.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1305,7 +1317,7 @@ impl AgentPanel {
|
||||
}
|
||||
|
||||
if focus {
|
||||
self.focus_handle(cx).focus(window, cx);
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1465,47 +1477,6 @@ impl AgentPanel {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn _external_thread(
|
||||
&mut self,
|
||||
server: Rc<dyn AgentServer>,
|
||||
resume_thread: Option<DbThreadMetadata>,
|
||||
summarize_thread: Option<DbThreadMetadata>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
loading: bool,
|
||||
ext_agent: ExternalAgent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let selected_agent = AgentType::from(ext_agent);
|
||||
if self.selected_agent != selected_agent {
|
||||
self.selected_agent = selected_agent;
|
||||
self.serialize(cx);
|
||||
}
|
||||
|
||||
let thread_view = cx.new(|cx| {
|
||||
crate::acp::AcpThreadView::new(
|
||||
server,
|
||||
resume_thread,
|
||||
summarize_thread,
|
||||
workspace.clone(),
|
||||
project,
|
||||
self.history_store.clone(),
|
||||
self.prompt_store.clone(),
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
self.set_active_view(
|
||||
ActiveView::ExternalAgentThread { thread_view },
|
||||
!loading,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AgentPanel {
|
||||
@@ -1620,19 +1591,14 @@ impl AgentPanel {
|
||||
|
||||
let content = match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => {
|
||||
let is_generating_title = thread_view
|
||||
.read(cx)
|
||||
.as_native_thread(cx)
|
||||
.map_or(false, |t| t.read(cx).is_generating_title());
|
||||
|
||||
if let Some(title_editor) = thread_view.read(cx).title_editor() {
|
||||
let container = div()
|
||||
div()
|
||||
.w_full()
|
||||
.on_action({
|
||||
let thread_view = thread_view.downgrade();
|
||||
move |_: &menu::Confirm, window, cx| {
|
||||
if let Some(thread_view) = thread_view.upgrade() {
|
||||
thread_view.focus_handle(cx).focus(window, cx);
|
||||
thread_view.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1640,25 +1606,12 @@ impl AgentPanel {
|
||||
let thread_view = thread_view.downgrade();
|
||||
move |_: &editor::actions::Cancel, window, cx| {
|
||||
if let Some(thread_view) = thread_view.upgrade() {
|
||||
thread_view.focus_handle(cx).focus(window, cx);
|
||||
thread_view.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
})
|
||||
.child(title_editor);
|
||||
|
||||
if is_generating_title {
|
||||
container
|
||||
.with_animation(
|
||||
"generating_title",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|div, delta| div.opacity(delta),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
container.into_any_element()
|
||||
}
|
||||
.child(title_editor)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Label::new(thread_view.read(cx).title(cx))
|
||||
.color(Color::Muted)
|
||||
@@ -1688,13 +1641,6 @@ impl AgentPanel {
|
||||
Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
||||
.truncate()
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"generating_title",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.alpha(delta),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -1738,25 +1684,6 @@ impl AgentPanel {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn handle_regenerate_thread_title(thread_view: Entity<AcpThreadView>, cx: &mut App) {
|
||||
thread_view.update(cx, |thread_view, cx| {
|
||||
if let Some(thread) = thread_view.as_native_thread(cx) {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.generate_title(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_regenerate_text_thread_title(
|
||||
text_thread_editor: Entity<TextThreadEditor>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
text_thread_editor.update(cx, |text_thread_editor, cx| {
|
||||
text_thread_editor.regenerate_summary(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_panel_options_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
@@ -1776,35 +1703,6 @@ impl AgentPanel {
|
||||
|
||||
let selected_agent = self.selected_agent.clone();
|
||||
|
||||
let text_thread_view = match &self.active_view {
|
||||
ActiveView::TextThread {
|
||||
text_thread_editor, ..
|
||||
} => Some(text_thread_editor.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let text_thread_with_messages = match &self.active_view {
|
||||
ActiveView::TextThread {
|
||||
text_thread_editor, ..
|
||||
} => text_thread_editor
|
||||
.read(cx)
|
||||
.text_thread()
|
||||
.read(cx)
|
||||
.messages(cx)
|
||||
.any(|message| message.role == language_model::Role::Assistant),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let thread_view = match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => Some(thread_view.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let thread_with_messages = match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view } => {
|
||||
thread_view.read(cx).has_user_submitted_prompt(cx)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
PopoverMenu::new("agent-options-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
||||
@@ -1827,7 +1725,6 @@ impl AgentPanel {
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
|
||||
menu = menu.context(focus_handle.clone());
|
||||
|
||||
if let Some(usage) = usage {
|
||||
menu = menu
|
||||
.header_with_link("Prompt Usage", "Manage", account_url.clone())
|
||||
@@ -1865,38 +1762,6 @@ impl AgentPanel {
|
||||
.separator()
|
||||
}
|
||||
|
||||
if thread_with_messages | text_thread_with_messages {
|
||||
menu = menu.header("Current Thread");
|
||||
|
||||
if let Some(text_thread_view) = text_thread_view.as_ref() {
|
||||
menu = menu
|
||||
.entry("Regenerate Thread Title", None, {
|
||||
let text_thread_view = text_thread_view.clone();
|
||||
move |_, cx| {
|
||||
Self::handle_regenerate_text_thread_title(
|
||||
text_thread_view.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.separator();
|
||||
}
|
||||
|
||||
if let Some(thread_view) = thread_view.as_ref() {
|
||||
menu = menu
|
||||
.entry("Regenerate Thread Title", None, {
|
||||
let thread_view = thread_view.clone();
|
||||
move |_, cx| {
|
||||
Self::handle_regenerate_thread_title(
|
||||
thread_view.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.separator();
|
||||
}
|
||||
}
|
||||
|
||||
menu = menu
|
||||
.header("MCP Servers")
|
||||
.action(
|
||||
@@ -2692,38 +2557,6 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
|
||||
if !self.show_trust_workspace_message {
|
||||
return None;
|
||||
}
|
||||
|
||||
let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
|
||||
|
||||
Some(
|
||||
Callout::new()
|
||||
.icon(IconName::Warning)
|
||||
.severity(Severity::Warning)
|
||||
.border_position(ui::BorderPosition::Bottom)
|
||||
.title("You're in Restricted Mode")
|
||||
.description(description)
|
||||
.actions_slot(
|
||||
Button::new("open-trust-modal", "Configure Project Trust")
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.show_worktree_trust_security_modal(true, window, cx)
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
@@ -2776,7 +2609,6 @@ impl Render for AgentPanel {
|
||||
}
|
||||
}))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.children(self.render_workspace_trust_message(cx))
|
||||
.children(self.render_onboarding(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::ExternalAgentThread { thread_view, .. } => parent
|
||||
|
||||
@@ -7,7 +7,6 @@ mod buffer_codegen;
|
||||
mod completion_provider;
|
||||
mod context;
|
||||
mod context_server_configuration;
|
||||
mod favorite_models;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod language_model_selector;
|
||||
@@ -68,8 +67,6 @@ actions!(
|
||||
ToggleProfileSelector,
|
||||
/// Cycles through available session modes.
|
||||
CycleModeSelector,
|
||||
/// Cycles through favorited models in the ACP model selector.
|
||||
CycleFavoriteModels,
|
||||
/// Expands the message editor to full size.
|
||||
ExpandMessageEditor,
|
||||
/// Removes all thread history.
|
||||
@@ -460,7 +457,6 @@ mod tests {
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: vec![],
|
||||
favorite_models: vec![],
|
||||
default_profile: AgentProfileId::default(),
|
||||
default_view: DefaultAgentView::Thread,
|
||||
profiles: Default::default(),
|
||||
|
||||
@@ -75,9 +75,6 @@ pub struct BufferCodegen {
|
||||
session_id: Uuid,
|
||||
}
|
||||
|
||||
pub const REWRITE_SECTION_TOOL_NAME: &str = "rewrite_section";
|
||||
pub const FAILURE_MESSAGE_TOOL_NAME: &str = "failure_message";
|
||||
|
||||
impl BufferCodegen {
|
||||
pub fn new(
|
||||
buffer: Entity<MultiBuffer>,
|
||||
@@ -444,8 +441,7 @@ impl CodegenAlternative {
|
||||
})
|
||||
.boxed_local()
|
||||
};
|
||||
self.generation =
|
||||
self.handle_stream(model, /* strip_invalid_spans: */ true, stream, cx);
|
||||
self.generation = self.handle_stream(model, stream, cx);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -525,12 +521,12 @@ impl CodegenAlternative {
|
||||
|
||||
let tools = vec![
|
||||
LanguageModelRequestTool {
|
||||
name: REWRITE_SECTION_TOOL_NAME.to_string(),
|
||||
name: "rewrite_section".to_string(),
|
||||
description: "Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
|
||||
},
|
||||
LanguageModelRequestTool {
|
||||
name: FAILURE_MESSAGE_TOOL_NAME.to_string(),
|
||||
name: "failure_message".to_string(),
|
||||
description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
|
||||
},
|
||||
@@ -633,7 +629,6 @@ impl CodegenAlternative {
|
||||
pub fn handle_stream(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
strip_invalid_spans: bool,
|
||||
stream: impl 'static + Future<Output = Result<LanguageModelTextStream>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<()> {
|
||||
@@ -718,16 +713,10 @@ impl CodegenAlternative {
|
||||
let mut response_latency = None;
|
||||
let request_start = Instant::now();
|
||||
let diff = async {
|
||||
let raw_stream = stream?.stream.map_err(|error| error.into());
|
||||
|
||||
let stripped;
|
||||
let mut chunks: Pin<Box<dyn Stream<Item = Result<String>> + Send>> =
|
||||
if strip_invalid_spans {
|
||||
stripped = StripInvalidSpans::new(raw_stream);
|
||||
Box::pin(stripped)
|
||||
} else {
|
||||
Box::pin(raw_stream)
|
||||
};
|
||||
let chunks = StripInvalidSpans::new(
|
||||
stream?.stream.map_err(|error| error.into()),
|
||||
);
|
||||
futures::pin_mut!(chunks);
|
||||
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
let mut line_diff = LineDiff::default();
|
||||
@@ -1170,7 +1159,7 @@ impl CodegenAlternative {
|
||||
let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option<ToolUseOutput> {
|
||||
let mut chars_read_so_far = chars_read_so_far.lock();
|
||||
match tool_use.name.as_ref() {
|
||||
REWRITE_SECTION_TOOL_NAME => {
|
||||
"rewrite_section" => {
|
||||
let Ok(input) =
|
||||
serde_json::from_value::<RewriteSectionInput>(tool_use.input)
|
||||
else {
|
||||
@@ -1183,7 +1172,7 @@ impl CodegenAlternative {
|
||||
description: None,
|
||||
})
|
||||
}
|
||||
FAILURE_MESSAGE_TOOL_NAME => {
|
||||
"failure_message" => {
|
||||
let Ok(mut input) =
|
||||
serde_json::from_value::<FailureMessageInput>(tool_use.input)
|
||||
else {
|
||||
@@ -1318,12 +1307,7 @@ impl CodegenAlternative {
|
||||
|
||||
let Some(task) = codegen
|
||||
.update(cx, move |codegen, cx| {
|
||||
codegen.handle_stream(
|
||||
model,
|
||||
/* strip_invalid_spans: */ false,
|
||||
async { Ok(language_model_text_stream) },
|
||||
cx,
|
||||
)
|
||||
codegen.handle_stream(model, async { Ok(language_model_text_stream) }, cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
@@ -1496,10 +1480,7 @@ mod tests {
|
||||
use indoc::indoc;
|
||||
use language::{Buffer, Point};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use language_model::{
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRegistry,
|
||||
LanguageModelToolUse, StopReason, TokenUsage,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use languages::rust_lang;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
@@ -1811,51 +1792,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// When not streaming tool calls, we strip backticks as part of parsing the model's
|
||||
// plain text response. This is a regression test for a bug where we stripped
|
||||
// backticks incorrectly.
|
||||
#[gpui::test]
|
||||
async fn test_allows_model_to_output_backticks(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let text = "- Improved; `cmd+click` behavior. Now requires `cmd` to be pressed before the click starts or it doesn't run. ([#44579](https://github.com/zed-industries/zed/pull/44579); thanks [Zachiah](https://github.com/Zachiah))";
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(0, 0))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
prompt_builder,
|
||||
Uuid::new_v4(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let events_tx = simulate_tool_based_completion(&codegen, cx);
|
||||
let chunk_len = text.find('`').unwrap();
|
||||
events_tx
|
||||
.unbounded_send(rewrite_tool_use("tool_1", &text[..chunk_len], false))
|
||||
.unwrap();
|
||||
events_tx
|
||||
.unbounded_send(rewrite_tool_use("tool_2", &text, true))
|
||||
.unwrap();
|
||||
events_tx
|
||||
.unbounded_send(LanguageModelCompletionEvent::Stop(StopReason::EndTurn))
|
||||
.unwrap();
|
||||
drop(events_tx);
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await;
|
||||
@@ -1910,7 +1846,6 @@ mod tests {
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
codegen.generation = codegen.handle_stream(
|
||||
model,
|
||||
/* strip_invalid_spans: */ false,
|
||||
future::ready(Ok(LanguageModelTextStream {
|
||||
message_id: None,
|
||||
stream: chunks_rx.map(Ok).boxed(),
|
||||
@@ -1921,39 +1856,4 @@ mod tests {
|
||||
});
|
||||
chunks_tx
|
||||
}
|
||||
|
||||
fn simulate_tool_based_completion(
|
||||
codegen: &Entity<CodegenAlternative>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> mpsc::UnboundedSender<LanguageModelCompletionEvent> {
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
let completion_stream = Task::ready(Ok(events_rx.map(Ok).boxed()
|
||||
as BoxStream<
|
||||
'static,
|
||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>));
|
||||
codegen.generation = codegen.handle_completion(model, completion_stream, cx);
|
||||
});
|
||||
events_tx
|
||||
}
|
||||
|
||||
fn rewrite_tool_use(
|
||||
id: &str,
|
||||
replacement_text: &str,
|
||||
is_complete: bool,
|
||||
) -> LanguageModelCompletionEvent {
|
||||
let input = RewriteSectionInput {
|
||||
replacement_text: replacement_text.into(),
|
||||
};
|
||||
LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
|
||||
id: id.into(),
|
||||
name: REWRITE_SECTION_TOOL_NAME.into(),
|
||||
raw_input: serde_json::to_string(&input).unwrap(),
|
||||
input: serde_json::to_value(&input).unwrap(),
|
||||
is_input_complete: is_complete,
|
||||
thought_signature: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse,
|
||||
PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId,
|
||||
};
|
||||
use prompt_store::{PromptStore, UserPromptId};
|
||||
use prompt_store::{PromptId, PromptStore, UserPromptId};
|
||||
use rope::Point;
|
||||
use text::{Anchor, ToPoint as _};
|
||||
use ui::prelude::*;
|
||||
@@ -1585,10 +1585,13 @@ pub(crate) fn search_rules(
|
||||
if metadata.default {
|
||||
None
|
||||
} else {
|
||||
Some(RulesContextEntry {
|
||||
prompt_id: metadata.id.user_id()?,
|
||||
title: metadata.title?,
|
||||
})
|
||||
match metadata.id {
|
||||
PromptId::EditWorkflow => None,
|
||||
PromptId::User { uuid } => Some(RulesContextEntry {
|
||||
prompt_id: uuid,
|
||||
title: metadata.title?,
|
||||
}),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::ModelId;
|
||||
use fs::Fs;
|
||||
use language_model::LanguageModel;
|
||||
use settings::{LanguageModelSelection, update_settings_file};
|
||||
use ui::App;
|
||||
|
||||
fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelSelection {
|
||||
LanguageModelSelection {
|
||||
provider: model.provider_id().to_string().into(),
|
||||
model: model.id().0.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn model_id_to_selection(model_id: &ModelId) -> LanguageModelSelection {
|
||||
let id = model_id.0.as_ref();
|
||||
let (provider, model) = id.split_once('/').unwrap_or(("", id));
|
||||
LanguageModelSelection {
|
||||
provider: provider.to_owned().into(),
|
||||
model: model.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_in_settings(
|
||||
model: Arc<dyn LanguageModel>,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let selection = language_model_to_selection(&model);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let agent = settings.agent.get_or_insert_default();
|
||||
if should_be_favorite {
|
||||
agent.add_favorite_model(selection.clone());
|
||||
} else {
|
||||
agent.remove_favorite_model(&selection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_model_id_in_settings(
|
||||
model_id: ModelId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let selection = model_id_to_selection(&model_id);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let agent = settings.agent.get_or_insert_default();
|
||||
if should_be_favorite {
|
||||
agent.add_favorite_model(selection.clone());
|
||||
} else {
|
||||
agent.remove_favorite_model(&selection);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1197,7 +1197,7 @@ impl InlineAssistant {
|
||||
|
||||
assist
|
||||
.editor
|
||||
.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
|
||||
.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -1209,7 +1209,7 @@ impl InlineAssistant {
|
||||
if let Some(decorations) = assist.decorations.as_ref() {
|
||||
decorations.prompt_editor.update(cx, |prompt_editor, cx| {
|
||||
prompt_editor.editor.update(cx, |editor, cx| {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.select_all(&SelectAll, window, cx);
|
||||
})
|
||||
});
|
||||
@@ -2271,36 +2271,6 @@ pub mod evals {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||
fn eval_empty_buffer() {
|
||||
run_eval(
|
||||
20,
|
||||
1.0,
|
||||
"Write a Python hello, world program".to_string(),
|
||||
"ˇ".to_string(),
|
||||
|output| match output {
|
||||
InlineAssistantOutput::Success {
|
||||
full_buffer_text, ..
|
||||
} => {
|
||||
if full_buffer_text.is_empty() {
|
||||
EvalOutput::failed("expected some output".to_string())
|
||||
} else {
|
||||
EvalOutput::passed(format!("Produced {full_buffer_text}"))
|
||||
}
|
||||
}
|
||||
o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn run_eval(
|
||||
iterations: usize,
|
||||
expected_pass_ratio: f32,
|
||||
|
||||
@@ -357,7 +357,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
|
||||
|
||||
if focus {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
}
|
||||
editor
|
||||
});
|
||||
@@ -844,59 +844,26 @@ impl<T: 'static> PromptEditor<T> {
|
||||
|
||||
if show_rating_buttons {
|
||||
buttons.push(
|
||||
h_flex()
|
||||
.pl_1()
|
||||
.gap_1()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
IconButton::new("thumbs-up", IconName::ThumbsUp)
|
||||
.shape(IconButtonShape::Square)
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Good Result",
|
||||
None,
|
||||
"You already rated this result",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Good Result"))
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_up(&ThumbsUpResult, window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("thumbs-down", IconName::ThumbsDown)
|
||||
.shape(IconButtonShape::Square)
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Bad Result",
|
||||
None,
|
||||
"You already rated this result",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Bad Result"))
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_down(&ThumbsDownResult, window, cx);
|
||||
})),
|
||||
)
|
||||
IconButton::new("thumbs-down", IconName::ThumbsDown)
|
||||
.icon_color(if rated { Color::Muted } else { Color::Default })
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(rated)
|
||||
.tooltip(Tooltip::text("Bad result"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_down(&ThumbsDownResult, window, cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
IconButton::new("thumbs-up", IconName::ThumbsUp)
|
||||
.icon_color(if rated { Color::Muted } else { Color::Default })
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(rated)
|
||||
.tooltip(Tooltip::text("Good result"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.thumbs_up(&ThumbsUpResult, window, cx);
|
||||
}))
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
@@ -960,21 +927,10 @@ impl<T: 'static> PromptEditor<T> {
|
||||
}
|
||||
|
||||
fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip({
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Close Assistant",
|
||||
&editor::actions::Cancel,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.tooltip(Tooltip::text("Close Assistant"))
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
use std::{cmp::Reverse, sync::Arc};
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use collections::IndexMap;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelRegistry,
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||
LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use ui::prelude::*;
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
@@ -20,14 +18,12 @@ use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem}
|
||||
|
||||
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
||||
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
|
||||
type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &App) + 'static>;
|
||||
|
||||
pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
|
||||
|
||||
pub fn language_model_selector(
|
||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -36,7 +32,6 @@ pub fn language_model_selector(
|
||||
let delegate = LanguageModelPickerDelegate::new(
|
||||
get_active_model,
|
||||
on_model_changed,
|
||||
on_toggle_favorite,
|
||||
popover_styles,
|
||||
focus_handle,
|
||||
window,
|
||||
@@ -54,17 +49,7 @@ pub fn language_model_selector(
|
||||
}
|
||||
|
||||
fn all_models(cx: &App) -> GroupedModels {
|
||||
let lm_registry = LanguageModelRegistry::global(cx).read(cx);
|
||||
let providers = lm_registry.providers();
|
||||
|
||||
let mut favorites_index = FavoritesIndex::default();
|
||||
|
||||
for sel in &AgentSettings::get_global(cx).favorite_models {
|
||||
favorites_index
|
||||
.entry(sel.provider.0.clone().into())
|
||||
.or_default()
|
||||
.insert(sel.model.clone().into());
|
||||
}
|
||||
let providers = LanguageModelRegistry::global(cx).read(cx).providers();
|
||||
|
||||
let recommended = providers
|
||||
.iter()
|
||||
@@ -72,7 +57,10 @@ fn all_models(cx: &App) -> GroupedModels {
|
||||
provider
|
||||
.recommended_models(cx)
|
||||
.into_iter()
|
||||
.map(|model| ModelInfo::new(&**provider, model, &favorites_index))
|
||||
.map(|model| ModelInfo {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -82,44 +70,25 @@ fn all_models(cx: &App) -> GroupedModels {
|
||||
provider
|
||||
.provided_models(cx)
|
||||
.into_iter()
|
||||
.map(|model| ModelInfo::new(&**provider, model, &favorites_index))
|
||||
.map(|model| ModelInfo {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
GroupedModels::new(all, recommended)
|
||||
}
|
||||
|
||||
type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ModelInfo {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
icon: IconName,
|
||||
is_favorite: bool,
|
||||
}
|
||||
|
||||
impl ModelInfo {
|
||||
fn new(
|
||||
provider: &dyn LanguageModelProvider,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
favorites_index: &FavoritesIndex,
|
||||
) -> Self {
|
||||
let is_favorite = favorites_index
|
||||
.get(&provider.id())
|
||||
.map_or(false, |set| set.contains(&model.id()));
|
||||
|
||||
Self {
|
||||
model,
|
||||
icon: provider.icon(),
|
||||
is_favorite,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LanguageModelPickerDelegate {
|
||||
on_model_changed: OnModelChanged,
|
||||
get_active_model: GetActiveModel,
|
||||
on_toggle_favorite: OnToggleFavorite,
|
||||
all_models: Arc<GroupedModels>,
|
||||
filtered_entries: Vec<LanguageModelPickerEntry>,
|
||||
selected_index: usize,
|
||||
@@ -133,7 +102,6 @@ impl LanguageModelPickerDelegate {
|
||||
fn new(
|
||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -149,7 +117,6 @@ impl LanguageModelPickerDelegate {
|
||||
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
|
||||
filtered_entries: entries,
|
||||
get_active_model: Arc::new(get_active_model),
|
||||
on_toggle_favorite: Arc::new(on_toggle_favorite),
|
||||
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
|
||||
_subscriptions: vec![cx.subscribe_in(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
@@ -249,57 +216,15 @@ impl LanguageModelPickerDelegate {
|
||||
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
(self.get_active_model)(cx)
|
||||
}
|
||||
|
||||
pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if self.all_models.favorites.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let active_model = (self.get_active_model)(cx);
|
||||
let active_provider_id = active_model.as_ref().map(|m| m.provider.id());
|
||||
let active_model_id = active_model.as_ref().map(|m| m.model.id());
|
||||
|
||||
let current_index = self
|
||||
.all_models
|
||||
.favorites
|
||||
.iter()
|
||||
.position(|info| {
|
||||
Some(info.model.provider_id()) == active_provider_id
|
||||
&& Some(info.model.id()) == active_model_id
|
||||
})
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
let next_index = if current_index == usize::MAX {
|
||||
0
|
||||
} else {
|
||||
(current_index + 1) % self.all_models.favorites.len()
|
||||
};
|
||||
|
||||
let next_model = self.all_models.favorites[next_index].model.clone();
|
||||
|
||||
(self.on_model_changed)(next_model, cx);
|
||||
|
||||
// Align the picker selection with the newly-active model
|
||||
let new_index =
|
||||
Self::get_active_model_index(&self.filtered_entries, (self.get_active_model)(cx));
|
||||
self.set_selected_index(new_index, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupedModels {
|
||||
favorites: Vec<ModelInfo>,
|
||||
recommended: Vec<ModelInfo>,
|
||||
all: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>,
|
||||
}
|
||||
|
||||
impl GroupedModels {
|
||||
pub fn new(all: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
|
||||
let favorites = all
|
||||
.iter()
|
||||
.filter(|info| info.is_favorite)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let mut all_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
|
||||
for model in all {
|
||||
let provider = model.model.provider_id();
|
||||
@@ -311,7 +236,6 @@ impl GroupedModels {
|
||||
}
|
||||
|
||||
Self {
|
||||
favorites,
|
||||
recommended,
|
||||
all: all_by_provider,
|
||||
}
|
||||
@@ -320,18 +244,13 @@ impl GroupedModels {
|
||||
fn entries(&self) -> Vec<LanguageModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
if !self.favorites.is_empty() {
|
||||
entries.push(LanguageModelPickerEntry::Separator("Favorite".into()));
|
||||
for info in &self.favorites {
|
||||
entries.push(LanguageModelPickerEntry::Model(info.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if !self.recommended.is_empty() {
|
||||
entries.push(LanguageModelPickerEntry::Separator("Recommended".into()));
|
||||
for info in &self.recommended {
|
||||
entries.push(LanguageModelPickerEntry::Model(info.clone()));
|
||||
}
|
||||
entries.extend(
|
||||
self.recommended
|
||||
.iter()
|
||||
.map(|info| LanguageModelPickerEntry::Model(info.clone())),
|
||||
);
|
||||
}
|
||||
|
||||
for models in self.all.values() {
|
||||
@@ -341,11 +260,12 @@ impl GroupedModels {
|
||||
entries.push(LanguageModelPickerEntry::Separator(
|
||||
models[0].model.provider_name().0,
|
||||
));
|
||||
for info in models {
|
||||
entries.push(LanguageModelPickerEntry::Model(info.clone()));
|
||||
}
|
||||
entries.extend(
|
||||
models
|
||||
.iter()
|
||||
.map(|info| LanguageModelPickerEntry::Model(info.clone())),
|
||||
);
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
}
|
||||
@@ -541,7 +461,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
is_focused: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
@@ -557,20 +477,11 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
|
||||
&& Some(model_info.model.id()) == active_model_id;
|
||||
|
||||
let is_favorite = model_info.is_favorite;
|
||||
let handle_action_click = {
|
||||
let model = model_info.model.clone();
|
||||
let on_toggle_favorite = self.on_toggle_favorite.clone();
|
||||
move |cx: &App| on_toggle_favorite(model.clone(), !is_favorite, cx)
|
||||
};
|
||||
|
||||
Some(
|
||||
ModelSelectorListItem::new(ix, model_info.model.name().0)
|
||||
.icon(model_info.icon)
|
||||
.is_focused(is_focused)
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.is_favorite(is_favorite)
|
||||
.on_toggle_favorite(handle_action_click)
|
||||
.icon(model_info.icon)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
@@ -582,12 +493,12 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<gpui::AnyElement> {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
if !self.popover_styles {
|
||||
return None;
|
||||
}
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
Some(ModelSelectorFooter::new(OpenSettings.boxed_clone(), focus_handle).into_any_element())
|
||||
}
|
||||
}
|
||||
@@ -687,24 +598,11 @@ mod tests {
|
||||
}
|
||||
|
||||
fn create_models(model_specs: Vec<(&str, &str)>) -> Vec<ModelInfo> {
|
||||
create_models_with_favorites(model_specs, vec![])
|
||||
}
|
||||
|
||||
fn create_models_with_favorites(
|
||||
model_specs: Vec<(&str, &str)>,
|
||||
favorites: Vec<(&str, &str)>,
|
||||
) -> Vec<ModelInfo> {
|
||||
model_specs
|
||||
.into_iter()
|
||||
.map(|(provider, name)| {
|
||||
let is_favorite = favorites
|
||||
.iter()
|
||||
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
|
||||
ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconName::Ai,
|
||||
is_favorite,
|
||||
}
|
||||
.map(|(provider, name)| ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconName::Ai,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -842,93 +740,4 @@ mod tests {
|
||||
vec!["zed/claude", "zed/gemini", "copilot/claude"],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models_with_favorites(
|
||||
vec![("zed", "claude"), ("zed", "gemini"), ("openai", "gpt-4")],
|
||||
vec![("zed", "gemini")],
|
||||
);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
let entries = grouped_models.entries();
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(LanguageModelPickerEntry::Separator(s)) if s == "Favorite"
|
||||
));
|
||||
|
||||
assert_models_eq(grouped_models.favorites, vec!["zed/gemini"]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
|
||||
let recommended_models = create_models(vec![("zed", "claude")]);
|
||||
let all_models = create_models(vec![("zed", "claude"), ("zed", "gemini")]);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
let entries = grouped_models.entries();
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
Some(LanguageModelPickerEntry::Separator(s)) if s == "Recommended"
|
||||
));
|
||||
|
||||
assert!(grouped_models.favorites.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
|
||||
let recommended_models =
|
||||
create_models_with_favorites(vec![("zed", "claude")], vec![("zed", "claude")]);
|
||||
let all_models = create_models_with_favorites(
|
||||
vec![("zed", "claude"), ("zed", "gemini"), ("openai", "gpt-4")],
|
||||
vec![("zed", "claude")],
|
||||
);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
let entries = grouped_models.entries();
|
||||
|
||||
for entry in &entries {
|
||||
if let LanguageModelPickerEntry::Model(info) = entry {
|
||||
if info.model.telemetry_id() == "zed/claude" {
|
||||
assert!(info.is_favorite, "zed/claude should be a favorite");
|
||||
} else {
|
||||
assert!(
|
||||
!info.is_favorite,
|
||||
"{} should not be a favorite",
|
||||
info.model.telemetry_id()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_appear_in_other_sections(_cx: &mut TestAppContext) {
|
||||
let favorites = vec![("zed", "gemini"), ("openai", "gpt-4")];
|
||||
|
||||
let recommended_models =
|
||||
create_models_with_favorites(vec![("zed", "claude")], favorites.clone());
|
||||
|
||||
let all_models = create_models_with_favorites(
|
||||
vec![
|
||||
("zed", "claude"),
|
||||
("zed", "gemini"),
|
||||
("openai", "gpt-4"),
|
||||
("openai", "gpt-3.5"),
|
||||
],
|
||||
favorites,
|
||||
);
|
||||
|
||||
let grouped_models = GroupedModels::new(all_models, recommended_models);
|
||||
|
||||
assert_models_eq(grouped_models.favorites, vec!["zed/gemini", "openai/gpt-4"]);
|
||||
assert_models_eq(grouped_models.recommended, vec!["zed/claude"]);
|
||||
assert_models_eq(
|
||||
grouped_models.all.values().flatten().cloned().collect(),
|
||||
vec!["zed/claude", "zed/gemini", "openai/gpt-4", "openai/gpt-3.5"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ impl TerminalInlineAssistant {
|
||||
if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
|
||||
prompt_editor.update(cx, |this, cx| {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.select_all(&SelectAll, window, cx);
|
||||
});
|
||||
});
|
||||
@@ -292,7 +292,7 @@ impl TerminalInlineAssistant {
|
||||
.terminal
|
||||
.update(cx, |this, cx| {
|
||||
this.clear_block_below_cursor(cx);
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
@@ -369,7 +369,7 @@ impl TerminalInlineAssistant {
|
||||
.terminal
|
||||
.update(cx, |this, cx| {
|
||||
this.clear_block_below_cursor(cx);
|
||||
this.focus_handle(cx).focus(window, cx);
|
||||
this.focus_handle(cx).focus(window);
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
ui::BurnModeTooltip,
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use agent_settings::CompletionMode;
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
|
||||
use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
|
||||
@@ -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()
|
||||
|
||||
@@ -222,8 +222,8 @@ impl Render for AcpOnboardingModal {
|
||||
acp_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(illustration)
|
||||
.child(
|
||||
|
||||
@@ -230,8 +230,8 @@ impl Render for ClaudeCodeOnboardingModal {
|
||||
claude_code_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(illustration)
|
||||
.child(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use gpui::{Action, FocusHandle, prelude::*};
|
||||
use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelectorHeader {
|
||||
@@ -42,8 +42,6 @@ pub struct ModelSelectorListItem {
|
||||
icon: Option<IconName>,
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
is_favorite: bool,
|
||||
on_toggle_favorite: Option<Box<dyn Fn(&App) + 'static>>,
|
||||
}
|
||||
|
||||
impl ModelSelectorListItem {
|
||||
@@ -54,8 +52,6 @@ impl ModelSelectorListItem {
|
||||
icon: None,
|
||||
is_selected: false,
|
||||
is_focused: false,
|
||||
is_favorite: false,
|
||||
on_toggle_favorite: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,16 +69,6 @@ impl ModelSelectorListItem {
|
||||
self.is_focused = is_focused;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_favorite(mut self, is_favorite: bool) -> Self {
|
||||
self.is_favorite = is_favorite;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle_favorite(mut self, handler: impl Fn(&App) + 'static) -> Self {
|
||||
self.on_toggle_favorite = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ModelSelectorListItem {
|
||||
@@ -93,8 +79,6 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let is_favorite = self.is_favorite;
|
||||
|
||||
ListItem::new(self.index)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
@@ -113,24 +97,11 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
.child(Label::new(self.title).truncate()),
|
||||
)
|
||||
.end_slot(div().pr_2().when(self.is_selected, |this| {
|
||||
this.child(Icon::new(IconName::Check).color(Color::Accent))
|
||||
}))
|
||||
.end_hover_slot(div().pr_1p5().when_some(self.on_toggle_favorite, {
|
||||
|this, handle_click| {
|
||||
let (icon, color, tooltip) = if is_favorite {
|
||||
(IconName::StarFilled, Color::Accent, "Unfavorite Model")
|
||||
} else {
|
||||
(IconName::Star, Color::Default, "Favorite Model")
|
||||
};
|
||||
this.child(
|
||||
IconButton::new(("toggle-favorite", self.index), icon)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.icon_color(color)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.on_click(move |_, _, cx| (handle_click)(cx)),
|
||||
)
|
||||
}
|
||||
this.child(
|
||||
Icon::new(IconName::Check)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ impl Render for AgentOnboardingModal {
|
||||
agent_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -12,10 +12,6 @@ workspace = true
|
||||
path = "src/agent_ui_v2.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["agent/test-support"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
agent_servers.workspace = true
|
||||
@@ -42,6 +38,3 @@ time_format.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
agent = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1 +1 @@
|
||||
../../LICENSE-GPL
|
||||
LICENSE-GPL
|
||||
@@ -1052,71 +1052,6 @@ pub fn parse_prompt_too_long(message: &str) -> Option<u64> {
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Request body for the token counting API.
|
||||
/// Similar to `Request` but without `max_tokens` since it's not needed for counting.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CountTokensRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<Message>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub system: Option<StringOrContents>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tools: Vec<Tool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub thinking: Option<Thinking>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<ToolChoice>,
|
||||
}
|
||||
|
||||
/// Response from the token counting API.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CountTokensResponse {
|
||||
pub input_tokens: u64,
|
||||
}
|
||||
|
||||
/// Count the number of tokens in a message without creating it.
|
||||
pub async fn count_tokens(
|
||||
client: &dyn HttpClient,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: CountTokensRequest,
|
||||
) -> Result<CountTokensResponse, AnthropicError> {
|
||||
let uri = format!("{api_url}/v1/messages/count_tokens");
|
||||
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("X-Api-Key", api_key.trim())
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
let serialized_request =
|
||||
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
||||
let http_request = request_builder
|
||||
.body(AsyncBody::from(serialized_request))
|
||||
.map_err(AnthropicError::BuildRequestBody)?;
|
||||
|
||||
let mut response = client
|
||||
.send(http_request)
|
||||
.await
|
||||
.map_err(AnthropicError::HttpSend)?;
|
||||
|
||||
let rate_limits = RateLimitInfo::from_headers(response.headers());
|
||||
|
||||
if response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.map_err(AnthropicError::ReadResponse)?;
|
||||
|
||||
serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)
|
||||
} else {
|
||||
Err(handle_error_response(response, rate_limits).await)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_window_exceeded() {
|
||||
let error = ApiError {
|
||||
|
||||
@@ -87,7 +87,7 @@ pub async fn stream_completion(
|
||||
Ok(None) => None,
|
||||
Err(err) => Some((
|
||||
Err(BedrockError::ClientError(anyhow!(
|
||||
"{}",
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
))),
|
||||
stream,
|
||||
|
||||
@@ -616,11 +616,13 @@ impl BufferDiffInner {
|
||||
secondary: Option<&'a Self>,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk> {
|
||||
let range = range.to_offset(buffer);
|
||||
println!(" >>> range = {range:?}");
|
||||
|
||||
let mut cursor = self
|
||||
.hunks
|
||||
.filter::<_, DiffHunkSummary>(buffer, move |summary| {
|
||||
let summary_range = summary.buffer_range.to_offset(buffer);
|
||||
println!(" >>> summary_range = {:?}", summary_range);
|
||||
let before_start = summary_range.end < range.start;
|
||||
let after_end = summary_range.start > range.end;
|
||||
!before_start && !after_end
|
||||
@@ -2155,7 +2157,7 @@ mod tests {
|
||||
let range = diff_1.inner.compare(&empty_diff.inner, &buffer).unwrap();
|
||||
assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
|
||||
|
||||
// Edit does affects the diff because it recalculates word diffs.
|
||||
// Edit does not affect the diff.
|
||||
buffer.edit_via_marked_text(
|
||||
&"
|
||||
one
|
||||
@@ -2170,14 +2172,7 @@ mod tests {
|
||||
.unindent(),
|
||||
);
|
||||
let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
|
||||
assert_eq!(
|
||||
Point::new(4, 0)..Point::new(5, 0),
|
||||
diff_2
|
||||
.inner
|
||||
.compare(&diff_1.inner, &buffer)
|
||||
.unwrap()
|
||||
.to_point(&buffer)
|
||||
);
|
||||
assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer));
|
||||
|
||||
// Edit turns a deletion hunk into a modification.
|
||||
buffer.edit_via_marked_text(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
|
||||
use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
|
||||
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{App, Context, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
@@ -300,6 +300,16 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate {
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_cursor_position: Anchor,
|
||||
_direction: Direction,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
// Codestral doesn't support multiple completions, so cycling does nothing
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut Context<Self>) {
|
||||
log::debug!("Codestral: Completion accepted");
|
||||
self.pending_request = None;
|
||||
|
||||
@@ -4,7 +4,6 @@ use collections::{HashMap, HashSet};
|
||||
|
||||
use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
|
||||
use debugger_ui::debugger_panel::DebugPanel;
|
||||
use editor::{Editor, EditorMode, MultiBuffer};
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
use futures::StreamExt as _;
|
||||
@@ -13,30 +12,22 @@ use http_client::BlockedHttpClient;
|
||||
use language::{
|
||||
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
|
||||
language_settings::{Formatter, FormatterList, language_settings},
|
||||
rust_lang, tree_sitter_typescript,
|
||||
tree_sitter_typescript,
|
||||
};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::{
|
||||
ProjectPath,
|
||||
debugger::session::ThreadId,
|
||||
lsp_store::{FormatTrigger, LspFormatTarget},
|
||||
trusted_worktrees::{PathTrust, TrustedWorktrees},
|
||||
};
|
||||
use remote::RemoteClient;
|
||||
use remote_server::{HeadlessAppState, HeadlessProject};
|
||||
use rpc::proto;
|
||||
use serde_json::json;
|
||||
use settings::{
|
||||
InlayHintSettingsContent, LanguageServerFormatterSpecifier, PrettierSettingsContent,
|
||||
SettingsStore,
|
||||
};
|
||||
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::{path, rel_path::rel_path};
|
||||
@@ -99,14 +90,13 @@ async fn test_sharing_an_ssh_remote_project(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/code/project1"), client_ssh, false, cx_a)
|
||||
.build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
@@ -260,14 +250,13 @@ async fn test_ssh_collaboration_git_branches(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project("/project", client_ssh, false, cx_a)
|
||||
.build_ssh_project("/project", client_ssh, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
@@ -465,14 +454,13 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id) = client_a
|
||||
.build_ssh_project(path!("/project"), client_ssh, false, cx_a)
|
||||
.build_ssh_project(path!("/project"), client_ssh, cx_a)
|
||||
.await;
|
||||
|
||||
// While the SSH worktree is being scanned, user A shares the remote project.
|
||||
@@ -627,7 +615,6 @@ async fn test_remote_server_debugger(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -640,7 +627,7 @@ async fn test_remote_server_debugger(
|
||||
command_palette_hooks::init(cx);
|
||||
});
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
|
||||
.await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
@@ -736,7 +723,6 @@ async fn test_slow_adapter_startup_retries(
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -749,7 +735,7 @@ async fn test_slow_adapter_startup_retries(
|
||||
command_palette_hooks::init(cx);
|
||||
});
|
||||
let (project_a, _) = client_a
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
|
||||
.build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
|
||||
.await;
|
||||
|
||||
let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
@@ -852,261 +838,3 @@ async fn test_slow_adapter_startup_retries(
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||
use project::trusted_worktrees::RemoteHostLocation;
|
||||
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
project::trusted_worktrees::init(HashMap::default(), None, None, cx);
|
||||
});
|
||||
server_cx.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
project::trusted_worktrees::init(HashMap::default(), None, None, cx);
|
||||
});
|
||||
|
||||
let mut server = TestServer::start(cx_a.executor().clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
|
||||
let server_name = "override-rust-analyzer";
|
||||
let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
|
||||
let remote_fs = FakeFs::new(server_cx.executor());
|
||||
remote_fs
|
||||
.insert_tree(
|
||||
path!("/projects"),
|
||||
json!({
|
||||
"project_a": {
|
||||
".zed": {
|
||||
"settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
|
||||
},
|
||||
"main.rs": "fn main() {}"
|
||||
},
|
||||
"project_b": { "lib.rs": "pub fn lib() {}" }
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
server_cx.update(HeadlessProject::init);
|
||||
let remote_http_client = Arc::new(BlockedHttpClient);
|
||||
let node = NodeRuntime::unavailable();
|
||||
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
|
||||
languages.add(rust_lang());
|
||||
|
||||
let capabilities = lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
let mut fake_language_servers = languages.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: server_name,
|
||||
capabilities: capabilities.clone(),
|
||||
initializer: Some(Box::new({
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
move |fake_server| {
|
||||
let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
|
||||
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
|
||||
move |_params, _| {
|
||||
lsp_inlay_hint_request_count.fetch_add(1, Ordering::Release);
|
||||
async move {
|
||||
Ok(Some(vec![lsp::InlayHint {
|
||||
position: lsp::Position::new(0, 0),
|
||||
label: lsp::InlayHintLabel::String("hint".to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
}]))
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
let _headless_project = server_cx.new(|cx| {
|
||||
HeadlessProject::new(
|
||||
HeadlessAppState {
|
||||
session: server_ssh,
|
||||
fs: remote_fs.clone(),
|
||||
http_client: remote_http_client,
|
||||
node_runtime: node,
|
||||
languages,
|
||||
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
|
||||
},
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
|
||||
let (project_a, worktree_id_a) = client_a
|
||||
.build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a)
|
||||
.await;
|
||||
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
let language_settings = &mut settings.project.all_languages.defaults;
|
||||
language_settings.inlay_hints = Some(InlayHintSettingsContent {
|
||||
enabled: Some(true),
|
||||
..InlayHintSettingsContent::default()
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.languages().add(rust_lang());
|
||||
project.languages().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: server_name,
|
||||
capabilities,
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
project.find_or_create_worktree(path!("/projects/project_b"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_a.run_until_parked();
|
||||
|
||||
let worktree_ids = project_a.read_with(cx_a, |project, cx| {
|
||||
project
|
||||
.worktrees(cx)
|
||||
.map(|wt| wt.read(cx).id())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(worktree_ids.len(), 2);
|
||||
|
||||
let remote_host = project_a.read_with(cx_a, |project, cx| {
|
||||
project
|
||||
.remote_connection_options(cx)
|
||||
.map(RemoteHostLocation::from)
|
||||
});
|
||||
|
||||
let trusted_worktrees =
|
||||
cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(!can_trust_a, "project_a should be restricted initially");
|
||||
assert!(!can_trust_b, "project_b should be restricted initially");
|
||||
|
||||
let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
|
||||
let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(has_restricted, "should have restricted worktrees");
|
||||
|
||||
let buffer_before_approval = project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.open_buffer((worktree_id_a, rel_path("main.rs")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (editor, cx_a) = cx_a.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::full(),
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
|
||||
Some(project_a.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
let fake_language_server = fake_language_servers.next();
|
||||
|
||||
cx_a.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||
["...".to_string()],
|
||||
"remote .zed/settings.json must not sync before trust approval"
|
||||
)
|
||||
});
|
||||
|
||||
editor.update_in(cx_a, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
cx_a.executor().advance_clock(Duration::from_secs(1));
|
||||
assert_eq!(
|
||||
lsp_inlay_hint_request_count.load(Ordering::Acquire),
|
||||
0,
|
||||
"inlay hints must not be queried before trust approval"
|
||||
);
|
||||
|
||||
trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.trust(
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
|
||||
remote_host.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
|
||||
cx_a.read(|cx| {
|
||||
let file = buffer_before_approval.read(cx).file();
|
||||
assert_eq!(
|
||||
language_settings(Some("Rust".into()), file, cx).language_servers,
|
||||
["override-rust-analyzer".to_string()],
|
||||
"remote .zed/settings.json should sync after trust approval"
|
||||
)
|
||||
});
|
||||
let _fake_language_server = fake_language_server.await.unwrap();
|
||||
editor.update_in(cx_a, |editor, window, cx| {
|
||||
editor.handle_input("1", window, cx);
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
cx_a.executor().advance_clock(Duration::from_secs(1));
|
||||
assert!(
|
||||
lsp_inlay_hint_request_count.load(Ordering::Acquire) > 0,
|
||||
"inlay hints should be queried after trust approval"
|
||||
);
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(can_trust_a, "project_a should be trusted after trust()");
|
||||
assert!(!can_trust_b, "project_b should still be restricted");
|
||||
|
||||
trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.trust(
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
|
||||
remote_host.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
assert!(can_trust_a, "project_a should remain trusted");
|
||||
assert!(can_trust_b, "project_b should now be trusted");
|
||||
|
||||
let has_restricted_after = trusted_worktrees.read_with(cx_a, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
assert!(
|
||||
!has_restricted_after,
|
||||
"should have no restricted worktrees after trusting both"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -761,7 +761,6 @@ impl TestClient {
|
||||
&self,
|
||||
root_path: impl AsRef<Path>,
|
||||
ssh: Entity<RemoteClient>,
|
||||
init_worktree_trust: bool,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Project>, WorktreeId) {
|
||||
let project = cx.update(|cx| {
|
||||
@@ -772,7 +771,6 @@ impl TestClient {
|
||||
self.app_state.user_store.clone(),
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
init_worktree_trust,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -841,7 +839,6 @@ impl TestClient {
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1252,7 +1252,7 @@ impl CollabPanel {
|
||||
context_menu
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
@@ -1424,7 +1424,7 @@ impl CollabPanel {
|
||||
context_menu
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
@@ -1487,7 +1487,7 @@ impl CollabPanel {
|
||||
})
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
@@ -1521,9 +1521,9 @@ impl CollabPanel {
|
||||
if cx.stop_active_drag(window) {
|
||||
return;
|
||||
} else if self.take_editing_state(window, cx) {
|
||||
window.focus(&self.filter_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.filter_editor.focus_handle(cx));
|
||||
} else if !self.reset_filter_editor_text(window, cx) {
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
}
|
||||
|
||||
if self.context_menu.is_some() {
|
||||
@@ -1826,7 +1826,7 @@ impl CollabPanel {
|
||||
});
|
||||
self.update_entries(false, cx);
|
||||
self.select_channel_editor();
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1851,7 +1851,7 @@ impl CollabPanel {
|
||||
});
|
||||
self.update_entries(false, cx);
|
||||
self.select_channel_editor();
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1900,7 +1900,7 @@ impl CollabPanel {
|
||||
editor.set_text(channel.name.clone(), window, cx);
|
||||
editor.select_all(&Default::default(), window, cx);
|
||||
});
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
|
||||
window.focus(&self.channel_name_editor.focus_handle(cx));
|
||||
self.update_entries(false, cx);
|
||||
self.select_channel_editor();
|
||||
}
|
||||
|
||||
@@ -642,7 +642,7 @@ impl ChannelModalDelegate {
|
||||
});
|
||||
menu
|
||||
});
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe_in(
|
||||
&context_menu,
|
||||
window,
|
||||
|
||||
@@ -588,7 +588,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let action = command.action;
|
||||
window.focus(&self.previous_focus_handle, cx);
|
||||
window.focus(&self.previous_focus_handle);
|
||||
self.dismissed(window, cx);
|
||||
window.dispatch_action(action, cx);
|
||||
}
|
||||
@@ -784,7 +784,7 @@ mod tests {
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
@@ -855,7 +855,7 @@ mod tests {
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
|
||||
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
|
||||
});
|
||||
|
||||
// Test normalize (trimming whitespace and double colons)
|
||||
|
||||
@@ -29,7 +29,6 @@ schemars.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
slotmap.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
|
||||
@@ -6,7 +6,6 @@ use parking_lot::Mutex;
|
||||
use postage::barrier;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use serde_json::{Value, value::RawValue};
|
||||
use slotmap::SlotMap;
|
||||
use smol::channel;
|
||||
use std::{
|
||||
fmt,
|
||||
@@ -51,7 +50,7 @@ pub(crate) struct Client {
|
||||
next_id: AtomicI32,
|
||||
outbound_tx: channel::Sender<String>,
|
||||
name: Arc<str>,
|
||||
subscription_set: Arc<Mutex<NotificationSubscriptionSet>>,
|
||||
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
||||
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[allow(dead_code)]
|
||||
@@ -192,20 +191,21 @@ impl Client {
|
||||
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
|
||||
let (output_done_tx, output_done_rx) = barrier::channel();
|
||||
|
||||
let subscription_set = Arc::new(Mutex::new(NotificationSubscriptionSet::default()));
|
||||
let notification_handlers =
|
||||
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
|
||||
let response_handlers =
|
||||
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
|
||||
let request_handlers = Arc::new(Mutex::new(HashMap::<_, RequestHandler>::default()));
|
||||
|
||||
let receive_input_task = cx.spawn({
|
||||
let subscription_set = subscription_set.clone();
|
||||
let notification_handlers = notification_handlers.clone();
|
||||
let response_handlers = response_handlers.clone();
|
||||
let request_handlers = request_handlers.clone();
|
||||
let transport = transport.clone();
|
||||
async move |cx| {
|
||||
Self::handle_input(
|
||||
transport,
|
||||
subscription_set,
|
||||
notification_handlers,
|
||||
request_handlers,
|
||||
response_handlers,
|
||||
cx,
|
||||
@@ -236,7 +236,7 @@ impl Client {
|
||||
|
||||
Ok(Self {
|
||||
server_id,
|
||||
subscription_set,
|
||||
notification_handlers,
|
||||
response_handlers,
|
||||
name: server_name,
|
||||
next_id: Default::default(),
|
||||
@@ -257,7 +257,7 @@ impl Client {
|
||||
/// to pending requests) and notifications (which trigger registered handlers).
|
||||
async fn handle_input(
|
||||
transport: Arc<dyn Transport>,
|
||||
subscription_set: Arc<Mutex<NotificationSubscriptionSet>>,
|
||||
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
|
||||
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
|
||||
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -282,11 +282,10 @@ impl Client {
|
||||
handler(Ok(message.to_string()));
|
||||
}
|
||||
} else if let Ok(notification) = serde_json::from_str::<AnyNotification>(&message) {
|
||||
subscription_set.lock().notify(
|
||||
¬ification.method,
|
||||
notification.params.unwrap_or(Value::Null),
|
||||
cx,
|
||||
)
|
||||
let mut notification_handlers = notification_handlers.lock();
|
||||
if let Some(handler) = notification_handlers.get_mut(notification.method.as_str()) {
|
||||
handler(notification.params.unwrap_or(Value::Null), cx.clone());
|
||||
}
|
||||
} else {
|
||||
log::error!("Unhandled JSON from context_server: {}", message);
|
||||
}
|
||||
@@ -452,18 +451,12 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn on_notification(
|
||||
&self,
|
||||
method: &'static str,
|
||||
f: Box<dyn 'static + Send + FnMut(Value, AsyncApp)>,
|
||||
) -> NotificationSubscription {
|
||||
let mut notification_subscriptions = self.subscription_set.lock();
|
||||
|
||||
NotificationSubscription {
|
||||
id: notification_subscriptions.add_handler(method, f),
|
||||
set: self.subscription_set.clone(),
|
||||
}
|
||||
) {
|
||||
self.notification_handlers.lock().insert(method, f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,73 +485,3 @@ impl fmt::Debug for Client {
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
slotmap::new_key_type! {
|
||||
struct NotificationSubscriptionId;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NotificationSubscriptionSet {
|
||||
// we have very few subscriptions at the moment
|
||||
methods: Vec<(&'static str, Vec<NotificationSubscriptionId>)>,
|
||||
handlers: SlotMap<NotificationSubscriptionId, NotificationHandler>,
|
||||
}
|
||||
|
||||
impl NotificationSubscriptionSet {
|
||||
#[must_use]
|
||||
fn add_handler(
|
||||
&mut self,
|
||||
method: &'static str,
|
||||
handler: NotificationHandler,
|
||||
) -> NotificationSubscriptionId {
|
||||
let id = self.handlers.insert(handler);
|
||||
if let Some((_, handler_ids)) = self
|
||||
.methods
|
||||
.iter_mut()
|
||||
.find(|(probe_method, _)| method == *probe_method)
|
||||
{
|
||||
debug_assert!(
|
||||
handler_ids.len() < 20,
|
||||
"Too many MCP handlers for {}. Consider using a different data structure.",
|
||||
method
|
||||
);
|
||||
|
||||
handler_ids.push(id);
|
||||
} else {
|
||||
self.methods.push((method, vec![id]));
|
||||
};
|
||||
id
|
||||
}
|
||||
|
||||
fn notify(&mut self, method: &str, payload: Value, cx: &mut AsyncApp) {
|
||||
let Some((_, handler_ids)) = self
|
||||
.methods
|
||||
.iter_mut()
|
||||
.find(|(probe_method, _)| method == *probe_method)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
for handler_id in handler_ids {
|
||||
if let Some(handler) = self.handlers.get_mut(*handler_id) {
|
||||
handler(payload.clone(), cx.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NotificationSubscription {
|
||||
id: NotificationSubscriptionId,
|
||||
set: Arc<Mutex<NotificationSubscriptionSet>>,
|
||||
}
|
||||
|
||||
impl Drop for NotificationSubscription {
|
||||
fn drop(&mut self) {
|
||||
let mut set = self.set.lock();
|
||||
set.handlers.remove(self.id);
|
||||
set.methods.retain_mut(|(_, handler_ids)| {
|
||||
handler_ids.retain(|id| *id != self.id);
|
||||
!handler_ids.is_empty()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,22 @@ impl ContextServer {
|
||||
self.initialize(self.new_client(cx)?).await
|
||||
}
|
||||
|
||||
/// Starts the context server, making sure handlers are registered before initialization happens
|
||||
pub async fn start_with_handlers(
|
||||
&self,
|
||||
notification_handlers: Vec<(
|
||||
&'static str,
|
||||
Box<dyn 'static + Send + FnMut(serde_json::Value, AsyncApp)>,
|
||||
)>,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<()> {
|
||||
let client = self.new_client(cx)?;
|
||||
for (method, handler) in notification_handlers {
|
||||
client.on_notification(method, handler);
|
||||
}
|
||||
self.initialize(client).await
|
||||
}
|
||||
|
||||
fn new_client(&self, cx: &AsyncApp) -> Result<Client> {
|
||||
Ok(match &self.configuration {
|
||||
ContextServerTransport::Stdio(command, working_directory) => Client::stdio(
|
||||
|
||||
@@ -12,7 +12,7 @@ use futures::channel::oneshot;
|
||||
use gpui::AsyncApp;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::client::{Client, NotificationSubscription};
|
||||
use crate::client::Client;
|
||||
use crate::types::{self, Notification, Request};
|
||||
|
||||
pub struct ModelContextProtocol {
|
||||
@@ -119,7 +119,7 @@ impl InitializedContextServerProtocol {
|
||||
&self,
|
||||
method: &'static str,
|
||||
f: Box<dyn 'static + Send + FnMut(Value, AsyncApp)>,
|
||||
) -> NotificationSubscription {
|
||||
self.inner.on_notification(method, f)
|
||||
) {
|
||||
self.inner.on_notification(method, f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +330,7 @@ pub struct PromptMessage {
|
||||
pub content: MessageContent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
User,
|
||||
|
||||
@@ -4,7 +4,6 @@ pub mod copilot_responses;
|
||||
pub mod request;
|
||||
mod sign_in;
|
||||
|
||||
use crate::request::NextEditSuggestions;
|
||||
use crate::sign_in::initiate_sign_out;
|
||||
use ::fs::Fs;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -19,7 +18,7 @@ use http_client::HttpClient;
|
||||
use language::language_settings::CopilotSettings;
|
||||
use language::{
|
||||
Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,
|
||||
language_settings::{EditPredictionProvider, all_language_settings},
|
||||
language_settings::{EditPredictionProvider, all_language_settings, language_settings},
|
||||
point_from_lsp, point_to_lsp,
|
||||
};
|
||||
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
|
||||
@@ -41,7 +40,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::Dimensions;
|
||||
use util::{ResultExt, fs::remove_matching};
|
||||
use util::{ResultExt, fs::remove_matching, rel_path::RelPath};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;
|
||||
@@ -316,15 +315,6 @@ struct GlobalCopilot(Entity<Copilot>);
|
||||
|
||||
impl Global for GlobalCopilot {}
|
||||
|
||||
/// Copilot's NextEditSuggestion response, with coordinates converted to Anchors.
|
||||
struct CopilotEditPrediction {
|
||||
buffer: Entity<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
text: String,
|
||||
command: Option<lsp::Command>,
|
||||
snapshot: BufferSnapshot,
|
||||
}
|
||||
|
||||
impl Copilot {
|
||||
pub fn global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<GlobalCopilot>()
|
||||
@@ -883,19 +873,101 @@ impl Copilot {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn completions(
|
||||
pub fn completions<T>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: Anchor,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<CopilotEditPrediction>>> {
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
|
||||
}
|
||||
|
||||
pub fn completions_cycling<T>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
|
||||
}
|
||||
|
||||
pub fn accept_completion(
|
||||
&mut self,
|
||||
completion: &Completion,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let request =
|
||||
server
|
||||
.lsp
|
||||
.request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
|
||||
uuid: completion.uuid.clone(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify accepted")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn discard_completions(
|
||||
&mut self,
|
||||
completions: &[Completion],
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(_) => return Task::ready(Ok(())),
|
||||
};
|
||||
let request =
|
||||
server
|
||||
.lsp
|
||||
.request::<request::NotifyRejected>(request::NotifyRejectedParams {
|
||||
uuids: completions
|
||||
.iter()
|
||||
.map(|completion| completion.uuid.clone())
|
||||
.collect(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify rejected")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn request_completions<R, T>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
R: 'static
|
||||
+ lsp::request::Request<
|
||||
Params = request::GetCompletionsParams,
|
||||
Result = request::GetCompletionsResult,
|
||||
>,
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
self.register_buffer(buffer, cx);
|
||||
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let buffer_entity = buffer.clone();
|
||||
let lsp = server.lsp.clone();
|
||||
let registered_buffer = server
|
||||
.registered_buffers
|
||||
@@ -905,31 +977,46 @@ impl Copilot {
|
||||
let buffer = buffer.read(cx);
|
||||
let uri = registered_buffer.uri.clone();
|
||||
let position = position.to_point_utf16(buffer);
|
||||
let settings = language_settings(
|
||||
buffer.language_at(position).map(|l| l.name()),
|
||||
buffer.file(),
|
||||
cx,
|
||||
);
|
||||
let tab_size = settings.tab_size;
|
||||
let hard_tabs = settings.hard_tabs;
|
||||
let relative_path = buffer
|
||||
.file()
|
||||
.map_or(RelPath::empty().into(), |file| file.path().clone());
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let (version, snapshot) = snapshot.await?;
|
||||
let result = lsp
|
||||
.request::<NextEditSuggestions>(request::NextEditSuggestionsParams {
|
||||
text_document: lsp::VersionedTextDocumentIdentifier { uri, version },
|
||||
position: point_to_lsp(position),
|
||||
.request::<R>(request::GetCompletionsParams {
|
||||
doc: request::GetCompletionsDocument {
|
||||
uri,
|
||||
tab_size: tab_size.into(),
|
||||
indent_size: 1,
|
||||
insert_spaces: !hard_tabs,
|
||||
relative_path: relative_path.to_proto(),
|
||||
position: point_to_lsp(position),
|
||||
version: version.try_into().unwrap(),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: get completions")?;
|
||||
let completions = result
|
||||
.edits
|
||||
.completions
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
let start = snapshot
|
||||
.clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
|
||||
let end =
|
||||
snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
|
||||
CopilotEditPrediction {
|
||||
buffer: buffer_entity.clone(),
|
||||
Completion {
|
||||
uuid: completion.uuid,
|
||||
range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
text: completion.text,
|
||||
command: completion.command,
|
||||
snapshot: snapshot.clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -937,35 +1024,6 @@ impl Copilot {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn accept_completion(
|
||||
&mut self,
|
||||
completion: &CopilotEditPrediction,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
if let Some(command) = &completion.command {
|
||||
let request = server
|
||||
.lsp
|
||||
.request::<lsp::ExecuteCommand>(lsp::ExecuteCommandParams {
|
||||
command: command.command.clone(),
|
||||
arguments: command.arguments.clone().unwrap_or_default(),
|
||||
..Default::default()
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify accepted")?;
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Status {
|
||||
match &self.server {
|
||||
CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
|
||||
@@ -1188,10 +1246,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
|
||||
.await;
|
||||
if should_install {
|
||||
node_runtime
|
||||
.npm_install_packages(
|
||||
paths::copilot_dir(),
|
||||
&[(PACKAGE_NAME, &latest_version.to_string())],
|
||||
)
|
||||
.npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)])
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -1202,11 +1257,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use util::{
|
||||
path,
|
||||
paths::PathStyle,
|
||||
rel_path::{RelPath, rel_path},
|
||||
};
|
||||
use util::{path, paths::PathStyle, rel_path::rel_path};
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_management(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
use crate::{Copilot, CopilotEditPrediction};
|
||||
use crate::{Completion, Copilot};
|
||||
use anyhow::Result;
|
||||
use edit_prediction_types::{EditPrediction, EditPredictionDelegate, interpolate_edits};
|
||||
use gpui::{App, Context, Entity, Task};
|
||||
use language::{Anchor, Buffer, EditPreview, OffsetRangeExt};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
|
||||
use gpui::{App, Context, Entity, EntityId, Task};
|
||||
use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
|
||||
pub struct CopilotEditPredictionDelegate {
|
||||
completion: Option<(CopilotEditPrediction, EditPreview)>,
|
||||
cycled: bool,
|
||||
buffer_id: Option<EntityId>,
|
||||
completions: Vec<Completion>,
|
||||
active_completion_index: usize,
|
||||
file_extension: Option<String>,
|
||||
pending_refresh: Option<Task<Result<()>>>,
|
||||
pending_cycling_refresh: Option<Task<Result<()>>>,
|
||||
copilot: Entity<Copilot>,
|
||||
}
|
||||
|
||||
impl CopilotEditPredictionDelegate {
|
||||
pub fn new(copilot: Entity<Copilot>) -> Self {
|
||||
Self {
|
||||
completion: None,
|
||||
cycled: false,
|
||||
buffer_id: None,
|
||||
completions: Vec::new(),
|
||||
active_completion_index: 0,
|
||||
file_extension: None,
|
||||
pending_refresh: None,
|
||||
pending_cycling_refresh: None,
|
||||
copilot,
|
||||
}
|
||||
}
|
||||
|
||||
fn active_completion(&self) -> Option<&(CopilotEditPrediction, EditPreview)> {
|
||||
self.completion.as_ref()
|
||||
fn active_completion(&self) -> Option<&Completion> {
|
||||
self.completions.get(self.active_completion_index)
|
||||
}
|
||||
|
||||
fn push_completion(&mut self, new_completion: Completion) {
|
||||
for completion in &self.completions {
|
||||
if completion.text == new_completion.text && completion.range == new_completion.range {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.completions.push(new_completion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +64,12 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_jump_to_edit() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_refreshing(&self, _cx: &App) -> bool {
|
||||
self.pending_refresh.is_some() && self.completion.is_none()
|
||||
self.pending_refresh.is_some() && self.completions.is_empty()
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
@@ -78,96 +102,160 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
if let Some(mut completion) = completions.into_iter().next()
|
||||
&& let Some(trimmed_completion) = cx
|
||||
.update(|cx| trim_completion(&completion, cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
let preview = buffer
|
||||
.update(cx, |this, cx| {
|
||||
this.preview_edits(Arc::from(std::slice::from_ref(&trimmed_completion)), cx)
|
||||
})?
|
||||
.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
if !completions.is_empty() {
|
||||
this.cycled = false;
|
||||
this.pending_refresh = None;
|
||||
completion.range = trimmed_completion.0;
|
||||
completion.text = trimmed_completion.1.to_string();
|
||||
this.completion = Some((completion, preview));
|
||||
this.pending_cycling_refresh = None;
|
||||
this.completions.clear();
|
||||
this.active_completion_index = 0;
|
||||
this.buffer_id = Some(buffer.entity_id());
|
||||
this.file_extension = buffer.read(cx).file().and_then(|file| {
|
||||
Some(
|
||||
Path::new(file.file_name(cx))
|
||||
.extension()?
|
||||
.to_str()?
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
|
||||
for completion in completions {
|
||||
this.push_completion(completion);
|
||||
}
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.cycled {
|
||||
match direction {
|
||||
Direction::Prev => {
|
||||
self.active_completion_index = if self.active_completion_index == 0 {
|
||||
self.completions.len().saturating_sub(1)
|
||||
} else {
|
||||
self.active_completion_index - 1
|
||||
};
|
||||
}
|
||||
Direction::Next => {
|
||||
if self.completions.is_empty() {
|
||||
self.active_completion_index = 0
|
||||
} else {
|
||||
self.active_completion_index =
|
||||
(self.active_completion_index + 1) % self.completions.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
let copilot = self.copilot.clone();
|
||||
self.pending_cycling_refresh = Some(cx.spawn(async move |this, cx| {
|
||||
let completions = copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.completions_cycling(&buffer, cursor_position, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.cycled = true;
|
||||
this.file_extension = buffer.read(cx).file().and_then(|file| {
|
||||
Some(
|
||||
Path::new(file.file_name(cx))
|
||||
.extension()?
|
||||
.to_str()?
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
for completion in completions {
|
||||
this.push_completion(completion);
|
||||
}
|
||||
this.cycle(buffer, cursor_position, direction, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some((completion, _)) = self.active_completion() {
|
||||
if let Some(completion) = self.active_completion() {
|
||||
self.copilot
|
||||
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn discard(&mut self, _: &mut Context<Self>) {}
|
||||
fn discard(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
let copilot_enabled = settings.show_edit_predictions(None, cx);
|
||||
|
||||
if !copilot_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.discard_completions(&self.completions, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn suggest(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
_: language::Anchor,
|
||||
cursor_position: language::Anchor,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<EditPrediction> {
|
||||
let buffer_id = buffer.entity_id();
|
||||
let buffer = buffer.read(cx);
|
||||
let (completion, edit_preview) = self.active_completion()?;
|
||||
|
||||
if Some(buffer_id) != Some(completion.buffer.entity_id())
|
||||
let completion = self.active_completion()?;
|
||||
if Some(buffer_id) != self.buffer_id
|
||||
|| !completion.range.start.is_valid(buffer)
|
||||
|| !completion.range.end.is_valid(buffer)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let edits = vec![(
|
||||
completion.range.clone(),
|
||||
Arc::from(completion.text.as_ref()),
|
||||
)];
|
||||
let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits)
|
||||
.filter(|edits| !edits.is_empty())?;
|
||||
|
||||
Some(EditPrediction::Local {
|
||||
id: None,
|
||||
edits,
|
||||
edit_preview: Some(edit_preview.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
let mut completion_range = completion.range.to_offset(buffer);
|
||||
let prefix_len = common_prefix(
|
||||
buffer.chars_for_range(completion_range.clone()),
|
||||
completion.text.chars(),
|
||||
);
|
||||
completion_range.start += prefix_len;
|
||||
let suffix_len = common_prefix(
|
||||
buffer.reversed_chars_for_range(completion_range.clone()),
|
||||
completion.text[prefix_len..].chars().rev(),
|
||||
);
|
||||
completion_range.end = completion_range.end.saturating_sub(suffix_len);
|
||||
|
||||
fn trim_completion(
|
||||
completion: &CopilotEditPrediction,
|
||||
cx: &mut App,
|
||||
) -> Option<(Range<Anchor>, Arc<str>)> {
|
||||
let buffer = completion.buffer.read(cx);
|
||||
let mut completion_range = completion.range.to_offset(buffer);
|
||||
let prefix_len = common_prefix(
|
||||
buffer.chars_for_range(completion_range.clone()),
|
||||
completion.text.chars(),
|
||||
);
|
||||
completion_range.start += prefix_len;
|
||||
let suffix_len = common_prefix(
|
||||
buffer.reversed_chars_for_range(completion_range.clone()),
|
||||
completion.text[prefix_len..].chars().rev(),
|
||||
);
|
||||
completion_range.end = completion_range.end.saturating_sub(suffix_len);
|
||||
let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
|
||||
if completion_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
let completion_range =
|
||||
buffer.anchor_after(completion_range.start)..buffer.anchor_after(completion_range.end);
|
||||
|
||||
Some((completion_range, Arc::from(completion_text)))
|
||||
if completion_range.is_empty()
|
||||
&& completion_range.start == cursor_position.to_offset(buffer)
|
||||
{
|
||||
let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
|
||||
if completion_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
let position = cursor_position.bias_right(buffer);
|
||||
Some(EditPrediction::Local {
|
||||
id: None,
|
||||
edits: vec![(position..position, completion_text.into())],
|
||||
edit_preview: None,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +282,6 @@ mod tests {
|
||||
Point,
|
||||
language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode},
|
||||
};
|
||||
use lsp::Uri;
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use settings::{AllLanguageSettingsContent, SettingsStore};
|
||||
@@ -250,15 +337,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -299,15 +383,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -331,15 +412,12 @@ mod tests {
|
||||
// After debouncing, new Copilot completions should be requested.
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot2".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -401,6 +479,45 @@ mod tests {
|
||||
assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.set_text("fn foo() {\n \n}", window, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
|
||||
});
|
||||
});
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: " let x = 4;".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
|
||||
// Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
|
||||
editor.tab(&Default::default(), window, cx);
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
|
||||
// Using AcceptEditPrediction again accepts the suggestion.
|
||||
editor.accept_edit_prediction(&Default::default(), window, cx);
|
||||
assert!(!editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -453,15 +570,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -500,15 +614,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.123. copilot\n 456".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -575,18 +686,15 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_edit_prediction(&Default::default(), window, cx)
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -595,22 +703,15 @@ mod tests {
|
||||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\nt\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\n\nthree\n");
|
||||
|
||||
// Deleting across the original suggestion range invalidates it.
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
assert!(!editor.has_active_edit_prediction());
|
||||
@@ -652,7 +753,7 @@ mod tests {
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
use gpui::Focusable;
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
})
|
||||
.unwrap();
|
||||
let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
|
||||
@@ -664,22 +765,19 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "b = 2 + a".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Ensure copilot suggestions are shown for the first excerpt.
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
|
||||
});
|
||||
editor.show_edit_prediction(&Default::default(), window, cx);
|
||||
editor.next_edit_prediction(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
_ = editor.update(cx, |editor, _, cx| {
|
||||
@@ -693,15 +791,12 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "d = 4 + c".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Move to another excerpt, ensuring the suggestion gets cleared.
|
||||
@@ -778,18 +873,15 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_edit_prediction(&Default::default(), window, cx)
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -811,15 +903,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -841,15 +930,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -914,7 +1000,7 @@ mod tests {
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
use gpui::Focusable;
|
||||
window.focus(&editor.focus_handle(cx), cx)
|
||||
window.focus(&editor.focus_handle(cx))
|
||||
})
|
||||
.unwrap();
|
||||
let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
|
||||
@@ -925,20 +1011,16 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let mut copilot_requests = copilot_lsp
|
||||
.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
|
||||
.set_request_handler::<crate::request::GetCompletions, _, _>(
|
||||
move |_params, _cx| async move {
|
||||
Ok(crate::request::NextEditSuggestionsResult {
|
||||
edits: vec![crate::request::NextEditSuggestion {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: vec![crate::request::Completion {
|
||||
text: "next line".into(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(1, 0),
|
||||
lsp::Position::new(1, 0),
|
||||
),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
})
|
||||
},
|
||||
@@ -967,14 +1049,23 @@ mod tests {
|
||||
|
||||
fn handle_copilot_completion_request(
|
||||
lsp: &lsp::FakeLanguageServer,
|
||||
completions: Vec<crate::request::NextEditSuggestion>,
|
||||
completions: Vec<crate::request::Completion>,
|
||||
completions_cycling: Vec<crate::request::Completion>,
|
||||
) {
|
||||
lsp.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
|
||||
lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions.clone(),
|
||||
})
|
||||
}
|
||||
});
|
||||
lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
|
||||
move |_params, _cx| {
|
||||
let completions = completions.clone();
|
||||
let completions_cycling = completions_cycling.clone();
|
||||
async move {
|
||||
Ok(crate::request::NextEditSuggestionsResult {
|
||||
edits: completions.clone(),
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions_cycling.clone(),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use lsp::VersionedTextDocumentIdentifier;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub enum CheckStatus {}
|
||||
@@ -89,6 +88,72 @@ impl lsp::request::Request for SignOut {
|
||||
const METHOD: &'static str = "signOut";
|
||||
}
|
||||
|
||||
pub enum GetCompletions {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsParams {
|
||||
pub doc: GetCompletionsDocument,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsDocument {
|
||||
pub tab_size: u32,
|
||||
pub indent_size: u32,
|
||||
pub insert_spaces: bool,
|
||||
pub uri: lsp::Uri,
|
||||
pub relative_path: String,
|
||||
pub position: lsp::Position,
|
||||
pub version: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsResult {
|
||||
pub completions: Vec<Completion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Completion {
|
||||
pub text: String,
|
||||
pub position: lsp::Position,
|
||||
pub uuid: String,
|
||||
pub range: lsp::Range,
|
||||
pub display_text: String,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for GetCompletions {
|
||||
type Params = GetCompletionsParams;
|
||||
type Result = GetCompletionsResult;
|
||||
const METHOD: &'static str = "getCompletions";
|
||||
}
|
||||
|
||||
pub enum GetCompletionsCycling {}
|
||||
|
||||
impl lsp::request::Request for GetCompletionsCycling {
|
||||
type Params = GetCompletionsParams;
|
||||
type Result = GetCompletionsResult;
|
||||
const METHOD: &'static str = "getCompletionsCycling";
|
||||
}
|
||||
|
||||
pub enum LogMessage {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogMessageParams {
|
||||
pub level: u8,
|
||||
pub message: String,
|
||||
pub metadata_str: String,
|
||||
pub extra: Vec<String>,
|
||||
}
|
||||
|
||||
impl lsp::notification::Notification for LogMessage {
|
||||
type Params = LogMessageParams;
|
||||
const METHOD: &'static str = "LogMessage";
|
||||
}
|
||||
|
||||
pub enum StatusNotification {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -158,36 +223,3 @@ impl lsp::request::Request for NotifyRejected {
|
||||
type Result = String;
|
||||
const METHOD: &'static str = "notifyRejected";
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestions;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestionsParams {
|
||||
pub(crate) text_document: VersionedTextDocumentIdentifier,
|
||||
pub(crate) position: lsp::Position,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestion {
|
||||
pub text: String,
|
||||
pub text_document: VersionedTextDocumentIdentifier,
|
||||
pub range: lsp::Range,
|
||||
pub command: Option<lsp::Command>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestionsResult {
|
||||
pub edits: Vec<NextEditSuggestion>,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for NextEditSuggestions {
|
||||
type Params = NextEditSuggestionsParams;
|
||||
type Result = NextEditSuggestionsResult;
|
||||
|
||||
const METHOD: &'static str = "textDocument/copilotInlineEdit";
|
||||
}
|
||||
|
||||
@@ -435,8 +435,8 @@ impl Render for CopilotCodeVerification {
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
window.focus(&this.focus_handle, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _| {
|
||||
window.focus(&this.focus_handle);
|
||||
}))
|
||||
.child(
|
||||
Vector::new(VectorName::ZedXCopilot, rems(8.), rems(4.))
|
||||
|
||||
@@ -577,7 +577,7 @@ impl DebugPanel {
|
||||
menu
|
||||
});
|
||||
|
||||
window.focus(&context_menu.focus_handle(cx), cx);
|
||||
window.focus(&context_menu.focus_handle(cx));
|
||||
let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
|
||||
this.context_menu.take();
|
||||
cx.notify();
|
||||
@@ -1052,7 +1052,7 @@ impl DebugPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
debug_assert!(self.sessions_with_children.contains_key(&session_item));
|
||||
session_item.focus_handle(cx).focus(window, cx);
|
||||
session_item.focus_handle(cx).focus(window);
|
||||
session_item.update(cx, |this, cx| {
|
||||
this.running_state().update(cx, |this, cx| {
|
||||
this.go_to_selected_stack_frame(window, cx);
|
||||
|
||||
@@ -574,7 +574,7 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Launch => NewProcessMode::Task,
|
||||
};
|
||||
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
}))
|
||||
.on_action(
|
||||
cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
|
||||
@@ -585,7 +585,7 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Launch => NewProcessMode::Attach,
|
||||
};
|
||||
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
@@ -602,7 +602,7 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Task.to_string(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewProcessMode::Task;
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
@@ -611,7 +611,7 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Debug.to_string(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewProcessMode::Debug;
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
@@ -629,7 +629,7 @@ impl Render for NewProcessModal {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
@@ -638,7 +638,7 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Launch.to_string(),
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.mode = NewProcessMode::Launch;
|
||||
this.mode_focus_handle(cx).focus(window, cx);
|
||||
this.mode_focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
@@ -840,17 +840,17 @@ impl ConfigureMode {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus_next(cx);
|
||||
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
|
||||
window.focus_next();
|
||||
}
|
||||
|
||||
fn on_tab_prev(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
window.focus_prev(cx);
|
||||
window.focus_prev();
|
||||
}
|
||||
|
||||
fn render(
|
||||
@@ -923,7 +923,7 @@ impl AttachMode {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
window.focus(&modal.focus_handle(cx), cx);
|
||||
window.focus(&modal.focus_handle(cx));
|
||||
|
||||
modal
|
||||
});
|
||||
|
||||
@@ -83,8 +83,8 @@ impl Render for DebuggerOnboardingModal {
|
||||
debugger_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
|
||||
this.focus_handle.focus(window, cx);
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -604,7 +604,7 @@ impl DebugTerminal {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let focus_subscription = cx.on_focus(&focus_handle, window, |this, window, cx| {
|
||||
if let Some(terminal) = this.terminal.as_ref() {
|
||||
terminal.focus_handle(cx).focus(window, cx);
|
||||
terminal.focus_handle(cx).focus(window);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ impl BreakpointList {
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.input.focus_handle(cx).contains_focused(window, cx) {
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
} else if self.strip_mode.is_some() {
|
||||
self.strip_mode.take();
|
||||
cx.notify();
|
||||
@@ -364,9 +364,9 @@ impl BreakpointList {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
} else {
|
||||
handle.focus(window, cx);
|
||||
handle.focus(window);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -627,7 +627,7 @@ impl BreakpointList {
|
||||
.on_click({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_, window, cx| {
|
||||
focus_handle.focus(window, cx);
|
||||
focus_handle.focus(window);
|
||||
window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
|
||||
}
|
||||
}),
|
||||
@@ -654,7 +654,7 @@ impl BreakpointList {
|
||||
)
|
||||
.on_click({
|
||||
move |_, window, cx| {
|
||||
focus_handle.focus(window, cx);
|
||||
focus_handle.focus(window);
|
||||
window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -105,7 +105,7 @@ impl Console {
|
||||
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
|
||||
cx.on_focus(&focus_handle, window, |console, window, cx| {
|
||||
if console.is_running(cx) {
|
||||
console.query_bar.focus_handle(cx).focus(window, cx);
|
||||
console.query_bar.focus_handle(cx).focus(window);
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -403,7 +403,7 @@ impl MemoryView {
|
||||
this.set_placeholder_text("Write to Selected Memory Range", window, cx);
|
||||
});
|
||||
self.is_writing_memory = true;
|
||||
self.query_editor.focus_handle(cx).focus(window, cx);
|
||||
self.query_editor.focus_handle(cx).focus(window);
|
||||
} else {
|
||||
self.query_editor.update(cx, |this, cx| {
|
||||
this.clear(window, cx);
|
||||
|
||||
@@ -529,7 +529,7 @@ impl VariableList {
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.edited_path.take();
|
||||
self.focus_handle.focus(window, cx);
|
||||
self.focus_handle.focus(window);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,7 @@ impl VariableList {
|
||||
editor.select_all(&editor::actions::SelectAll, window, cx);
|
||||
editor
|
||||
});
|
||||
editor.focus_handle(cx).focus(window, cx);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
editor
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ impl BufferDiagnosticsEditor {
|
||||
// `BufferDiagnosticsEditor` instance.
|
||||
EditorEvent::Focused => {
|
||||
if buffer_diagnostics_editor.multibuffer.read(cx).is_empty() {
|
||||
window.focus(&buffer_diagnostics_editor.focus_handle, cx);
|
||||
window.focus(&buffer_diagnostics_editor.focus_handle);
|
||||
}
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
@@ -517,7 +517,7 @@ impl BufferDiagnosticsEditor {
|
||||
.editor
|
||||
.read(cx)
|
||||
.focus_handle(cx)
|
||||
.focus(window, cx);
|
||||
.focus(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -617,7 +617,7 @@ impl BufferDiagnosticsEditor {
|
||||
// not empty, focus on the editor instead, which will allow the user to
|
||||
// start interacting and editing the buffer's contents.
|
||||
if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||
self.editor.focus_handle(cx).focus(window, cx)
|
||||
self.editor.focus_handle(cx).focus(window)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -315,6 +315,6 @@ impl DiagnosticBlock {
|
||||
editor.change_selections(Default::default(), window, cx, |s| {
|
||||
s.select_ranges([range.start..range.start]);
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ impl ProjectDiagnosticsEditor {
|
||||
match event {
|
||||
EditorEvent::Focused => {
|
||||
if this.multibuffer.read(cx).is_empty() {
|
||||
window.focus(&this.focus_handle, cx);
|
||||
window.focus(&this.focus_handle);
|
||||
}
|
||||
}
|
||||
EditorEvent::Blurred => this.close_diagnosticless_buffers(cx, false),
|
||||
@@ -434,7 +434,7 @@ impl ProjectDiagnosticsEditor {
|
||||
|
||||
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||
self.editor.focus_handle(cx).focus(window, cx)
|
||||
self.editor.focus_handle(cx).focus(window)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -650,7 +650,7 @@ impl ProjectDiagnosticsEditor {
|
||||
})
|
||||
});
|
||||
if this.focus_handle.is_focused(window) {
|
||||
this.editor.read(cx).focus_handle(cx).focus(window, cx);
|
||||
this.editor.read(cx).focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{cmp, sync::Arc};
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use cloud_llm_client::EditPredictionRejectReason;
|
||||
use edit_prediction_types::{DataCollectionState, EditPredictionDelegate};
|
||||
use edit_prediction_types::{DataCollectionState, Direction, EditPredictionDelegate};
|
||||
use gpui::{App, Entity, prelude::*};
|
||||
use language::{Buffer, ToPoint as _};
|
||||
use project::Project;
|
||||
@@ -139,6 +139,15 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
|
||||
});
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: Direction,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
self.store.update(cx, |store, cx| {
|
||||
store.accept_current_prediction(&self.project, cx);
|
||||
|
||||
@@ -8,7 +8,8 @@ use gpui_tokio::Tokio;
|
||||
use language::LanguageRegistry;
|
||||
use language_extension::LspAccess;
|
||||
use node_runtime::{NodeBinaryOptions, NodeRuntime};
|
||||
use project::{Project, project_settings::ProjectSettings};
|
||||
use project::Project;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use release_channel::{AppCommitSha, AppVersion};
|
||||
use reqwest_client::ReqwestClient;
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
||||
@@ -179,7 +179,6 @@ async fn setup_project(
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -95,6 +95,13 @@ pub trait EditPredictionDelegate: 'static + Sized {
|
||||
debounce: bool,
|
||||
cx: &mut Context<Self>,
|
||||
);
|
||||
fn cycle(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut Context<Self>,
|
||||
);
|
||||
fn accept(&mut self, cx: &mut Context<Self>);
|
||||
fn discard(&mut self, cx: &mut Context<Self>);
|
||||
fn did_show(&mut self, _cx: &mut Context<Self>) {}
|
||||
@@ -129,6 +136,13 @@ pub trait EditPredictionDelegateHandle {
|
||||
debounce: bool,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn cycle(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn did_show(&self, cx: &mut App);
|
||||
fn accept(&self, cx: &mut App);
|
||||
fn discard(&self, cx: &mut App);
|
||||
@@ -201,6 +215,18 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.cycle(buffer, cursor_position, direction, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn accept(&self, cx: &mut App) {
|
||||
self.update(cx, |this, cx| this.accept(cx))
|
||||
}
|
||||
|
||||
@@ -305,7 +305,7 @@ impl RatePredictionsModal {
|
||||
&& prediction.id == prev_prediction.prediction.id
|
||||
{
|
||||
if focus {
|
||||
window.focus(&prev_prediction.feedback_editor.focus_handle(cx), cx);
|
||||
window.focus(&prev_prediction.feedback_editor.focus_handle(cx));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ fn editor_input_with_1000_cursors(bencher: &mut Bencher<'_>, cx: &TestAppContext
|
||||
);
|
||||
editor
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ fn open_editor_with_one_long_line(bencher: &mut Bencher<'_>, args: &(String, Tes
|
||||
editor.set_style(editor::EditorStyle::default(), window, cx);
|
||||
editor
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor
|
||||
});
|
||||
});
|
||||
@@ -100,7 +100,7 @@ fn editor_render(bencher: &mut Bencher<'_>, cx: &TestAppContext) {
|
||||
editor.set_style(editor::EditorStyle::default(), window, cx);
|
||||
editor
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor
|
||||
});
|
||||
|
||||
|
||||
@@ -14,57 +14,8 @@
|
||||
//! - [`DisplayMap`] that adds background highlights to the regions of text.
|
||||
//! Each one of those builds on top of preceding map.
|
||||
//!
|
||||
//! ## Structure of the display map layers
|
||||
//!
|
||||
//! Each layer in the map (and the multibuffer itself to some extent) has a few
|
||||
//! structures that are used to implement the public API available to the layer
|
||||
//! above:
|
||||
//! - a `Transform` type - this represents a region of text that the layer in
|
||||
//! question is "managing", that it transforms into a more "processed" text
|
||||
//! for the layer above. For example, the inlay map has an `enum Transform`
|
||||
//! that has two variants:
|
||||
//! - `Isomorphic`, representing a region of text that has no inlay hints (i.e.
|
||||
//! is passed through the map transparently)
|
||||
//! - `Inlay`, representing a location where an inlay hint is to be inserted.
|
||||
//! - a `TransformSummary` type, which is usually a struct with two fields:
|
||||
//! [`input: TextSummary`][`TextSummary`] and [`output: TextSummary`][`TextSummary`]. Here,
|
||||
//! `input` corresponds to "text in the layer below", and `output` corresponds to the text
|
||||
//! exposed to the layer above. So in the inlay map case, a `Transform::Isomorphic`'s summary is
|
||||
//! just `input = output = summary`, where `summary` is the [`TextSummary`] stored in that
|
||||
//! variant. Conversely, a `Transform::Inlay` always has an empty `input` summary, because it's
|
||||
//! not "replacing" any text that exists on disk. The `output` is the summary of the inlay text
|
||||
//! to be injected. - Various newtype wrappers for co-ordinate spaces (e.g. [`WrapRow`]
|
||||
//! represents a row index, after soft-wrapping (and all lower layers)).
|
||||
//! - A `Snapshot` type (e.g. [`InlaySnapshot`]) that captures the state of a layer at a specific
|
||||
//! point in time.
|
||||
//! - various APIs which drill through the layers below to work with the underlying text. Notably:
|
||||
//! - `fn text_summary_for_offset()` returns a [`TextSummary`] for the range in the co-ordinate
|
||||
//! space that the map in question is responsible for.
|
||||
//! - `fn <A>_point_to_<B>_point()` converts a point in co-ordinate space `A` into co-ordinate
|
||||
//! space `B`.
|
||||
//! - A [`RowInfo`] iterator (e.g. [`InlayBufferRows`]) and a [`Chunk`] iterator
|
||||
//! (e.g. [`InlayChunks`])
|
||||
//! - A `sync` function (e.g. [`InlayMap::sync`]) that takes a snapshot and list of [`Edit<T>`]s,
|
||||
//! and returns a new snapshot and a list of transformed [`Edit<S>`]s. Note that the generic
|
||||
//! parameter on `Edit` changes, since these methods take in edits in the co-ordinate space of
|
||||
//! the lower layer, and return edits in their own co-ordinate space. The term "edit" is
|
||||
//! slightly misleading, since an [`Edit<T>`] doesn't tell you what changed - rather it can be
|
||||
//! thought of as a "region to invalidate". In theory, it would be correct to always use a
|
||||
//! single edit that covers the entire range. However, this would lead to lots of unnecessary
|
||||
//! recalculation.
|
||||
//!
|
||||
//! See the docs for the [`inlay_map`] module for a more in-depth explanation of how a single layer
|
||||
//! works.
|
||||
//!
|
||||
//! [Editor]: crate::Editor
|
||||
//! [EditorElement]: crate::element::EditorElement
|
||||
//! [`TextSummary`]: multi_buffer::MBTextSummary
|
||||
//! [`WrapRow`]: wrap_map::WrapRow
|
||||
//! [`InlayBufferRows`]: inlay_map::InlayBufferRows
|
||||
//! [`InlayChunks`]: inlay_map::InlayChunks
|
||||
//! [`Edit<T>`]: text::Edit
|
||||
//! [`Edit<S>`]: text::Edit
|
||||
//! [`Chunk`]: language::Chunk
|
||||
|
||||
#[macro_use]
|
||||
mod dimensions;
|
||||
|
||||
@@ -545,7 +545,7 @@ impl BlockMap {
|
||||
{
|
||||
let max_point = wrap_snapshot.max_point();
|
||||
let edit_start = wrap_snapshot.prev_row_boundary(max_point);
|
||||
let edit_end = max_point.row() + WrapRow(1); // this is end of file
|
||||
let edit_end = max_point.row() + WrapRow(1);
|
||||
edits = edits.compose([WrapEdit {
|
||||
old: edit_start..edit_end,
|
||||
new: edit_start..edit_end,
|
||||
@@ -715,7 +715,6 @@ impl BlockMap {
|
||||
let placement = block.placement.to_wrap_row(wrap_snapshot)?;
|
||||
if let BlockPlacement::Above(row) = placement
|
||||
&& row < new_start
|
||||
// this will be true more often now
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
//! The inlay map. See the [`display_map`][super] docs for an overview of how the inlay map fits
|
||||
//! into the rest of the [`DisplayMap`][super::DisplayMap]. Much of the documentation for this
|
||||
//! module generalizes to other layers.
|
||||
//!
|
||||
//! The core of this module is the [`InlayMap`] struct, which maintains a vec of [`Inlay`]s, and
|
||||
//! [`InlaySnapshot`], which holds a sum tree of [`Transform`]s.
|
||||
|
||||
use crate::{
|
||||
ChunkRenderer, HighlightStyles,
|
||||
inlays::{Inlay, InlayContent},
|
||||
@@ -76,9 +69,7 @@ impl sum_tree::Item for Transform {
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct TransformSummary {
|
||||
/// Summary of the text before inlays have been applied.
|
||||
input: MBTextSummary,
|
||||
/// Summary of the text after inlays have been applied.
|
||||
output: MBTextSummary,
|
||||
}
|
||||
|
||||
|
||||
@@ -840,62 +840,35 @@ impl WrapSnapshot {
|
||||
self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
|
||||
}
|
||||
|
||||
/// Try to find a TabRow start that is also a WrapRow start
|
||||
/// Every TabRow start is a WrapRow start
|
||||
#[ztracing::instrument(skip_all, fields(point=?point))]
|
||||
pub fn prev_row_boundary(&self, point: WrapPoint) -> WrapRow {
|
||||
#[ztracing::instrument(skip_all, fields(point=?point, ret))]
|
||||
pub fn prev_row_boundary(&self, mut point: WrapPoint) -> WrapRow {
|
||||
if self.transforms.is_empty() {
|
||||
return WrapRow(0);
|
||||
}
|
||||
|
||||
let point = WrapPoint::new(point.row(), 0);
|
||||
*point.column_mut() = 0;
|
||||
|
||||
let mut cursor = self
|
||||
.transforms
|
||||
.cursor::<Dimensions<WrapPoint, TabPoint>>(());
|
||||
|
||||
// start
|
||||
cursor.seek(&point, Bias::Right);
|
||||
// end
|
||||
if cursor.item().is_none() {
|
||||
cursor.prev();
|
||||
}
|
||||
|
||||
// real newline fake fake
|
||||
// text: helloworldasldlfjasd\njdlasfalsk\naskdjfasdkfj\n
|
||||
// dimensions v v v v v
|
||||
// transforms |-------|-----NW----|-----W------|-----W------|
|
||||
// cursor ^ ^^^^^^^^^^^^^ ^
|
||||
// (^) ^^^^^^^^^^^^^^
|
||||
// point: ^
|
||||
// point(col_zero): (^)
|
||||
|
||||
// start
|
||||
while let Some(transform) = cursor.item() {
|
||||
if transform.is_isomorphic() {
|
||||
// this transform only has real linefeeds
|
||||
let tab_summary = &transform.summary.input;
|
||||
// is the wrap just before the end of the transform a tab row?
|
||||
// thats only if this transform has at least one newline
|
||||
//
|
||||
// "this wrap row is a tab row" <=> self.to_tab_point(WrapPoint::new(wrap_row, 0)).column() == 0
|
||||
|
||||
// Note on comparison:
|
||||
// We have code that relies on this to be row > 1
|
||||
// It should work with row >= 1 but it does not :(
|
||||
//
|
||||
// That means that if every line is wrapped we walk back all the
|
||||
// way to the start. Which invalidates the entire state triggering
|
||||
// a full re-render.
|
||||
if tab_summary.lines.row > 1 {
|
||||
let wrap_point_at_end = cursor.end().0.row();
|
||||
return cmp::min(wrap_point_at_end - RowDelta(1), point.row());
|
||||
} else if cursor.start().1.column() == 0 {
|
||||
return cmp::min(cursor.end().0.row(), point.row());
|
||||
}
|
||||
if transform.is_isomorphic() && cursor.start().1.column() == 0 {
|
||||
return cmp::min(cursor.end().0.row(), point.row());
|
||||
} else {
|
||||
cursor.prev();
|
||||
}
|
||||
|
||||
cursor.prev();
|
||||
}
|
||||
// end
|
||||
|
||||
WrapRow(0)
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[ztracing::instrument(skip_all)]
|
||||
@@ -918,11 +891,13 @@ impl WrapSnapshot {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ztracing::instrument(skip_all)]
|
||||
pub fn text(&self) -> String {
|
||||
self.text_chunks(WrapRow(0)).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ztracing::instrument(skip_all)]
|
||||
pub fn text_chunks(&self, wrap_row: WrapRow) -> impl Iterator<Item = &str> {
|
||||
self.chunks(
|
||||
wrap_row..self.max_point().row() + WrapRow(1),
|
||||
@@ -1323,71 +1298,6 @@ mod tests {
|
||||
use text::Rope;
|
||||
use theme::LoadThemes;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prev_row_boundary(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
fn test_wrap_snapshot(
|
||||
text: &str,
|
||||
soft_wrap_every: usize, // font size multiple
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> WrapSnapshot {
|
||||
let text_system = cx.read(|cx| cx.text_system().clone());
|
||||
let tab_size = 4.try_into().unwrap();
|
||||
let font = test_font();
|
||||
let _font_id = text_system.resolve_font(&font);
|
||||
let font_size = px(14.0);
|
||||
// this is very much an estimate to try and get the wrapping to
|
||||
// occur at `soft_wrap_every` we check that it pans out for every test case
|
||||
let soft_wrapping = Some(font_size * soft_wrap_every * 0.6);
|
||||
|
||||
let buffer = cx.new(|cx| language::Buffer::local(text, cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
let (_inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
|
||||
let (_fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size);
|
||||
let tabs_snapshot = tab_map.set_max_expansion_column(32);
|
||||
let (_wrap_map, wrap_snapshot) =
|
||||
cx.update(|cx| WrapMap::new(tabs_snapshot, font, font_size, soft_wrapping, cx));
|
||||
|
||||
wrap_snapshot
|
||||
}
|
||||
|
||||
// These two should pass but dont, see the comparison note in
|
||||
// prev_row_boundary about why.
|
||||
//
|
||||
// // 0123 4567 wrap_rows
|
||||
// let wrap_snapshot = test_wrap_snapshot("1234\n5678", 1, cx);
|
||||
// assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8");
|
||||
// let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
|
||||
// assert_eq!(row.0, 3);
|
||||
|
||||
// // 012 345 678 wrap_rows
|
||||
// let wrap_snapshot = test_wrap_snapshot("123\n456\n789", 1, cx);
|
||||
// assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8\n9");
|
||||
// let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
|
||||
// assert_eq!(row.0, 5);
|
||||
|
||||
// 012345678 wrap_rows
|
||||
let wrap_snapshot = test_wrap_snapshot("123456789", 1, cx);
|
||||
assert_eq!(wrap_snapshot.text(), "1\n2\n3\n4\n5\n6\n7\n8\n9");
|
||||
let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
|
||||
assert_eq!(row.0, 0);
|
||||
|
||||
// 111 2222 44 wrap_rows
|
||||
let wrap_snapshot = test_wrap_snapshot("123\n4567\n\n89", 4, cx);
|
||||
assert_eq!(wrap_snapshot.text(), "123\n4567\n\n89");
|
||||
let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
|
||||
assert_eq!(row.0, 2);
|
||||
|
||||
// 11 2223 wrap_rows
|
||||
let wrap_snapshot = test_wrap_snapshot("12\n3456\n\n", 3, cx);
|
||||
assert_eq!(wrap_snapshot.text(), "12\n345\n6\n\n");
|
||||
let row = wrap_snapshot.prev_row_boundary(wrap_snapshot.max_point());
|
||||
assert_eq!(row.0, 3);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
|
||||
// todo this test is flaky
|
||||
|
||||
@@ -485,6 +485,15 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate {
|
||||
) {
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: gpui::Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: edit_prediction_types::Direction,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
@@ -552,6 +561,15 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate {
|
||||
) {
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: gpui::Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: edit_prediction_types::Direction,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
@@ -73,7 +73,11 @@ pub use multi_buffer::{
|
||||
pub use split::SplittableEditor;
|
||||
pub use text::Bias;
|
||||
|
||||
use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
|
||||
use ::git::{
|
||||
Restore,
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
status::FileStatus,
|
||||
};
|
||||
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use blink_manager::BlinkManager;
|
||||
@@ -120,9 +124,8 @@ use language::{
|
||||
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
|
||||
BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
|
||||
DiagnosticEntryRef, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
|
||||
IndentSize, Language, LanguageName, LanguageRegistry, LanguageScope, OffsetRangeExt,
|
||||
OutlineItem, Point, Runnable, Selection, SelectionGoal, TextObject, TransactionId,
|
||||
TreeSitterOptions, WordsQuery,
|
||||
IndentSize, Language, LanguageName, LanguageRegistry, OffsetRangeExt, OutlineItem, Point,
|
||||
Runnable, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
|
||||
language_settings::{
|
||||
self, LanguageSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
|
||||
all_language_settings, language_settings,
|
||||
@@ -2060,34 +2063,46 @@ impl Editor {
|
||||
})
|
||||
});
|
||||
});
|
||||
let edited_buffers_already_open = {
|
||||
let other_editors: Vec<Entity<Editor>> = workspace
|
||||
.read(cx)
|
||||
.panes()
|
||||
.iter()
|
||||
.flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
|
||||
.filter(|editor| editor.entity_id() != cx.entity_id())
|
||||
.collect();
|
||||
|
||||
Self::open_transaction_for_hidden_buffers(
|
||||
workspace,
|
||||
transaction.clone(),
|
||||
"Rename".to_string(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
project::Event::WorkspaceEditApplied(transaction) => {
|
||||
let Some(workspace) = editor.workspace() else {
|
||||
return;
|
||||
};
|
||||
let Some(active_editor) = workspace.read(cx).active_item_as::<Self>(cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if active_editor.entity_id() == cx.entity_id() {
|
||||
Self::open_transaction_for_hidden_buffers(
|
||||
workspace,
|
||||
transaction.clone(),
|
||||
"LSP Edit".to_string(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
transaction.0.keys().all(|buffer| {
|
||||
other_editors.iter().any(|editor| {
|
||||
let multi_buffer = editor.read(cx).buffer();
|
||||
multi_buffer.read(cx).is_singleton()
|
||||
&& multi_buffer.read(cx).as_singleton().map_or(
|
||||
false,
|
||||
|singleton| {
|
||||
singleton.entity_id() == buffer.entity_id()
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
};
|
||||
if !edited_buffers_already_open {
|
||||
let workspace = workspace.downgrade();
|
||||
let transaction = transaction.clone();
|
||||
cx.defer_in(window, move |_, window, cx| {
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
Self::open_project_transaction(
|
||||
&editor,
|
||||
workspace,
|
||||
transaction,
|
||||
"Rename".to_string(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3812,7 +3827,7 @@ impl Editor {
|
||||
) {
|
||||
if !self.focus_handle.is_focused(window) {
|
||||
self.last_focused_descendant = None;
|
||||
window.focus(&self.focus_handle, cx);
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
@@ -3917,7 +3932,7 @@ impl Editor {
|
||||
) {
|
||||
if !self.focus_handle.is_focused(window) {
|
||||
self.last_focused_descendant = None;
|
||||
window.focus(&self.focus_handle, cx);
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
@@ -4787,51 +4802,205 @@ impl Editor {
|
||||
let end = selection.end;
|
||||
let selection_is_empty = start == end;
|
||||
let language_scope = buffer.language_scope_at(start);
|
||||
let (comment_delimiter, doc_delimiter, newline_formatting) =
|
||||
if let Some(language) = &language_scope {
|
||||
let mut newline_formatting =
|
||||
NewlineFormatting::new(&buffer, start..end, language);
|
||||
let (
|
||||
comment_delimiter,
|
||||
doc_delimiter,
|
||||
insert_extra_newline,
|
||||
indent_on_newline,
|
||||
indent_on_extra_newline,
|
||||
) = if let Some(language) = &language_scope {
|
||||
let mut insert_extra_newline =
|
||||
insert_extra_newline_brackets(&buffer, start..end, language)
|
||||
|| insert_extra_newline_tree_sitter(&buffer, start..end);
|
||||
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
let delimiters = language.line_comment_prefixes();
|
||||
let max_len_of_delimiter =
|
||||
delimiters.iter().map(|delimiter| delimiter.len()).max()?;
|
||||
let (snapshot, range) =
|
||||
buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
let comment_candidate = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_len_of_delimiter)
|
||||
.collect::<String>();
|
||||
let (delimiter, trimmed_len) = delimiters
|
||||
.iter()
|
||||
.filter_map(|delimiter| {
|
||||
let prefix = delimiter.trim_end();
|
||||
if comment_candidate.starts_with(prefix) {
|
||||
Some((delimiter, prefix.len()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.max_by_key(|(_, len)| *len)?;
|
||||
|
||||
if let Some(BlockCommentConfig {
|
||||
start: block_start, ..
|
||||
}) = language.block_comment()
|
||||
{
|
||||
let block_start_trimmed = block_start.trim_end();
|
||||
if block_start_trimmed.starts_with(delimiter.trim_end()) {
|
||||
let line_content = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(num_of_whitespaces)
|
||||
.take(block_start_trimmed.len())
|
||||
.collect::<String>();
|
||||
|
||||
if line_content.starts_with(block_start_trimmed) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_is_placed_after_comment_marker =
|
||||
num_of_whitespaces + trimmed_len <= start_point.column as usize;
|
||||
if cursor_is_placed_after_comment_marker {
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let mut indent_on_newline = IndentSize::spaces(0);
|
||||
let mut indent_on_extra_newline = IndentSize::spaces(0);
|
||||
|
||||
let doc_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
let BlockCommentConfig {
|
||||
start: start_tag,
|
||||
end: end_tag,
|
||||
prefix: delimiter,
|
||||
tab_size: len,
|
||||
} = language.documentation_comment()?;
|
||||
let is_within_block_comment = buffer
|
||||
.language_scope_at(start_point)
|
||||
.is_some_and(|scope| scope.override_name() == Some("comment"));
|
||||
if !is_within_block_comment {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (snapshot, range) =
|
||||
buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
|
||||
// It is safe to use a column from MultiBufferPoint in context of a single buffer ranges, because we're only ever looking at a single line at a time.
|
||||
let column = start_point.column;
|
||||
let cursor_is_after_start_tag = {
|
||||
let start_tag_len = start_tag.len();
|
||||
let start_tag_line = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(start_tag_len)
|
||||
.collect::<String>();
|
||||
if start_tag_line.starts_with(start_tag.as_ref()) {
|
||||
num_of_whitespaces + start_tag_len <= column as usize
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let cursor_is_after_delimiter = {
|
||||
let delimiter_trim = delimiter.trim_end();
|
||||
let delimiter_line = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(delimiter_trim.len())
|
||||
.collect::<String>();
|
||||
if delimiter_line.starts_with(delimiter_trim) {
|
||||
num_of_whitespaces + delimiter_trim.len() <= column as usize
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let cursor_is_before_end_tag_if_exists = {
|
||||
let mut char_position = 0u32;
|
||||
let mut end_tag_offset = None;
|
||||
|
||||
'outer: for chunk in snapshot.text_for_range(range) {
|
||||
if let Some(byte_pos) = chunk.find(&**end_tag) {
|
||||
let chars_before_match =
|
||||
chunk[..byte_pos].chars().count() as u32;
|
||||
end_tag_offset =
|
||||
Some(char_position + chars_before_match);
|
||||
break 'outer;
|
||||
}
|
||||
char_position += chunk.chars().count() as u32;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline
|
||||
{
|
||||
return None;
|
||||
if let Some(end_tag_offset) = end_tag_offset {
|
||||
let cursor_is_before_end_tag = column <= end_tag_offset;
|
||||
if cursor_is_after_start_tag {
|
||||
if cursor_is_before_end_tag {
|
||||
insert_extra_newline = true;
|
||||
}
|
||||
let cursor_is_at_start_of_end_tag =
|
||||
column == end_tag_offset;
|
||||
if cursor_is_at_start_of_end_tag {
|
||||
indent_on_extra_newline.len = *len;
|
||||
}
|
||||
}
|
||||
cursor_is_before_end_tag
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
return comment_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
);
|
||||
});
|
||||
|
||||
let doc_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
if (cursor_is_after_start_tag || cursor_is_after_delimiter)
|
||||
&& cursor_is_before_end_tag_if_exists
|
||||
{
|
||||
if cursor_is_after_start_tag {
|
||||
indent_on_newline.len = *len;
|
||||
}
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
return documentation_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
&mut newline_formatting,
|
||||
);
|
||||
});
|
||||
|
||||
(comment_delimiter, doc_delimiter, newline_formatting)
|
||||
} else {
|
||||
(None, None, NewlineFormatting::default())
|
||||
};
|
||||
(
|
||||
comment_delimiter,
|
||||
doc_delimiter,
|
||||
insert_extra_newline,
|
||||
indent_on_newline,
|
||||
indent_on_extra_newline,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
IndentSize::default(),
|
||||
IndentSize::default(),
|
||||
)
|
||||
};
|
||||
|
||||
let prevent_auto_indent = doc_delimiter.is_some();
|
||||
let delimiter = comment_delimiter.or(doc_delimiter);
|
||||
@@ -4841,28 +5010,28 @@ impl Editor {
|
||||
let mut new_text = String::with_capacity(
|
||||
1 + capacity_for_delimiter
|
||||
+ existing_indent.len as usize
|
||||
+ newline_formatting.indent_on_newline.len as usize
|
||||
+ newline_formatting.indent_on_extra_newline.len as usize,
|
||||
+ indent_on_newline.len as usize
|
||||
+ indent_on_extra_newline.len as usize,
|
||||
);
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(newline_formatting.indent_on_newline.chars());
|
||||
new_text.extend(indent_on_newline.chars());
|
||||
|
||||
if let Some(delimiter) = &delimiter {
|
||||
new_text.push_str(delimiter);
|
||||
}
|
||||
|
||||
if newline_formatting.insert_extra_newline {
|
||||
if insert_extra_newline {
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(newline_formatting.indent_on_extra_newline.chars());
|
||||
new_text.extend(indent_on_extra_newline.chars());
|
||||
}
|
||||
|
||||
let anchor = buffer.anchor_after(end);
|
||||
let new_selection = selection.map(|_| anchor);
|
||||
(
|
||||
((start..end, new_text), prevent_auto_indent),
|
||||
(newline_formatting.insert_extra_newline, new_selection),
|
||||
(insert_extra_newline, new_selection),
|
||||
)
|
||||
})
|
||||
.unzip()
|
||||
@@ -6503,52 +6672,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn open_transaction_for_hidden_buffers(
|
||||
workspace: Entity<Workspace>,
|
||||
transaction: ProjectTransaction,
|
||||
title: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if transaction.0.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let edited_buffers_already_open = {
|
||||
let other_editors: Vec<Entity<Editor>> = workspace
|
||||
.read(cx)
|
||||
.panes()
|
||||
.iter()
|
||||
.flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
|
||||
.filter(|editor| editor.entity_id() != cx.entity_id())
|
||||
.collect();
|
||||
|
||||
transaction.0.keys().all(|buffer| {
|
||||
other_editors.iter().any(|editor| {
|
||||
let multi_buffer = editor.read(cx).buffer();
|
||||
multi_buffer.read(cx).is_singleton()
|
||||
&& multi_buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.map_or(false, |singleton| {
|
||||
singleton.entity_id() == buffer.entity_id()
|
||||
})
|
||||
})
|
||||
})
|
||||
};
|
||||
if !edited_buffers_already_open {
|
||||
let workspace = workspace.downgrade();
|
||||
cx.defer_in(window, move |_, window, cx| {
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
Self::open_project_transaction(&editor, workspace, transaction, title, cx)
|
||||
.await
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn open_project_transaction(
|
||||
editor: &WeakEntity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -6708,7 +6831,7 @@ impl Editor {
|
||||
})
|
||||
})
|
||||
.on_click(cx.listener(move |editor, _: &ClickEvent, window, cx| {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.toggle_code_actions(
|
||||
&crate::actions::ToggleCodeActions {
|
||||
deployed_from: Some(crate::actions::CodeActionSource::Indicator(
|
||||
@@ -7464,6 +7587,26 @@ impl Editor {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn cycle_edit_prediction(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
let provider = self.edit_prediction_provider()?;
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
let (buffer, cursor_buffer_position) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||
if self.edit_predictions_hidden_for_vim_mode || !self.should_show_edit_predictions() {
|
||||
return None;
|
||||
}
|
||||
|
||||
provider.cycle(buffer, cursor_buffer_position, direction, cx);
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn show_edit_prediction(
|
||||
&mut self,
|
||||
_: &ShowEditPrediction,
|
||||
@@ -7501,6 +7644,42 @@ impl Editor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn next_edit_prediction(
|
||||
&mut self,
|
||||
_: &NextEditPrediction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.has_active_edit_prediction() {
|
||||
self.cycle_edit_prediction(Direction::Next, window, cx);
|
||||
} else {
|
||||
let is_copilot_disabled = self
|
||||
.refresh_edit_prediction(false, true, window, cx)
|
||||
.is_none();
|
||||
if is_copilot_disabled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_edit_prediction(
|
||||
&mut self,
|
||||
_: &PreviousEditPrediction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.has_active_edit_prediction() {
|
||||
self.cycle_edit_prediction(Direction::Prev, window, cx);
|
||||
} else {
|
||||
let is_copilot_disabled = self
|
||||
.refresh_edit_prediction(false, true, window, cx)
|
||||
.is_none();
|
||||
if is_copilot_disabled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept_partial_edit_prediction(
|
||||
&mut self,
|
||||
granularity: EditPredictionGranularity,
|
||||
@@ -8545,7 +8724,7 @@ impl Editor {
|
||||
BreakpointEditAction::Toggle
|
||||
};
|
||||
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
position,
|
||||
breakpoint.as_ref().clone(),
|
||||
@@ -8737,7 +8916,7 @@ impl Editor {
|
||||
ClickEvent::Mouse(e) => e.down.button == MouseButton::Left,
|
||||
};
|
||||
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.toggle_code_actions(
|
||||
&ToggleCodeActions {
|
||||
deployed_from: Some(CodeActionSource::RunMenu(row)),
|
||||
@@ -11152,7 +11331,7 @@ impl Editor {
|
||||
}];
|
||||
|
||||
let focus_handle = bp_prompt.focus_handle(cx);
|
||||
window.focus(&focus_handle, cx);
|
||||
window.focus(&focus_handle);
|
||||
|
||||
let block_ids = self.insert_blocks(blocks, None, cx);
|
||||
bp_prompt.update(cx, |prompt, _| {
|
||||
@@ -15355,9 +15534,10 @@ impl Editor {
|
||||
I: IntoIterator<Item = P>,
|
||||
P: AsRef<[u8]>,
|
||||
{
|
||||
let case_sensitive = self
|
||||
.select_next_is_case_sensitive
|
||||
.unwrap_or_else(|| EditorSettings::get_global(cx).search.case_sensitive);
|
||||
let case_sensitive = self.select_next_is_case_sensitive.map_or_else(
|
||||
|| EditorSettings::get_global(cx).search.case_sensitive,
|
||||
|value| value,
|
||||
);
|
||||
|
||||
let mut builder = AhoCorasickBuilder::new();
|
||||
builder.ascii_case_insensitive(!case_sensitive);
|
||||
@@ -17979,7 +18159,7 @@ impl Editor {
|
||||
cx,
|
||||
);
|
||||
let rename_focus_handle = rename_editor.focus_handle(cx);
|
||||
window.focus(&rename_focus_handle, cx);
|
||||
window.focus(&rename_focus_handle);
|
||||
let block_id = this.insert_blocks(
|
||||
[BlockProperties {
|
||||
style: BlockStyle::Flex,
|
||||
@@ -18093,7 +18273,7 @@ impl Editor {
|
||||
) -> Option<RenameState> {
|
||||
let rename = self.pending_rename.take()?;
|
||||
if rename.editor.focus_handle(cx).is_focused(window) {
|
||||
window.focus(&self.focus_handle, cx);
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
self.remove_blocks(
|
||||
@@ -19914,8 +20094,10 @@ impl Editor {
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
println!("\n\nin toggle_diff_hunks_in_ranges");
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
let expand = !buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx);
|
||||
println!("expand={expand}\n\n");
|
||||
buffer.expand_or_collapse_diff_hunks(ranges, expand, cx);
|
||||
})
|
||||
}
|
||||
@@ -22663,7 +22845,7 @@ impl Editor {
|
||||
.take()
|
||||
.and_then(|descendant| descendant.upgrade())
|
||||
{
|
||||
window.focus(&descendant, cx);
|
||||
window.focus(&descendant);
|
||||
} else {
|
||||
if let Some(blame) = self.blame.as_ref() {
|
||||
blame.update(cx, GitBlame::focus)
|
||||
@@ -23294,256 +23476,76 @@ struct CompletionEdit {
|
||||
snippet: Option<Snippet>,
|
||||
}
|
||||
|
||||
fn comment_delimiter_for_newline(
|
||||
start_point: &Point,
|
||||
fn insert_extra_newline_brackets(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
) -> Option<Arc<str>> {
|
||||
let delimiters = language.line_comment_prefixes();
|
||||
let max_len_of_delimiter = delimiters.iter().map(|delimiter| delimiter.len()).max()?;
|
||||
let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
range: Range<MultiBufferOffset>,
|
||||
language: &language::LanguageScope,
|
||||
) -> bool {
|
||||
let leading_whitespace_len = buffer
|
||||
.reversed_chars_at(range.start)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let trailing_whitespace_len = buffer
|
||||
.chars_at(range.end)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
let comment_candidate = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_len_of_delimiter)
|
||||
.collect::<String>();
|
||||
let (delimiter, trimmed_len) = delimiters
|
||||
.iter()
|
||||
.filter_map(|delimiter| {
|
||||
let prefix = delimiter.trim_end();
|
||||
if comment_candidate.starts_with(prefix) {
|
||||
Some((delimiter, prefix.len()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.max_by_key(|(_, len)| *len)?;
|
||||
language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
|
||||
if let Some(BlockCommentConfig {
|
||||
start: block_start, ..
|
||||
}) = language.block_comment()
|
||||
{
|
||||
let block_start_trimmed = block_start.trim_end();
|
||||
if block_start_trimmed.starts_with(delimiter.trim_end()) {
|
||||
let line_content = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(num_of_whitespaces)
|
||||
.take(block_start_trimmed.len())
|
||||
.collect::<String>();
|
||||
|
||||
if line_content.starts_with(block_start_trimmed) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_is_placed_after_comment_marker =
|
||||
num_of_whitespaces + trimmed_len <= start_point.column as usize;
|
||||
if cursor_is_placed_after_comment_marker {
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer.contains_str_at(range.end, pair_end)
|
||||
&& buffer.contains_str_at(
|
||||
range.start.saturating_sub_usize(pair_start.len()),
|
||||
pair_start,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn documentation_delimiter_for_newline(
|
||||
start_point: &Point,
|
||||
fn insert_extra_newline_tree_sitter(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
newline_formatting: &mut NewlineFormatting,
|
||||
) -> Option<Arc<str>> {
|
||||
let BlockCommentConfig {
|
||||
start: start_tag,
|
||||
end: end_tag,
|
||||
prefix: delimiter,
|
||||
tab_size: len,
|
||||
} = language.documentation_comment()?;
|
||||
let is_within_block_comment = buffer
|
||||
.language_scope_at(*start_point)
|
||||
.is_some_and(|scope| scope.override_name() == Some("comment"));
|
||||
if !is_within_block_comment {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
|
||||
// It is safe to use a column from MultiBufferPoint in context of a single buffer ranges, because we're only ever looking at a single line at a time.
|
||||
let column = start_point.column;
|
||||
let cursor_is_after_start_tag = {
|
||||
let start_tag_len = start_tag.len();
|
||||
let start_tag_line = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(start_tag_len)
|
||||
.collect::<String>();
|
||||
if start_tag_line.starts_with(start_tag.as_ref()) {
|
||||
num_of_whitespaces + start_tag_len <= column as usize
|
||||
} else {
|
||||
false
|
||||
}
|
||||
range: Range<MultiBufferOffset>,
|
||||
) -> bool {
|
||||
let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() {
|
||||
[(buffer, range, _)] => (*buffer, range.clone()),
|
||||
_ => return false,
|
||||
};
|
||||
let pair = {
|
||||
let mut result: Option<BracketMatch<usize>> = None;
|
||||
|
||||
let cursor_is_after_delimiter = {
|
||||
let delimiter_trim = delimiter.trim_end();
|
||||
let delimiter_line = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(delimiter_trim.len())
|
||||
.collect::<String>();
|
||||
if delimiter_line.starts_with(delimiter_trim) {
|
||||
num_of_whitespaces + delimiter_trim.len() <= column as usize
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
for pair in buffer
|
||||
.all_bracket_ranges(range.start.0..range.end.0)
|
||||
.filter(move |pair| {
|
||||
pair.open_range.start <= range.start.0 && pair.close_range.end >= range.end.0
|
||||
})
|
||||
{
|
||||
let len = pair.close_range.end - pair.open_range.start;
|
||||
|
||||
let cursor_is_before_end_tag_if_exists = {
|
||||
let mut char_position = 0u32;
|
||||
let mut end_tag_offset = None;
|
||||
|
||||
'outer: for chunk in snapshot.text_for_range(range) {
|
||||
if let Some(byte_pos) = chunk.find(&**end_tag) {
|
||||
let chars_before_match = chunk[..byte_pos].chars().count() as u32;
|
||||
end_tag_offset = Some(char_position + chars_before_match);
|
||||
break 'outer;
|
||||
}
|
||||
char_position += chunk.chars().count() as u32;
|
||||
}
|
||||
|
||||
if let Some(end_tag_offset) = end_tag_offset {
|
||||
let cursor_is_before_end_tag = column <= end_tag_offset;
|
||||
if cursor_is_after_start_tag {
|
||||
if cursor_is_before_end_tag {
|
||||
newline_formatting.insert_extra_newline = true;
|
||||
}
|
||||
let cursor_is_at_start_of_end_tag = column == end_tag_offset;
|
||||
if cursor_is_at_start_of_end_tag {
|
||||
newline_formatting.indent_on_extra_newline.len = *len;
|
||||
if let Some(existing) = &result {
|
||||
let existing_len = existing.close_range.end - existing.open_range.start;
|
||||
if len > existing_len {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
cursor_is_before_end_tag
|
||||
} else {
|
||||
true
|
||||
|
||||
result = Some(pair);
|
||||
}
|
||||
|
||||
result
|
||||
};
|
||||
|
||||
if (cursor_is_after_start_tag || cursor_is_after_delimiter)
|
||||
&& cursor_is_before_end_tag_if_exists
|
||||
{
|
||||
if cursor_is_after_start_tag {
|
||||
newline_formatting.indent_on_newline.len = *len;
|
||||
}
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct NewlineFormatting {
|
||||
insert_extra_newline: bool,
|
||||
indent_on_newline: IndentSize,
|
||||
indent_on_extra_newline: IndentSize,
|
||||
}
|
||||
|
||||
impl NewlineFormatting {
|
||||
fn new(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
language: &LanguageScope,
|
||||
) -> Self {
|
||||
Self {
|
||||
insert_extra_newline: Self::insert_extra_newline_brackets(
|
||||
buffer,
|
||||
range.clone(),
|
||||
language,
|
||||
) || Self::insert_extra_newline_tree_sitter(buffer, range),
|
||||
indent_on_newline: IndentSize::spaces(0),
|
||||
indent_on_extra_newline: IndentSize::spaces(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_extra_newline_brackets(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
language: &language::LanguageScope,
|
||||
) -> bool {
|
||||
let leading_whitespace_len = buffer
|
||||
.reversed_chars_at(range.start)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let trailing_whitespace_len = buffer
|
||||
.chars_at(range.end)
|
||||
.take_while(|c| c.is_whitespace() && *c != '\n')
|
||||
.map(|c| c.len_utf8())
|
||||
.sum::<usize>();
|
||||
let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len;
|
||||
|
||||
language.brackets().any(|(pair, enabled)| {
|
||||
let pair_start = pair.start.trim_end();
|
||||
let pair_end = pair.end.trim_start();
|
||||
|
||||
enabled
|
||||
&& pair.newline
|
||||
&& buffer.contains_str_at(range.end, pair_end)
|
||||
&& buffer.contains_str_at(
|
||||
range.start.saturating_sub_usize(pair_start.len()),
|
||||
pair_start,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_extra_newline_tree_sitter(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
) -> bool {
|
||||
let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() {
|
||||
[(buffer, range, _)] => (*buffer, range.clone()),
|
||||
_ => return false,
|
||||
};
|
||||
let pair = {
|
||||
let mut result: Option<BracketMatch<usize>> = None;
|
||||
|
||||
for pair in buffer
|
||||
.all_bracket_ranges(range.start.0..range.end.0)
|
||||
.filter(move |pair| {
|
||||
pair.open_range.start <= range.start.0 && pair.close_range.end >= range.end.0
|
||||
})
|
||||
{
|
||||
let len = pair.close_range.end - pair.open_range.start;
|
||||
|
||||
if let Some(existing) = &result {
|
||||
let existing_len = existing.close_range.end - existing.open_range.start;
|
||||
if len > existing_len {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result = Some(pair);
|
||||
}
|
||||
|
||||
result
|
||||
};
|
||||
let Some(pair) = pair else {
|
||||
return false;
|
||||
};
|
||||
pair.newline_only
|
||||
&& buffer
|
||||
.chars_for_range(pair.open_range.end..range.start.0)
|
||||
.chain(buffer.chars_for_range(range.end.0..pair.close_range.start))
|
||||
.all(|c| c.is_whitespace() && c != '\n')
|
||||
}
|
||||
let Some(pair) = pair else {
|
||||
return false;
|
||||
};
|
||||
pair.newline_only
|
||||
&& buffer
|
||||
.chars_for_range(pair.open_range.end..range.start.0)
|
||||
.chain(buffer.chars_for_range(range.end.0..pair.close_range.start))
|
||||
.all(|c| c.is_whitespace() && c != '\n')
|
||||
}
|
||||
|
||||
fn update_uncommitted_diff_for_buffer(
|
||||
@@ -25909,7 +25911,7 @@ impl BreakpointPromptEditor {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.remove_blocks(self.block_ids.clone(), None, cx);
|
||||
window.focus(&editor.focus_handle, cx);
|
||||
window.focus(&editor.focus_handle);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -41,16 +41,14 @@ use multi_buffer::{
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::{
|
||||
FakeFs, Project,
|
||||
FakeFs,
|
||||
debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
|
||||
project_settings::LspSettings,
|
||||
trusted_worktrees::{PathTrust, TrustedWorktrees},
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use settings::{
|
||||
AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
|
||||
IndentGuideColoring, InlayHintSettingsContent, ProjectSettingsContent, SearchSettingsContent,
|
||||
SettingsStore,
|
||||
IndentGuideColoring, ProjectSettingsContent, SearchSettingsContent,
|
||||
};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
|
||||
use std::{
|
||||
@@ -69,6 +67,7 @@ use util::{
|
||||
use workspace::{
|
||||
CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
|
||||
OpenOptions, ViewId,
|
||||
invalid_item_view::InvalidItemView,
|
||||
item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
|
||||
register_project_item,
|
||||
};
|
||||
@@ -18200,7 +18199,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
editor_handle.update_in(cx, |editor, window, cx| {
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
|
||||
});
|
||||
@@ -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]:
|
||||
aˇ
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user