diff --git a/.github/actions/run_tests/action.yml b/.github/actions/run_tests/action.yml
index a071aba3a87dcf8e8f48f740115cfddf48b9f805..610c334a65c3a3817ab0ee2bb7356a923643092b 100644
--- a/.github/actions/run_tests/action.yml
+++ b/.github/actions/run_tests/action.yml
@@ -5,7 +5,7 @@ runs:
using: "composite"
steps:
- name: Install nextest
- uses: taiki-e/install-action@nextest
+ uses: taiki-e/install-action@921e2c9f7148d7ba14cd819f417db338f63e733c # nextest
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
diff --git a/.github/actions/run_tests_windows/action.yml b/.github/actions/run_tests_windows/action.yml
index 307b73f363b7d5fd7a3c9e5082c4f17d622ec165..3752cbb50d538459ea58d2219e591d1abbda6247 100644
--- a/.github/actions/run_tests_windows/action.yml
+++ b/.github/actions/run_tests_windows/action.yml
@@ -12,7 +12,7 @@ runs:
steps:
- name: Install test runner
working-directory: ${{ inputs.working-directory }}
- uses: taiki-e/install-action@nextest
+ uses: taiki-e/install-action@921e2c9f7148d7ba14cd819f417db338f63e733c # nextest
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
diff --git a/.github/workflows/add_commented_closed_issue_to_project.yml b/.github/workflows/add_commented_closed_issue_to_project.yml
index bd84eaa9446e57c5482ab818df3dbcfe587e040e..27315e7160200dc323899b58d5c307aae656d5c6 100644
--- a/.github/workflows/add_commented_closed_issue_to_project.yml
+++ b/.github/workflows/add_commented_closed_issue_to_project.yml
@@ -35,7 +35,7 @@ jobs:
- if: steps.is-post-close-comment.outputs.result == 'true'
id: get-app-token
- uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 # v2.1.4
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
diff --git a/.github/workflows/after_release.yml b/.github/workflows/after_release.yml
index 95229f9f46bbd34ffe02832114b2b39da1b7e090..ab2220764861b17317f1fa3971ecf2aa9b645c8d 100644
--- a/.github/workflows/after_release.yml
+++ b/.github/workflows/after_release.yml
@@ -27,7 +27,7 @@ jobs:
- name: after_release::rebuild_releases_page::refresh_cloud_releases
run: curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag=${{ github.event.release.tag_name || inputs.tag_name }}
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: after_release::rebuild_releases_page::redeploy_zed_dev
@@ -110,7 +110,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: release::create_sentry_release
diff --git a/.github/workflows/assign-reviewers.yml b/.github/workflows/assign-reviewers.yml
index 1a21879b639736232f965863a31b9a8d3a2c2b35..c16a363db18c9ac11f000ad65961a165db43c982 100644
--- a/.github/workflows/assign-reviewers.yml
+++ b/.github/workflows/assign-reviewers.yml
@@ -51,7 +51,7 @@ jobs:
steps:
- name: Generate app token
id: app-token
- uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ vars.COORDINATOR_APP_ID }}
private-key: ${{ secrets.COORDINATOR_APP_PRIVATE_KEY }}
@@ -60,7 +60,7 @@ jobs:
# SECURITY: checks out the coordinator repo at ref: main, NOT the PR branch.
# persist-credentials: false prevents the token from leaking into .git/config.
- name: Checkout coordinator repo
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
repository: zed-industries/codeowner-coordinator
ref: main
@@ -69,7 +69,7 @@ jobs:
persist-credentials: false
- name: Setup Python
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.11"
@@ -95,7 +95,7 @@ jobs:
- name: Upload output
if: always()
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: assign-reviewers-output
path: /tmp/assign-reviewers-output.txt
diff --git a/.github/workflows/autofix_pr.yml b/.github/workflows/autofix_pr.yml
index 1f9e6320700d14cab69662e317c30fa7206eb655..f055c078cf4f814e342697e311ad5660f68f4624 100644
--- a/.github/workflows/autofix_pr.yml
+++ b/.github/workflows/autofix_pr.yml
@@ -18,7 +18,7 @@ jobs:
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: autofix_pr::run_autofix::checkout_pr
@@ -31,7 +31,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -91,22 +91,22 @@ jobs:
if: needs.run_autofix.outputs.has_changes == 'true'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- - id: get-app-token
+ - id: generate-token
name: steps::authenticate_as_zippy
- uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- token: ${{ steps.get-app-token.outputs.token }}
+ token: ${{ steps.generate-token.outputs.token }}
- name: autofix_pr::commit_changes::checkout_pr
run: gh pr checkout "$PR_NUMBER"
env:
PR_NUMBER: ${{ inputs.pr_number }}
- GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
+ GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
- name: autofix_pr::download_patch_artifact
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
with:
@@ -122,7 +122,7 @@ jobs:
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 }}
+ GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
concurrency:
group: ${{ github.workflow }}-${{ inputs.pr_number }}
cancel-in-progress: true
diff --git a/.github/workflows/background_agent_mvp.yml b/.github/workflows/background_agent_mvp.yml
index 528600138243cb8aca2e0fe0645eda198fc4f2b2..2f048d572df6fb45368c6d7aece574e83c9e7949 100644
--- a/.github/workflows/background_agent_mvp.yml
+++ b/.github/workflows/background_agent_mvp.yml
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
fetch-depth: 0
@@ -50,7 +50,7 @@ jobs:
"${HOME}/.local/bin/droid" --version
- name: Setup Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
diff --git a/.github/workflows/bump_collab_staging.yml b/.github/workflows/bump_collab_staging.yml
index d400905b4da3304a8b916d3a38ae9d8a2855dbf5..4f9724439f37b276de625e5810c777c12f20e4b9 100644
--- a/.github/workflows/bump_collab_staging.yml
+++ b/.github/workflows/bump_collab_staging.yml
@@ -11,7 +11,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
fetch-depth: 0
diff --git a/.github/workflows/bump_patch_version.yml b/.github/workflows/bump_patch_version.yml
index 62540321ed755f2fd3879a7ddfc3a37237d8e7de..6b2fa66147b656efd9c8e28cd43cd2e010930dd1 100644
--- a/.github/workflows/bump_patch_version.yml
+++ b/.github/workflows/bump_patch_version.yml
@@ -13,18 +13,18 @@ jobs:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- - id: get-app-token
+ - id: generate-token
name: steps::authenticate_as_zippy
- uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
ref: ${{ inputs.branch }}
- token: ${{ steps.get-app-token.outputs.token }}
+ token: ${{ steps.generate-token.outputs.token }}
- name: bump_patch_version::run_bump_patch_version::bump_patch_version
run: |
channel="$(cat crates/zed/RELEASE_CHANNEL)"
@@ -51,7 +51,7 @@ jobs:
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 }}
+ GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
concurrency:
group: ${{ github.workflow }}-${{ inputs.branch }}
cancel-in-progress: true
diff --git a/.github/workflows/catch_blank_issues.yml b/.github/workflows/catch_blank_issues.yml
index c6f595ef2e0890ce107829f3e91490332567368a..dbceac5a196f2dc9c0963e491bd346dc8c0eff51 100644
--- a/.github/workflows/catch_blank_issues.yml
+++ b/.github/workflows/catch_blank_issues.yml
@@ -16,7 +16,7 @@ jobs:
timeout-minutes: 5
steps:
- id: get-app-token
- uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 # v2.1.4
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
diff --git a/.github/workflows/cherry_pick.yml b/.github/workflows/cherry_pick.yml
index ee0c1d35d0f9825d7c39b81fba0fe35901de2611..4a3bd0e643e027e7feaeac4760797e2a1fb16e11 100644
--- a/.github/workflows/cherry_pick.yml
+++ b/.github/workflows/cherry_pick.yml
@@ -26,12 +26,12 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- - id: get-app-token
+ - id: generate-token
name: steps::authenticate_as_zippy
- uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
@@ -43,7 +43,7 @@ jobs:
CHANNEL: ${{ inputs.channel }}
GIT_COMMITTER_NAME: Zed Zippy
GIT_COMMITTER_EMAIL: hi@zed.dev
- GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
+ GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
defaults:
run:
shell: bash -euxo pipefail {0}
diff --git a/.github/workflows/comment_on_potential_duplicate_issues.yml b/.github/workflows/comment_on_potential_duplicate_issues.yml
index de51cb1105c98901237ec88d47c34c69ea5c8080..0d7ce3aad3ce9deacfedfe1d237c41127a639da0 100644
--- a/.github/workflows/comment_on_potential_duplicate_issues.yml
+++ b/.github/workflows/comment_on_potential_duplicate_issues.yml
@@ -27,14 +27,14 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
sparse-checkout: script/github-check-new-issue-for-duplicates.py
sparse-checkout-cone-mode: false
- name: Get github app token
id: get-app-token
- uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 # v1.11.7
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
diff --git a/.github/workflows/community_champion_auto_labeler.yml b/.github/workflows/community_champion_auto_labeler.yml
index fa44afc16dcaee4c1e1176b9344aed476ac6d8e5..82a9e274d64725b0e55c6ced46ca64ac3890e35e 100644
--- a/.github/workflows/community_champion_auto_labeler.yml
+++ b/.github/workflows/community_champion_auto_labeler.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: Check if author is a community champion and apply label
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
env:
COMMUNITY_CHAMPIONS: |
0x2CA
diff --git a/.github/workflows/community_update_all_top_ranking_issues.yml b/.github/workflows/community_update_all_top_ranking_issues.yml
index ef3b4fc39ddb5f0db9b09c5e861547ae8cd7eb08..b8003a69b243c3cafbf40857c653fb03f515eeec 100644
--- a/.github/workflows/community_update_all_top_ranking_issues.yml
+++ b/.github/workflows/community_update_all_top_ranking_issues.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
if: github.repository == 'zed-industries/zed'
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Set up uv
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
with:
diff --git a/.github/workflows/community_update_weekly_top_ranking_issues.yml b/.github/workflows/community_update_weekly_top_ranking_issues.yml
index 53b548f2bb4286e5de86d3823e67d75c0413a1cb..90d1934ffcb6d5d711896d3902b70599e4b06872 100644
--- a/.github/workflows/community_update_weekly_top_ranking_issues.yml
+++ b/.github/workflows/community_update_weekly_top_ranking_issues.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
if: github.repository == 'zed-industries/zed'
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Set up uv
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
with:
diff --git a/.github/workflows/compare_perf.yml b/.github/workflows/compare_perf.yml
index 03113f2aa0be4dc794f8f5edec18df22fb0daa31..2b2154ce9bd14c85d0f0d10e95c4065a458006a1 100644
--- a/.github/workflows/compare_perf.yml
+++ b/.github/workflows/compare_perf.yml
@@ -21,7 +21,7 @@ jobs:
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -33,7 +33,7 @@ jobs:
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
- name: compare_perf::run_perf::install_hyperfine
- uses: taiki-e/install-action@hyperfine
+ uses: taiki-e/install-action@b4f2d5cb8597b15997c8ede873eb6185efc5f0ad
- name: steps::git_checkout
run: git fetch origin "$REF_NAME" && git checkout "$REF_NAME"
env:
diff --git a/.github/workflows/congrats.yml b/.github/workflows/congrats.yml
index 6a4111a1c5b5143ee9be067911207d5b4ca1448c..4866b3c33bc6bab9f9d20ac1701b7d6535b356ee 100644
--- a/.github/workflows/congrats.yml
+++ b/.github/workflows/congrats.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Get PR info and check if author is external
id: check
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
github-token: ${{ secrets.CONGRATSBOT_GITHUB_TOKEN }}
script: |
@@ -29,6 +29,13 @@ jobs:
}
const mergedPR = prs.find(pr => pr.merged_at !== null) || prs[0];
+
+ if (mergedPR.user.type === "Bot") {
+ // They are a good bot, but not good enough to be congratulated
+ core.setOutput('should_congratulate', 'false');
+ return;
+ }
+
const prAuthor = mergedPR.user.login;
try {
@@ -50,7 +57,7 @@ jobs:
congrats:
needs: check-author
if: needs.check-author.outputs.should_congratulate == 'true'
- uses: withastro/automation/.github/workflows/congratsbot.yml@main
+ uses: withastro/automation/.github/workflows/congratsbot.yml@a5bd0c5748c4d56e687cdd558064f9ee8adfb1f2 # main
with:
EMOJIS: 🎉,🎊,🧑🚀,🥳,🙌,🚀,🦀,🔥,🚢
secrets:
diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index 62f799baae1fb64a31807030c5700019a3d2c1b7..62739b21675fec2b4289b646fec794846c5fe783 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_pnpm
diff --git a/.github/workflows/deploy_cloudflare.yml b/.github/workflows/deploy_cloudflare.yml
index 37f23b20d2825e9f3d26c456903962a10c2d0081..4e029c63ccd8a022ac9d6107748f964585058735 100644
--- a/.github/workflows/deploy_cloudflare.yml
+++ b/.github/workflows/deploy_cloudflare.yml
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Checkout repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
clean: false
diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml
index 7fe06460f752599513c79b71bb01636d69d20e6c..5a3eff186814128ebb3973642040d9228f0e87fd 100644
--- a/.github/workflows/deploy_collab.yml
+++ b/.github/workflows/deploy_collab.yml
@@ -17,7 +17,7 @@ jobs:
CXX: clang++
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: 0
@@ -26,7 +26,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -48,7 +48,7 @@ jobs:
CXX: clang++
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: 0
@@ -57,7 +57,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -66,7 +66,7 @@ jobs:
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
- name: steps::cargo_install_nextest
- uses: taiki-e/install-action@nextest
+ uses: taiki-e/install-action@921e2c9f7148d7ba14cd819f417db338f63e733c
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
- name: deploy_collab::tests::run_collab_tests
@@ -93,7 +93,7 @@ jobs:
- name: deploy_collab::publish::sign_into_registry
run: doctl registry login
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: deploy_collab::publish::build_docker_image
@@ -113,7 +113,7 @@ jobs:
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: deploy_collab::deploy::install_doctl
diff --git a/.github/workflows/docs_suggestions.yml b/.github/workflows/docs_suggestions.yml
index c2dc8b4d5197bcbf38dbfb92dac8c23386726d53..c3d04d5780b290c81470dea16d11f473ee7361b1 100644
--- a/.github/workflows/docs_suggestions.yml
+++ b/.github/workflows/docs_suggestions.yml
@@ -64,7 +64,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
@@ -296,7 +296,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
fetch-depth: 0
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.ref || '' }}
diff --git a/.github/workflows/extension_auto_bump.yml b/.github/workflows/extension_auto_bump.yml
index 9388a0a442bf249505aaf51e9b6826d3bb228fb7..d4480194edbcacd24d0dff9bfd807abeb513d8ae 100644
--- a/.github/workflows/extension_auto_bump.yml
+++ b/.github/workflows/extension_auto_bump.yml
@@ -17,7 +17,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: 2
diff --git a/.github/workflows/extension_bump.yml b/.github/workflows/extension_bump.yml
index cbe38ee9e5b958eeee80eb5576c93896cc6763e1..b4cbac4ec8c0ab37ebad73eb96c2ee074ca969a6 100644
--- a/.github/workflows/extension_bump.yml
+++ b/.github/workflows/extension_bump.yml
@@ -5,7 +5,7 @@ env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: '1'
CARGO_INCREMENTAL: '0'
- ZED_EXTENSION_CLI_SHA: 03d8e9aee95ea6117d75a48bcac2e19241f6e667
+ ZED_EXTENSION_CLI_SHA: 1fa7f1a3ec28ea1eae6db2e937d7a538fb10c0c7
on:
workflow_call:
inputs:
@@ -34,7 +34,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: 0
@@ -74,17 +74,17 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: generate-token
- name: extension_bump::generate_token
- uses: actions/create-github-app-token@v2
+ name: steps::generate_token
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
with:
app-id: ${{ secrets.app-id }}
private-key: ${{ secrets.app-secret }}
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -138,7 +138,7 @@ jobs:
BUMP_TYPE: ${{ inputs.bump-type }}
WORKING_DIR: ${{ inputs.working-directory }}
- name: extension_bump::create_pull_request
- uses: peter-evans/create-pull-request@v7
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725
with:
title: ${{ steps.bump-version.outputs.title }}
body: ${{ steps.bump-version.outputs.body }}
@@ -162,13 +162,13 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: generate-token
- name: extension_bump::generate_token
- uses: actions/create-github-app-token@v2
+ name: steps::generate_token
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
with:
app-id: ${{ secrets.app-id }}
private-key: ${{ secrets.app-secret }}
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- id: determine-tag
@@ -187,7 +187,7 @@ jobs:
CURRENT_VERSION: ${{ needs.check_version_changed.outputs.current_version }}
WORKING_DIR: ${{ inputs.working-directory }}
- name: extension_bump::create_version_tag
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b
with:
script: |-
github.rest.git.createRef({
@@ -212,15 +212,15 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: generate-token
- name: extension_bump::generate_token
- uses: actions/create-github-app-token@v2
+ name: steps::generate_token
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
with:
app-id: ${{ secrets.app-id }}
private-key: ${{ secrets.app-secret }}
owner: zed-industries
repositories: extensions
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- id: get-extension-id
@@ -239,7 +239,7 @@ jobs:
env:
COMMITTER_TOKEN: ${{ steps.generate-token.outputs.token }}
- name: extension_bump::enable_automerge_if_staff
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
diff --git a/.github/workflows/extension_tests.yml b/.github/workflows/extension_tests.yml
index 89668c028a6d1fa4baddd417687226dd55a52426..622f4c8f1034b4ec0c7625a361ecdb6fb84d9429 100644
--- a/.github/workflows/extension_tests.yml
+++ b/.github/workflows/extension_tests.yml
@@ -5,7 +5,7 @@ env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: '1'
CARGO_INCREMENTAL: '0'
- ZED_EXTENSION_CLI_SHA: 03d8e9aee95ea6117d75a48bcac2e19241f6e667
+ ZED_EXTENSION_CLI_SHA: 1fa7f1a3ec28ea1eae6db2e937d7a538fb10c0c7
RUSTUP_TOOLCHAIN: stable
CARGO_BUILD_TARGET: wasm32-wasip2
on:
@@ -21,7 +21,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: ${{ github.ref == 'refs/heads/main' && 2 || 350 }}
@@ -73,11 +73,11 @@ jobs:
runs-on: namespace-profile-8x32-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -97,7 +97,7 @@ jobs:
env:
PACKAGE_NAME: ${{ steps.get-package-name.outputs.package_name }}
- name: steps::cargo_install_nextest
- uses: taiki-e/install-action@nextest
+ uses: taiki-e/install-action@921e2c9f7148d7ba14cd819f417db338f63e733c
- name: extension_tests::run_nextest
run: 'cargo nextest run -p "$PACKAGE_NAME" --no-fail-fast --no-tests=warn --target "$(rustc -vV | sed -n ''s|host: ||p'')"'
env:
@@ -115,7 +115,7 @@ jobs:
runs-on: namespace-profile-8x32-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: 0
@@ -131,7 +131,7 @@ jobs:
wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension" -O "$GITHUB_WORKSPACE/zed-extension"
chmod +x "$GITHUB_WORKSPACE/zed-extension"
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
diff --git a/.github/workflows/extension_workflow_rollout.yml b/.github/workflows/extension_workflow_rollout.yml
index f695b43ecac47a221bbc795d03e6ddd6259d7014..5bb315a730d8f25f6e1eccbbe5e1734e1cda6d99 100644
--- a/.github/workflows/extension_workflow_rollout.yml
+++ b/.github/workflows/extension_workflow_rollout.yml
@@ -20,7 +20,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: checkout_zed_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: 0
@@ -57,7 +57,7 @@ jobs:
PREV_COMMIT: ${{ steps.prev-tag.outputs.prev_commit }}
- id: list-repos
name: extension_workflow_rollout::fetch_extension_repos::get_repositories
- uses: actions/github-script@v7
+ uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b
with:
script: |
const repos = await github.paginate(github.rest.repos.listForOrg, {
@@ -81,7 +81,7 @@ jobs:
return filteredRepos;
result-encoding: json
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -114,8 +114,8 @@ jobs:
max-parallel: 10
steps:
- id: generate-token
- name: extension_bump::generate_token
- uses: actions/create-github-app-token@v2
+ name: steps::generate_token
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
@@ -125,7 +125,7 @@ jobs:
permission-contents: write
permission-workflows: write
- name: checkout_extension_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
path: extension
@@ -173,7 +173,7 @@ jobs:
echo "sha_short=$(echo "$GITHUB_SHA" | cut -c1-7)" >> "$GITHUB_OUTPUT"
- id: create-pr
name: extension_workflow_rollout::rollout_workflows_to_extension::create_pull_request
- uses: peter-evans/create-pull-request@v7
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725
with:
path: extension
title: Update CI workflows to `${{ steps.short-sha.outputs.sha_short }}`
@@ -207,14 +207,14 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: generate-token
- name: extension_bump::generate_token
- uses: actions/create-github-app-token@v2
+ name: steps::generate_token
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
permission-contents: write
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: 0
diff --git a/.github/workflows/good_first_issue_notifier.yml b/.github/workflows/good_first_issue_notifier.yml
index f366c671726348f605325576d65e13c6faa5616e..fc1b49424dce248d107d35cd6f228dd297478cad 100644
--- a/.github/workflows/good_first_issue_notifier.yml
+++ b/.github/workflows/good_first_issue_notifier.yml
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Prepare Discord message
id: prepare-message
diff --git a/.github/workflows/pr_labeler.yml b/.github/workflows/pr_labeler.yml
index 4a1f9c474c6d00bec137bbfb58ba78acb15440d1..2f09ad681698d008845565c989b26f51c489d500 100644
--- a/.github/workflows/pr_labeler.yml
+++ b/.github/workflows/pr_labeler.yml
@@ -17,7 +17,7 @@ jobs:
timeout-minutes: 5
steps:
- id: get-app-token
- uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 # v2.1.4
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
diff --git a/.github/workflows/publish_extension_cli.yml b/.github/workflows/publish_extension_cli.yml
index 75f1b16b007e33d0c4f346a33a1403648f1cd6c6..17248cea11307d4604b05d5160212a4f38e2874a 100644
--- a/.github/workflows/publish_extension_cli.yml
+++ b/.github/workflows/publish_extension_cli.yml
@@ -11,14 +11,14 @@ on:
jobs:
publish_job:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
- runs-on: namespace-profile-2x4-ubuntu-2404
+ runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -38,17 +38,17 @@ jobs:
runs-on: namespace-profile-8x16-ubuntu-2204
steps:
- id: generate-token
- name: extension_bump::generate_token
- uses: actions/create-github-app-token@v2
+ name: steps::generate_token
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -63,7 +63,7 @@ jobs:
- name: publish_extension_cli::update_sha_in_zed::regenerate_workflows
run: cargo xtask workflows
- name: publish_extension_cli::create_pull_request_zed
- uses: peter-evans/create-pull-request@v7
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725
with:
title: 'extension_ci: Bump extension CLI version to `${{ steps.short-sha.outputs.sha_short }}`'
body: |
@@ -87,8 +87,8 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: generate-token
- name: extension_bump::generate_token
- uses: actions/create-github-app-token@v2
+ name: steps::generate_token
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
@@ -108,7 +108,7 @@ jobs:
sed -i "s/ZED_EXTENSION_CLI_SHA: [a-f0-9]*/ZED_EXTENSION_CLI_SHA: $GITHUB_SHA/" \
.github/workflows/ci.yml
- name: publish_extension_cli::create_pull_request_extensions
- uses: peter-evans/create-pull-request@v7
+ uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725
with:
title: Bump extension CLI version to `${{ steps.short-sha.outputs.sha_short }}`
body: |
diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml
index de96c3df78bdb67edd584696f02316478e4446dd..9655a81235d79e1e24ae5185ebce8c8051437392 100644
--- a/.github/workflows/randomized_tests.yml
+++ b/.github/workflows/randomized_tests.yml
@@ -28,7 +28,7 @@ jobs:
node-version: "18"
- name: Checkout repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
clean: false
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 07a0a6d672a0a66c9c1609e82a22af9034dc936e..b651e7046bc7d603a7a829ce1b59fcf0468bdd3b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: namespace-profile-mac-large
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -22,7 +22,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -31,7 +31,7 @@ jobs:
with:
node-version: '20'
- name: steps::cargo_install_nextest
- uses: taiki-e/install-action@nextest
+ uses: taiki-e/install-action@921e2c9f7148d7ba14cd819f417db338f63e733c
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
- name: steps::setup_sccache
@@ -58,7 +58,7 @@ jobs:
CXX: clang++
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -66,7 +66,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -79,7 +79,7 @@ jobs:
with:
node-version: '20'
- name: steps::cargo_install_nextest
- uses: taiki-e/install-action@nextest
+ uses: taiki-e/install-action@921e2c9f7148d7ba14cd819f417db338f63e733c
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
- name: steps::setup_sccache
@@ -111,7 +111,7 @@ jobs:
runs-on: self-32vcpu-windows-2022
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -151,7 +151,7 @@ jobs:
runs-on: namespace-profile-mac-large
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -159,7 +159,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -183,7 +183,7 @@ jobs:
CXX: clang++
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -191,7 +191,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -216,7 +216,7 @@ jobs:
runs-on: self-32vcpu-windows-2022
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -244,7 +244,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: run_tests::check_scripts::run_shellcheck
@@ -257,7 +257,7 @@ jobs:
env:
ACTIONLINT_BIN: ${{ steps.get_actionlint.outputs.executable }}
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -275,7 +275,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: 25
@@ -305,7 +305,7 @@ jobs:
CXX: clang++-18
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_sentry
@@ -345,7 +345,7 @@ jobs:
CXX: clang++-18
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_sentry
@@ -388,7 +388,7 @@ jobs:
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_node
@@ -433,7 +433,7 @@ jobs:
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_node
@@ -482,7 +482,7 @@ jobs:
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_sentry
@@ -527,7 +527,7 @@ jobs:
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_sentry
@@ -617,16 +617,16 @@ 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
+ - id: generate-token
name: steps::authenticate_as_zippy
- uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859
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
env:
- GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
+ GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
push_release_update_notification:
needs:
- create_draft_release
diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml
index 093a17e8760e52fc4278d56dd6144b6a0432f3c5..30d0e1fbf9c7955d1216e2e3d7ac51a9a51f4416 100644
--- a/.github/workflows/release_nightly.yml
+++ b/.github/workflows/release_nightly.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: namespace-profile-mac-large
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: 0
@@ -30,7 +30,7 @@ jobs:
runs-on: self-32vcpu-windows-2022
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -70,7 +70,7 @@ jobs:
runs-on: self-32vcpu-windows-2022
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -107,7 +107,7 @@ jobs:
CXX: clang++-18
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
@@ -153,7 +153,7 @@ jobs:
CXX: clang++-18
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
@@ -202,7 +202,7 @@ jobs:
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
@@ -253,7 +253,7 @@ jobs:
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
@@ -308,7 +308,7 @@ jobs:
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
@@ -361,7 +361,7 @@ jobs:
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
@@ -406,11 +406,11 @@ jobs:
GIT_LFS_SKIP_SMUDGE: '1'
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_nix_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: nix
- name: nix_build::build_nix::install_nix
@@ -440,11 +440,11 @@ jobs:
GIT_LFS_SKIP_SMUDGE: '1'
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_nix_store_macos
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
path: ~/nix-cache
- name: nix_build::build_nix::install_nix
@@ -488,7 +488,7 @@ jobs:
runs-on: namespace-profile-4x8-ubuntu-2204
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: 0
diff --git a/.github/workflows/run_agent_evals.yml b/.github/workflows/run_agent_evals.yml
index 56cbd17a197200a6764ed1e28c87e90740cd7deb..83fd91b037fd982a25845b10aaff561b42af5fc5 100644
--- a/.github/workflows/run_agent_evals.yml
+++ b/.github/workflows/run_agent_evals.yml
@@ -24,11 +24,11 @@ jobs:
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
diff --git a/.github/workflows/run_bundling.yml b/.github/workflows/run_bundling.yml
index 5a93cf074e2a2d7f2f3cf8418ed508c5ad359d9e..71b2e4d5fa0b386334bb8acab8e732f1c7d0ad93 100644
--- a/.github/workflows/run_bundling.yml
+++ b/.github/workflows/run_bundling.yml
@@ -23,7 +23,7 @@ jobs:
CXX: clang++-18
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_sentry
@@ -62,7 +62,7 @@ jobs:
CXX: clang++-18
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_sentry
@@ -104,7 +104,7 @@ jobs:
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_node
@@ -148,7 +148,7 @@ jobs:
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_node
@@ -196,7 +196,7 @@ jobs:
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_sentry
@@ -240,7 +240,7 @@ jobs:
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_sentry
@@ -274,11 +274,11 @@ jobs:
GIT_LFS_SKIP_SMUDGE: '1'
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_nix_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: nix
- name: nix_build::build_nix::install_nix
@@ -306,11 +306,11 @@ jobs:
GIT_LFS_SKIP_SMUDGE: '1'
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_nix_store_macos
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
path: ~/nix-cache
- name: nix_build::build_nix::install_nix
diff --git a/.github/workflows/run_cron_unit_evals.yml b/.github/workflows/run_cron_unit_evals.yml
index 6af46e678d3d629cc2f7973b8b31ee99477dfefc..7bb7f79473eb4dae170eb18edd454b7ae35d13e8 100644
--- a/.github/workflows/run_cron_unit_evals.yml
+++ b/.github/workflows/run_cron_unit_evals.yml
@@ -21,7 +21,7 @@ jobs:
fail-fast: false
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -29,7 +29,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -38,7 +38,7 @@ jobs:
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
- name: steps::cargo_install_nextest
- uses: taiki-e/install-action@nextest
+ uses: taiki-e/install-action@921e2c9f7148d7ba14cd819f417db338f63e733c
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
- name: steps::setup_sccache
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
index 1906acf9fab7bbaab81b0549328c2e85d732756d..9f335a76beab036d97fe5555cd049ea46b4f87f0 100644
--- a/.github/workflows/run_tests.yml
+++ b/.github/workflows/run_tests.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: ${{ github.ref == 'refs/heads/main' && 2 || 350 }}
@@ -124,11 +124,11 @@ jobs:
runs-on: namespace-profile-4x8-ubuntu-2204
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -171,7 +171,7 @@ jobs:
runs-on: self-32vcpu-windows-2022
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -204,7 +204,7 @@ jobs:
CXX: clang++
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -212,7 +212,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -239,7 +239,7 @@ jobs:
runs-on: namespace-profile-mac-large
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -247,7 +247,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -270,7 +270,7 @@ jobs:
runs-on: namespace-profile-mac-large
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -278,7 +278,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -303,7 +303,7 @@ jobs:
runs-on: self-32vcpu-windows-2022
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -348,7 +348,7 @@ jobs:
CXX: clang++
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -356,7 +356,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -369,7 +369,7 @@ jobs:
with:
node-version: '20'
- name: steps::cargo_install_nextest
- uses: taiki-e/install-action@nextest
+ uses: taiki-e/install-action@921e2c9f7148d7ba14cd819f417db338f63e733c
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
- name: steps::setup_sccache
@@ -403,7 +403,7 @@ jobs:
runs-on: namespace-profile-mac-large
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -411,7 +411,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -420,7 +420,7 @@ jobs:
with:
node-version: '20'
- name: steps::cargo_install_nextest
- uses: taiki-e/install-action@nextest
+ uses: taiki-e/install-action@921e2c9f7148d7ba14cd819f417db338f63e733c
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
- name: steps::setup_sccache
@@ -449,11 +449,11 @@ jobs:
CXX: clang++
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -493,7 +493,7 @@ jobs:
CXX: clang++
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -501,7 +501,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -534,7 +534,7 @@ jobs:
runs-on: namespace-profile-8x16-ubuntu-2204
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -542,7 +542,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -576,11 +576,11 @@ jobs:
CXX: clang++
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -611,7 +611,7 @@ jobs:
CXX: clang++
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -619,7 +619,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -657,11 +657,11 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -676,7 +676,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: run_tests::check_scripts::run_shellcheck
@@ -689,7 +689,7 @@ jobs:
env:
ACTIONLINT_BIN: ${{ steps.get_actionlint.outputs.executable }}
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -714,7 +714,7 @@ jobs:
GIT_COMMITTER_EMAIL: ci@zed.dev
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
fetch-depth: 0
diff --git a/.github/workflows/run_unit_evals.yml b/.github/workflows/run_unit_evals.yml
index 44f12a1886bdac2fa1da8c870d223dd358285658..1bf75188832668f40a24c4d3452940bf05fcd3fd 100644
--- a/.github/workflows/run_unit_evals.yml
+++ b/.github/workflows/run_unit_evals.yml
@@ -24,7 +24,7 @@ jobs:
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
with:
clean: false
- name: steps::setup_cargo_config
@@ -32,7 +32,7 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: steps::cache_rust_dependencies_namespace
- uses: namespacelabs/nscloud-cache-action@v1
+ uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9
with:
cache: rust
path: ~/.rustup
@@ -41,7 +41,7 @@ jobs:
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
- name: steps::cargo_install_nextest
- uses: taiki-e/install-action@nextest
+ uses: taiki-e/install-action@921e2c9f7148d7ba14cd819f417db338f63e733c
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
- name: steps::setup_sccache
diff --git a/.github/workflows/track_duplicate_bot_effectiveness.yml b/.github/workflows/track_duplicate_bot_effectiveness.yml
index fa1c80616cb6133a7a4cad8841bbaad03115ff58..0d41a6070610ce9e9cc3faa06af78145bc9caec1 100644
--- a/.github/workflows/track_duplicate_bot_effectiveness.yml
+++ b/.github/workflows/track_duplicate_bot_effectiveness.yml
@@ -22,14 +22,14 @@ jobs:
timeout-minutes: 5
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
sparse-checkout: script/github-track-duplicate-bot-effectiveness.py
sparse-checkout-cone-mode: false
- name: Get github app token
id: get-app-token
- uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 # v1.11.7
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
@@ -61,14 +61,14 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout repository
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
sparse-checkout: script/github-track-duplicate-bot-effectiveness.py
sparse-checkout-cone-mode: false
- name: Get github app token
id: get-app-token
- uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1 # v1.11.7
+ uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ZED_COMMUNITY_BOT_APP_ID }}
private-key: ${{ secrets.ZED_COMMUNITY_BOT_PRIVATE_KEY }}
diff --git a/.github/workflows/update_duplicate_magnets.yml b/.github/workflows/update_duplicate_magnets.yml
index c3832b7bdbec13f74a8136cb1120a682f6e53920..d14f4aa92451aab9c36df49d3be128fd4797a4da 100644
--- a/.github/workflows/update_duplicate_magnets.yml
+++ b/.github/workflows/update_duplicate_magnets.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
if: github.repository == 'zed-industries/zed'
steps:
- - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
+ - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
diff --git a/Cargo.lock b/Cargo.lock
index 07047a62fff48a168c8702f47f4f5ab0ee52a143..e0ff23bdf3c056ca10b7a5d22793150655819f2c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -511,21 +511,6 @@ dependencies = [
"equator",
]
-[[package]]
-name = "alloc-no-stdlib"
-version = "2.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
-
-[[package]]
-name = "alloc-stdlib"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
-dependencies = [
- "alloc-no-stdlib",
-]
-
[[package]]
name = "allocator-api2"
version = "0.2.21"
@@ -2247,27 +2232,6 @@ dependencies = [
"workspace",
]
-[[package]]
-name = "brotli"
-version = "8.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
-dependencies = [
- "alloc-no-stdlib",
- "alloc-stdlib",
- "brotli-decompressor",
-]
-
-[[package]]
-name = "brotli-decompressor"
-version = "5.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
-dependencies = [
- "alloc-no-stdlib",
- "alloc-stdlib",
-]
-
[[package]]
name = "brush-parser"
version = "0.3.0"
@@ -5244,7 +5208,6 @@ version = "0.1.0"
dependencies = [
"ai_onboarding",
"anyhow",
- "brotli",
"buffer_diff",
"client",
"clock",
@@ -7921,6 +7884,35 @@ dependencies = [
"zed-scap",
]
+[[package]]
+name = "grammars"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "language_core",
+ "rust-embed",
+ "toml 0.8.23",
+ "tree-sitter",
+ "tree-sitter-bash",
+ "tree-sitter-c",
+ "tree-sitter-cpp",
+ "tree-sitter-css",
+ "tree-sitter-diff",
+ "tree-sitter-gitcommit",
+ "tree-sitter-go",
+ "tree-sitter-gomod",
+ "tree-sitter-gowork",
+ "tree-sitter-jsdoc",
+ "tree-sitter-json",
+ "tree-sitter-md",
+ "tree-sitter-python",
+ "tree-sitter-regex",
+ "tree-sitter-rust",
+ "tree-sitter-typescript",
+ "tree-sitter-yaml",
+ "util",
+]
+
[[package]]
name = "grid"
version = "0.18.0"
@@ -9368,6 +9360,7 @@ dependencies = [
"async-trait",
"clock",
"collections",
+ "criterion",
"ctor",
"diffy",
"ec4rs",
@@ -9381,6 +9374,7 @@ dependencies = [
"imara-diff",
"indoc",
"itertools 0.14.0",
+ "language_core",
"log",
"lsp",
"parking_lot",
@@ -9389,7 +9383,6 @@ dependencies = [
"rand 0.9.2",
"regex",
"rpc",
- "schemars",
"semver",
"serde",
"serde_json",
@@ -9424,6 +9417,25 @@ dependencies = [
"ztracing",
]
+[[package]]
+name = "language_core"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "gpui",
+ "log",
+ "lsp",
+ "parking_lot",
+ "regex",
+ "schemars",
+ "serde",
+ "serde_json",
+ "toml 0.8.23",
+ "tree-sitter",
+ "util",
+]
+
[[package]]
name = "language_extension"
version = "0.1.0"
@@ -9616,9 +9628,11 @@ dependencies = [
"async-trait",
"chrono",
"collections",
+ "fs",
"futures 0.3.31",
"globset",
"gpui",
+ "grammars",
"http_client",
"itertools 0.14.0",
"json_schema_store",
@@ -9638,7 +9652,6 @@ dependencies = [
"project",
"regex",
"rope",
- "rust-embed",
"semver",
"serde",
"serde_json",
@@ -9650,25 +9663,16 @@ dependencies = [
"task",
"terminal",
"theme",
- "toml 0.8.23",
"tree-sitter",
"tree-sitter-bash",
"tree-sitter-c",
"tree-sitter-cpp",
"tree-sitter-css",
- "tree-sitter-diff",
"tree-sitter-gitcommit",
"tree-sitter-go",
- "tree-sitter-gomod",
- "tree-sitter-gowork",
- "tree-sitter-jsdoc",
- "tree-sitter-json",
- "tree-sitter-md",
"tree-sitter-python",
- "tree-sitter-regex",
"tree-sitter-rust",
"tree-sitter-typescript",
- "tree-sitter-yaml",
"unindent",
"url",
"util",
@@ -10261,6 +10265,7 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
name = "markdown"
version = "0.1.0"
dependencies = [
+ "anyhow",
"assets",
"base64 0.22.1",
"collections",
@@ -10269,13 +10274,17 @@ dependencies = [
"futures 0.3.31",
"gpui",
"gpui_platform",
+ "html5ever 0.27.0",
"language",
"languages",
"linkify",
"log",
+ "markup5ever_rcdom",
+ "mermaid-rs-renderer",
"node_runtime",
"pulldown-cmark 0.13.0",
"settings",
+ "stacksafe",
"sum_tree",
"theme",
"ui",
@@ -10287,21 +10296,13 @@ name = "markdown_preview"
version = "0.1.0"
dependencies = [
"anyhow",
- "async-recursion",
- "collections",
"editor",
"gpui",
- "html5ever 0.27.0",
"language",
- "linkify",
"log",
"markdown",
- "markup5ever_rcdom",
- "mermaid-rs-renderer",
- "pretty_assertions",
- "pulldown-cmark 0.13.0",
"settings",
- "stacksafe",
+ "tempfile",
"theme",
"ui",
"urlencoding",
@@ -10788,6 +10789,7 @@ dependencies = [
"theme",
"tracing",
"tree-sitter",
+ "unicode-segmentation",
"util",
"zlog",
"ztracing",
@@ -15975,6 +15977,7 @@ dependencies = [
"action_log",
"agent",
"agent-client-protocol",
+ "agent_settings",
"agent_ui",
"anyhow",
"assistant_text_thread",
@@ -21513,6 +21516,7 @@ dependencies = [
name = "workspace"
version = "0.1.0"
dependencies = [
+ "agent_settings",
"any_vec",
"anyhow",
"async-recursion",
@@ -21971,7 +21975,7 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.230.0"
+version = "0.231.0"
dependencies = [
"acp_thread",
"acp_tools",
diff --git a/Cargo.toml b/Cargo.toml
index 17efe21800962cf5c7cd1b21b2e7c7a0c8df4c12..e9993d821888a2107427026f742aaca0cec220bb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -87,6 +87,7 @@ members = [
"crates/git_ui",
"crates/go_to_line",
"crates/google_ai",
+ "crates/grammars",
"crates/gpui",
"crates/gpui_linux",
"crates/gpui_macos",
@@ -108,6 +109,7 @@ members = [
"crates/json_schema_store",
"crates/keymap_editor",
"crates/language",
+ "crates/language_core",
"crates/language_extension",
"crates/language_model",
"crates/language_models",
@@ -330,6 +332,7 @@ git_hosting_providers = { path = "crates/git_hosting_providers" }
git_ui = { path = "crates/git_ui" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
+grammars = { path = "crates/grammars" }
gpui = { path = "crates/gpui", default-features = false }
gpui_linux = { path = "crates/gpui_linux", default-features = false }
gpui_macos = { path = "crates/gpui_macos", default-features = false }
@@ -354,6 +357,7 @@ journal = { path = "crates/journal" }
json_schema_store = { path = "crates/json_schema_store" }
keymap_editor = { path = "crates/keymap_editor" }
language = { path = "crates/language" }
+language_core = { path = "crates/language_core" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_models = { path = "crates/language_models" }
diff --git a/assets/icons/sweep_ai.svg b/assets/icons/sweep_ai.svg
deleted file mode 100644
index 9c63c810dd9e164c14c1ad1a1bca9c6ec68fc95e..0000000000000000000000000000000000000000
--- a/assets/icons/sweep_ai.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/assets/icons/sweep_ai_disabled.svg b/assets/icons/sweep_ai_disabled.svg
deleted file mode 100644
index b15a8d8526f36f312482effefd3d7538ce5f7a04..0000000000000000000000000000000000000000
--- a/assets/icons/sweep_ai_disabled.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/assets/icons/sweep_ai_down.svg b/assets/icons/sweep_ai_down.svg
deleted file mode 100644
index f08dcb171811c761cd13c4efd0ef0acdc78f9951..0000000000000000000000000000000000000000
--- a/assets/icons/sweep_ai_down.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/assets/icons/sweep_ai_error.svg b/assets/icons/sweep_ai_error.svg
deleted file mode 100644
index 95285a1273e72ec4f02cb23e3c2fb39460f42761..0000000000000000000000000000000000000000
--- a/assets/icons/sweep_ai_error.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/assets/icons/sweep_ai_up.svg b/assets/icons/sweep_ai_up.svg
deleted file mode 100644
index 7c28282a6a14c47561a50ab456c0bec2e05b07cc..0000000000000000000000000000000000000000
--- a/assets/icons/sweep_ai_up.svg
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/assets/icons/threads_sidebar_right_closed.svg b/assets/icons/threads_sidebar_right_closed.svg
new file mode 100644
index 0000000000000000000000000000000000000000..10fa4b792fd65b5875dcf2cadab1fc12a123ab47
--- /dev/null
+++ b/assets/icons/threads_sidebar_right_closed.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/threads_sidebar_right_open.svg b/assets/icons/threads_sidebar_right_open.svg
new file mode 100644
index 0000000000000000000000000000000000000000..23a01eb3f82a5866157220172c868ed9ded46033
--- /dev/null
+++ b/assets/icons/threads_sidebar_right_open.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 10c78ffa1660d81e86b7b2614770c5390bb819a6..617d7a6d0662264858ac3066d40481135dab9ae6 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -698,12 +698,18 @@
"left": "menu::SelectParent",
"right": "menu::SelectChild",
"enter": "menu::Confirm",
- "space": "menu::Confirm",
"ctrl-f": "agents_sidebar::FocusSidebarFilter",
"ctrl-g": "agents_sidebar::ToggleArchive",
"shift-backspace": "agent::RemoveSelectedThread",
},
},
+ {
+ "context": "ThreadsSidebar && not_searching",
+ "use_key_equivalents": true,
+ "bindings": {
+ "space": "menu::Confirm",
+ },
+ },
{
"context": "Workspace && debugger_running",
"bindings": {
@@ -1071,6 +1077,7 @@
"alt-up": "collab_panel::MoveChannelUp",
"alt-down": "collab_panel::MoveChannelDown",
"alt-enter": "collab_panel::OpenSelectedChannelNotes",
+ "shift-enter": "collab_panel::ToggleSelectedChannelFavorite",
},
},
{
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index 1095c4b82316bf8debac010a9954a962c495ee28..d3dda49c9a52a8c9b52dfddc04ae573f2fa4cf28 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -764,12 +764,18 @@
"left": "menu::SelectParent",
"right": "menu::SelectChild",
"enter": "menu::Confirm",
- "space": "menu::Confirm",
"cmd-f": "agents_sidebar::FocusSidebarFilter",
"cmd-g": "agents_sidebar::ToggleArchive",
"shift-backspace": "agent::RemoveSelectedThread",
},
},
+ {
+ "context": "ThreadsSidebar && not_searching",
+ "use_key_equivalents": true,
+ "bindings": {
+ "space": "menu::Confirm",
+ },
+ },
{
"context": "Workspace && debugger_running",
"use_key_equivalents": true,
@@ -1132,6 +1138,7 @@
"alt-up": "collab_panel::MoveChannelUp",
"alt-down": "collab_panel::MoveChannelDown",
"alt-enter": "collab_panel::OpenSelectedChannelNotes",
+ "shift-enter": "collab_panel::ToggleSelectedChannelFavorite",
},
},
{
diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json
index 02816c1adf48355b9ffada14608e248b29ab9270..e665d26aaf0c90d6c2fa4ee66284687c843fcd62 100644
--- a/assets/keymaps/default-windows.json
+++ b/assets/keymaps/default-windows.json
@@ -700,12 +700,18 @@
"left": "menu::SelectParent",
"right": "menu::SelectChild",
"enter": "menu::Confirm",
- "space": "menu::Confirm",
"ctrl-f": "agents_sidebar::FocusSidebarFilter",
"ctrl-g": "agents_sidebar::ToggleArchive",
"shift-backspace": "agent::RemoveSelectedThread",
},
},
+ {
+ "context": "ThreadsSidebar && not_searching",
+ "use_key_equivalents": true,
+ "bindings": {
+ "space": "menu::Confirm",
+ },
+ },
{
"context": "ApplicationMenu",
"use_key_equivalents": true,
@@ -1076,6 +1082,7 @@
"alt-up": "collab_panel::MoveChannelUp",
"alt-down": "collab_panel::MoveChannelDown",
"alt-enter": "collab_panel::OpenSelectedChannelNotes",
+ "shift-enter": "collab_panel::ToggleSelectedChannelFavorite",
},
},
{
diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json
index 5da15a1c1f304743c55e87ecc208fd6adbdc7cc2..ae0a0dd0f1ef3ba99814b39db6ec3932d0ef3730 100644
--- a/assets/keymaps/vim.json
+++ b/assets/keymaps/vim.json
@@ -337,6 +337,8 @@
"shift-j": "vim::JoinLines",
"i": "vim::InsertBefore",
"a": "vim::InsertAfter",
+ "o": "vim::InsertLineBelow",
+ "shift-o": "vim::InsertLineAbove",
"p": "vim::Paste",
"u": "vim::Undo",
"r": "vim::PushReplace",
diff --git a/assets/settings/default.json b/assets/settings/default.json
index d3fb32fd438eddd42ca5bf69815cf66d618ad570..7bfb1f2cdb68856d66073e8629d9921602d806d8 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -943,6 +943,8 @@
"button": true,
// Where to dock the agent panel. Can be 'left', 'right' or 'bottom'.
"dock": "right",
+ // Where to position the sidebar. Can be 'left', 'right', or 'follow_agent'.
+ "sidebar_side": "follow_agent",
// Default width when the agent panel is docked to the left or right.
"default_width": 640,
// Default height when the agent panel is docked to the bottom.
@@ -1585,13 +1587,6 @@
"model": "codestral-latest",
"max_tokens": 150,
},
- "sweep": {
- // When enabled, Sweep will not store edit prediction inputs or outputs.
- // When disabled, Sweep may collect data including buffer contents,
- // diagnostics, file paths, repository names, and generated predictions
- // to improve the service.
- "privacy_mode": false,
- },
"ollama": {
"api_url": "http://localhost:11434",
"model": "qwen2.5-coder:7b-base",
@@ -1622,6 +1617,8 @@
"status_bar": {
// Whether to show the status bar.
"experimental.show": true,
+ // Whether to show the name of the active file in the status bar.
+ "show_active_file": false,
// Whether to show the active language button in the status bar.
"active_language_button": true,
// Whether to show the cursor position button in the status bar.
diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs
index 3b143efbfdbd9c1ef636f76db795cd2f5b3b43e7..f33732f1e0f3623df5ce6833356f3547c5781adb 100644
--- a/crates/acp_thread/src/acp_thread.rs
+++ b/crates/acp_thread/src/acp_thread.rs
@@ -160,6 +160,7 @@ pub enum AgentThreadEntry {
UserMessage(UserMessage),
AssistantMessage(AssistantMessage),
ToolCall(ToolCall),
+ CompletedPlan(Vec),
}
impl AgentThreadEntry {
@@ -168,6 +169,7 @@ impl AgentThreadEntry {
Self::UserMessage(message) => message.indented,
Self::AssistantMessage(message) => message.indented,
Self::ToolCall(_) => false,
+ Self::CompletedPlan(_) => false,
}
}
@@ -176,6 +178,14 @@ impl AgentThreadEntry {
Self::UserMessage(message) => message.to_markdown(cx),
Self::AssistantMessage(message) => message.to_markdown(cx),
Self::ToolCall(tool_call) => tool_call.to_markdown(cx),
+ Self::CompletedPlan(entries) => {
+ let mut md = String::from("## Plan\n\n");
+ for entry in entries {
+ let source = entry.content.read(cx).source().to_string();
+ md.push_str(&format!("- [x] {}\n", source));
+ }
+ md
+ }
}
}
@@ -1298,7 +1308,9 @@ impl AcpThread {
status: ToolCallStatus::WaitingForConfirmation { .. },
..
}) => return true,
- AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
+ AgentThreadEntry::ToolCall(_)
+ | AgentThreadEntry::AssistantMessage(_)
+ | AgentThreadEntry::CompletedPlan(_) => {}
}
}
false
@@ -1320,7 +1332,9 @@ impl AcpThread {
) if call.diffs().next().is_some() => {
return true;
}
- AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
+ AgentThreadEntry::ToolCall(_)
+ | AgentThreadEntry::AssistantMessage(_)
+ | AgentThreadEntry::CompletedPlan(_) => {}
}
}
@@ -1337,7 +1351,9 @@ impl AcpThread {
}) => {
return true;
}
- AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
+ AgentThreadEntry::ToolCall(_)
+ | AgentThreadEntry::AssistantMessage(_)
+ | AgentThreadEntry::CompletedPlan(_) => {}
}
}
@@ -1348,7 +1364,9 @@ impl AcpThread {
for entry in self.entries.iter().rev() {
match entry {
AgentThreadEntry::UserMessage(..) => return false,
- AgentThreadEntry::AssistantMessage(..) => continue,
+ AgentThreadEntry::AssistantMessage(..) | AgentThreadEntry::CompletedPlan(..) => {
+ continue;
+ }
AgentThreadEntry::ToolCall(..) => return true,
}
}
@@ -2065,6 +2083,13 @@ impl AcpThread {
cx.notify();
}
+ pub fn snapshot_completed_plan(&mut self, cx: &mut Context) {
+ if !self.plan.is_empty() && self.plan.stats().pending == 0 {
+ let completed_entries = std::mem::take(&mut self.plan.entries);
+ self.push_entry(AgentThreadEntry::CompletedPlan(completed_entries), cx);
+ }
+ }
+
fn clear_completed_plan_entries(&mut self, cx: &mut Context) {
self.plan
.entries
@@ -2072,6 +2097,11 @@ impl AcpThread {
cx.notify();
}
+ pub fn clear_plan(&mut self, cx: &mut Context) {
+ self.plan.entries.clear();
+ cx.notify();
+ }
+
#[cfg(any(test, feature = "test-support"))]
pub fn send_raw(
&mut self,
@@ -2218,6 +2248,10 @@ impl AcpThread {
this.mark_pending_tools_as_canceled();
}
+ if !canceled {
+ this.snapshot_completed_plan(cx);
+ }
+
// Handle refusal - distinguish between user prompt and tool call refusals
if let acp::StopReason::Refusal = r.stop_reason {
this.had_error = true;
@@ -3177,9 +3211,27 @@ mod tests {
);
});
- // Wait for the printf command to execute and produce output
- // Use real time since parking is enabled
- cx.executor().timer(Duration::from_millis(500)).await;
+ // Poll until the printf command produces output, rather than using a
+ // fixed sleep which is flaky on loaded machines.
+ let deadline = std::time::Instant::now() + Duration::from_secs(10);
+ loop {
+ let has_output = thread.read_with(cx, |thread, cx| {
+ let term = thread
+ .terminals
+ .get(&terminal_id)
+ .expect("terminal not found");
+ let content = term.read(cx).inner().read(cx).get_content();
+ content.contains("output_before_kill")
+ });
+ if has_output {
+ break;
+ }
+ assert!(
+ std::time::Instant::now() < deadline,
+ "Timed out waiting for printf output to appear in terminal",
+ );
+ cx.executor().timer(Duration::from_millis(50)).await;
+ }
// Get the acp_thread Terminal and kill it
let wait_for_exit = thread.update(cx, |thread, cx| {
diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs
index 30d13effcb53395972879ef109a253be0c134ec1..78c873c3a1a12c1f24a2c64e96ce1d1801bc4eb9 100644
--- a/crates/acp_tools/src/acp_tools.rs
+++ b/crates/acp_tools/src/acp_tools.rs
@@ -291,7 +291,6 @@ impl AcpTools {
v_flex()
.id(index)
.group("message")
- .cursor_pointer()
.font_buffer(cx)
.w_full()
.py_3()
@@ -303,27 +302,29 @@ impl AcpTools {
.border_color(colors.border)
.border_b_1()
.hover(|this| this.bg(colors.element_background.opacity(0.5)))
- .on_click(cx.listener(move |this, _, _, cx| {
- if this.expanded.contains(&index) {
- this.expanded.remove(&index);
- } else {
- this.expanded.insert(index);
- let Some(connection) = &mut this.watched_connection else {
- return;
- };
- let Some(message) = connection.messages.get_mut(index) else {
- return;
- };
- message.expanded(this.project.read(cx).languages().clone(), cx);
- connection.list_state.scroll_to_reveal_item(index);
- }
- cx.notify()
- }))
.child(
h_flex()
+ .id(("acp-log-message-header", index))
.w_full()
.gap_2()
.flex_shrink_0()
+ .cursor_pointer()
+ .on_click(cx.listener(move |this, _, _, cx| {
+ if this.expanded.contains(&index) {
+ this.expanded.remove(&index);
+ } else {
+ this.expanded.insert(index);
+ let Some(connection) = &mut this.watched_connection else {
+ return;
+ };
+ let Some(message) = connection.messages.get_mut(index) else {
+ return;
+ };
+ message.expanded(this.project.read(cx).languages().clone(), cx);
+ connection.list_state.scroll_to_reveal_item(index);
+ }
+ cx.notify()
+ }))
.child(match message.direction {
acp::StreamMessageDirection::Incoming => Icon::new(IconName::ArrowDown)
.color(Color::Error)
diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs
index f36d8c0497430c27c7cafd99445c8baad18406f5..b7aa9d1e311016f572928993e049798c2b5e3bb2 100644
--- a/crates/agent/src/agent.rs
+++ b/crates/agent/src/agent.rs
@@ -942,6 +942,9 @@ impl NativeAgent {
NativeAgentConnection::handle_thread_events(events, acp_thread.downgrade(), cx)
})
.await?;
+ acp_thread.update(cx, |thread, cx| {
+ thread.snapshot_completed_plan(cx);
+ });
Ok(acp_thread)
})
}
diff --git a/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs
index 1ce2ca6f361a7e8186711d35d4dc640b8f13ce5a..e6a56099a293215050fa082a0432f216754473af 100644
--- a/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs
+++ b/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs
@@ -72,6 +72,18 @@ impl StreamingFuzzyMatcher {
pub fn finish(&mut self) -> Vec> {
// Process any remaining incomplete line
if !self.incomplete_line.is_empty() {
+ if self.matches.len() == 1 {
+ let range = &mut self.matches[0];
+ if range.end < self.snapshot.len()
+ && self
+ .snapshot
+ .contains_str_at(range.end + 1, &self.incomplete_line)
+ {
+ range.end += 1 + self.incomplete_line.len();
+ return self.matches.clone();
+ }
+ }
+
self.query_lines.push(self.incomplete_line.clone());
self.incomplete_line.clear();
self.matches = self.resolve_location_fuzzy();
@@ -722,6 +734,54 @@ mod tests {
);
}
+ #[gpui::test]
+ fn test_prefix_of_last_line_resolves_to_correct_range() {
+ let text = indoc! {r#"
+ fn on_query_change(&mut self, cx: &mut Context) {
+ self.filter(cx);
+ }
+
+
+
+ fn render_search(&self, cx: &mut Context) -> Div {
+ div()
+ }
+ "#};
+
+ let buffer = TextBuffer::new(
+ ReplicaId::LOCAL,
+ BufferId::new(1).unwrap(),
+ text.to_string(),
+ );
+ let snapshot = buffer.snapshot();
+
+ // Query with a partial last line.
+ let query = "}\n\n\n\nfn render_search";
+
+ let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
+ matcher.push(query, None);
+ let matches = matcher.finish();
+
+ // The match should include the line containing "fn render_search".
+ let matched_text = matches
+ .first()
+ .map(|range| snapshot.text_for_range(range.clone()).collect::());
+
+ assert!(
+ matches.len() == 1,
+ "Expected exactly one match, got {}: {:?}",
+ matches.len(),
+ matched_text,
+ );
+
+ let matched_text = matched_text.unwrap();
+ pretty_assertions::assert_eq!(
+ matched_text,
+ "}\n\n\n\nfn render_search",
+ "Match should include the render_search line",
+ );
+ }
+
#[track_caller]
fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) {
let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false);
diff --git a/crates/agent/src/tool_permissions.rs b/crates/agent/src/tool_permissions.rs
index 345511c5025b25601c630c572980d44a23f724e7..ec22fa11da00f6f41dfbdcee283ea983fbeac1af 100644
--- a/crates/agent/src/tool_permissions.rs
+++ b/crates/agent/src/tool_permissions.rs
@@ -596,6 +596,7 @@ mod tests {
tool_permissions,
show_turn_stats: false,
new_thread_location: Default::default(),
+ sidebar_side: Default::default(),
}
}
diff --git a/crates/agent/src/tools/evals/fixtures/extract_handle_command_output/possible-09.diff b/crates/agent/src/tools/evals/fixtures/extract_handle_command_output/possible-09.diff
new file mode 100644
index 0000000000000000000000000000000000000000..6bc45657b3d6bf23b4542deb4f6016472a0e89b9
--- /dev/null
+++ b/crates/agent/src/tools/evals/fixtures/extract_handle_command_output/possible-09.diff
@@ -0,0 +1,20 @@
+@@ -5,7 +5,7 @@
+ use futures::AsyncWriteExt;
+ use gpui::SharedString;
+ use serde::{Deserialize, Serialize};
+-use std::process::Stdio;
++use std::process::{Output, Stdio};
+ use std::{ops::Range, path::Path};
+ use text::Rope;
+ use time::OffsetDateTime;
+@@ -94,6 +94,10 @@
+
+ let output = child.output().await.context("reading git blame output")?;
+
++ handle_command_output(output)
++}
++
++fn handle_command_output(output: Output) -> Result {
+ if !output.status.success() {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let trimmed = stderr.trim();
diff --git a/crates/agent/src/tools/evals/streaming_edit_file.rs b/crates/agent/src/tools/evals/streaming_edit_file.rs
index 5ab931915e4789e2dd9f6fb7c1da19be6da59de2..6a55517037e54ae4166cd22427201d9325ef0f76 100644
--- a/crates/agent/src/tools/evals/streaming_edit_file.rs
+++ b/crates/agent/src/tools/evals/streaming_edit_file.rs
@@ -808,6 +808,8 @@ fn eval_extract_handle_command_output() {
include_str!("fixtures/extract_handle_command_output/possible-05.diff"),
include_str!("fixtures/extract_handle_command_output/possible-06.diff"),
include_str!("fixtures/extract_handle_command_output/possible-07.diff"),
+ include_str!("fixtures/extract_handle_command_output/possible-08.diff"),
+ include_str!("fixtures/extract_handle_command_output/possible-09.diff"),
];
eval_utils::eval(100, 0.95, eval_utils::NoProcessor, move || {
diff --git a/crates/agent/src/tools/streaming_edit_file_tool.rs b/crates/agent/src/tools/streaming_edit_file_tool.rs
index e62e47d404364f8aaddef3b4329cf93e1295370b..df99b4d65a62e3bb12239ef58d9ad49416554209 100644
--- a/crates/agent/src/tools/streaming_edit_file_tool.rs
+++ b/crates/agent/src/tools/streaming_edit_file_tool.rs
@@ -111,12 +111,13 @@ pub enum StreamingEditFileMode {
}
/// A single edit operation that replaces old text with new text
+/// Properly escape all text fields as valid JSON strings.
+/// Remember to escape special characters like newlines (`\n`) and quotes (`"`) in JSON strings.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct Edit {
/// The exact text to find in the file. This will be matched using fuzzy matching
/// to handle minor differences in whitespace or formatting.
///
- /// Always include complete lines. Do not start or end mid-line.
/// Be minimal with replacements:
/// - For unique lines, include only those lines
/// - For non-unique lines, include enough context to identify them
@@ -3916,6 +3917,58 @@ mod tests {
assert_eq!(new_text, "new_content");
}
+ #[gpui::test]
+ async fn test_streaming_edit_partial_last_line(cx: &mut TestAppContext) {
+ let file_content = indoc::indoc! {r#"
+ fn on_query_change(&mut self, cx: &mut Context) {
+ self.filter(cx);
+ }
+
+
+
+ fn render_search(&self, cx: &mut Context) -> Div {
+ div()
+ }
+ "#}
+ .to_string();
+
+ let (tool, _project, _action_log, _fs, _thread) =
+ setup_test(cx, json!({"file.rs": file_content})).await;
+
+ // The model sends old_text with a PARTIAL last line.
+ let old_text = "}\n\n\n\nfn render_search";
+ let new_text = "}\n\nfn render_search";
+
+ let (sender, input) = ToolInput::::test();
+ let (event_stream, _receiver) = ToolCallEventStream::test();
+ let task = cx.update(|cx| tool.clone().run(input, event_stream, cx));
+
+ sender.send_final(json!({
+ "display_description": "Remove extra blank lines",
+ "path": "root/file.rs",
+ "mode": "edit",
+ "edits": [{"old_text": old_text, "new_text": new_text}]
+ }));
+
+ let result = task.await;
+ let StreamingEditFileToolOutput::Success {
+ new_text: final_text,
+ ..
+ } = result.unwrap()
+ else {
+ panic!("expected success");
+ };
+
+ // The edit should reduce 3 blank lines to 1 blank line before
+ // fn render_search, without duplicating the function signature.
+ let expected = file_content.replace("}\n\n\n\nfn render_search", "}\n\nfn render_search");
+ pretty_assertions::assert_eq!(
+ final_text,
+ expected,
+ "Edit should only remove blank lines before render_search"
+ );
+ }
+
#[gpui::test]
async fn test_streaming_reject_created_file_deletes_it(cx: &mut TestAppContext) {
let (tool, _project, action_log, fs, _thread) = setup_test(cx, json!({"dir": {}})).await;
diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs
index d5d4f16eb742a92f6abf8081c43709f161ef4038..ec0a46af0636877210d26b2c45660baae648ffea 100644
--- a/crates/agent_settings/src/agent_settings.rs
+++ b/crates/agent_settings/src/agent_settings.rs
@@ -12,7 +12,8 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{
DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
- NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, ToolPermissionMode,
+ NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, SidebarDockPosition,
+ SidebarSide, ToolPermissionMode,
};
pub use crate::agent_profile::*;
@@ -26,6 +27,7 @@ pub struct AgentSettings {
pub enabled: bool,
pub button: bool,
pub dock: DockPosition,
+ pub sidebar_side: SidebarDockPosition,
pub default_width: Pixels,
pub default_height: Pixels,
pub default_model: Option,
@@ -77,6 +79,17 @@ impl AgentSettings {
return None;
}
+ pub fn sidebar_side(&self) -> SidebarSide {
+ match self.sidebar_side {
+ SidebarDockPosition::Left => SidebarSide::Left,
+ SidebarDockPosition::Right => SidebarSide::Right,
+ SidebarDockPosition::FollowAgent => match self.dock {
+ DockPosition::Right => SidebarSide::Right,
+ _ => SidebarSide::Left,
+ },
+ }
+ }
+
pub fn set_message_editor_max_lines(&self) -> usize {
self.message_editor_min_lines * 2
}
@@ -407,6 +420,7 @@ impl Settings for AgentSettings {
enabled: agent.enabled.unwrap(),
button: agent.button.unwrap(),
dock: agent.dock.unwrap(),
+ sidebar_side: agent.sidebar_side.unwrap(),
default_width: px(agent.default_width.unwrap()),
default_height: px(agent.default_height.unwrap()),
default_model: Some(agent.default_model.unwrap()),
diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs
index fc5a78dfc936617f3782eae154b6a13531e5c425..5f71df75d6287822c77eedfcb2f8fb96487b7950 100644
--- a/crates/agent_ui/src/agent_configuration.rs
+++ b/crates/agent_ui/src/agent_configuration.rs
@@ -4,7 +4,7 @@ mod configure_context_server_tools_modal;
mod manage_profiles_modal;
mod tool_picker;
-use std::{ops::Range, sync::Arc};
+use std::{ops::Range, rc::Rc, sync::Arc};
use agent::ContextServerRegistry;
use anyhow::Result;
@@ -33,9 +33,9 @@ use project::{
};
use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
- ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider,
- DividerColor, ElevationIndex, Indicator, LabelSize, PopoverMenu, Switch, Tooltip,
- WithScrollbar, prelude::*,
+ AiSettingItem, AiSettingItemSource, AiSettingItemStatus, ButtonStyle, Chip, ContextMenu,
+ ContextMenuEntry, Disclosure, Divider, DividerColor, ElevationIndex, LabelSize, PopoverMenu,
+ Switch, Tooltip, WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
@@ -45,29 +45,32 @@ pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
-use crate::agent_configuration::add_llm_provider_modal::{
- AddLlmProviderModal, LlmCompatibleProvider,
+use crate::{
+ Agent,
+ agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
+ agent_connection_store::{AgentConnectionStatus, AgentConnectionStore},
};
pub struct AgentConfiguration {
fs: Arc,
language_registry: Arc,
agent_server_store: Entity,
+ agent_connection_store: Entity,
workspace: WeakEntity,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap,
context_server_store: Entity,
expanded_provider_configurations: HashMap,
context_server_registry: Entity,
- _registry_subscription: Subscription,
+ _subscriptions: Vec,
scroll_handle: ScrollHandle,
- _check_for_gemini: Task<()>,
}
impl AgentConfiguration {
pub fn new(
fs: Arc,
agent_server_store: Entity,
+ agent_connection_store: Entity,
context_server_store: Entity,
context_server_registry: Entity,
language_registry: Arc,
@@ -77,25 +80,27 @@ impl AgentConfiguration {
) -> Self {
let focus_handle = cx.focus_handle();
- let registry_subscription = cx.subscribe_in(
- &LanguageModelRegistry::global(cx),
- window,
- |this, _, event: &language_model::Event, window, cx| match event {
- language_model::Event::AddedProvider(provider_id) => {
- let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
- if let Some(provider) = provider {
- this.add_provider_configuration_view(&provider, window, cx);
+ let subscriptions = vec![
+ cx.subscribe_in(
+ &LanguageModelRegistry::global(cx),
+ window,
+ |this, _, event: &language_model::Event, window, cx| match event {
+ language_model::Event::AddedProvider(provider_id) => {
+ let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
+ if let Some(provider) = provider {
+ this.add_provider_configuration_view(&provider, window, cx);
+ }
}
- }
- language_model::Event::RemovedProvider(provider_id) => {
- this.remove_provider_configuration_view(provider_id);
- }
- _ => {}
- },
- );
-
- cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
- .detach();
+ language_model::Event::RemovedProvider(provider_id) => {
+ this.remove_provider_configuration_view(provider_id);
+ }
+ _ => {}
+ },
+ ),
+ cx.subscribe(&agent_server_store, |_, _, _, cx| cx.notify()),
+ cx.observe(&agent_connection_store, |_, _, cx| cx.notify()),
+ cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()),
+ ];
let mut this = Self {
fs,
@@ -104,13 +109,14 @@ impl AgentConfiguration {
focus_handle,
configuration_views_by_provider: HashMap::default(),
agent_server_store,
+ agent_connection_store,
context_server_store,
expanded_provider_configurations: HashMap::default(),
context_server_registry,
- _registry_subscription: registry_subscription,
+ _subscriptions: subscriptions,
scroll_handle: ScrollHandle::new(),
- _check_for_gemini: Task::ready(()),
};
+
this.build_provider_configuration_views(window, cx);
this
}
@@ -636,6 +642,22 @@ impl AgentConfiguration {
)
});
+ let display_name = if provided_by_extension {
+ resolve_extension_for_context_server(&context_server_id, cx)
+ .map(|(_, manifest)| {
+ let name = manifest.name.as_str();
+ let stripped = name
+ .strip_suffix(" MCP Server")
+ .or_else(|| name.strip_suffix(" MCP"))
+ .or_else(|| name.strip_suffix(" Context Server"))
+ .unwrap_or(name);
+ SharedString::from(stripped.to_string())
+ })
+ .unwrap_or_else(|| item_id.clone())
+ } else {
+ item_id.clone()
+ };
+
let error = if let ContextServerStatus::Error(error) = server_status.clone() {
Some(error)
} else {
@@ -651,57 +673,19 @@ impl AgentConfiguration {
.tools_for_server(&context_server_id)
.count();
- let (source_icon, source_tooltip) = if provided_by_extension {
- (
- IconName::ZedSrcExtension,
- "This MCP server was installed from an extension.",
- )
+ let source = if provided_by_extension {
+ AiSettingItemSource::Extension
} else {
- (
- IconName::ZedSrcCustom,
- "This custom MCP server was installed directly.",
- )
+ AiSettingItemSource::Custom
};
- let (status_indicator, tooltip_text) = match server_status {
- ContextServerStatus::Starting => (
- Icon::new(IconName::LoadCircle)
- .size(IconSize::XSmall)
- .color(Color::Accent)
- .with_keyed_rotate_animation(
- SharedString::from(format!("{}-starting", context_server_id.0)),
- 3,
- )
- .into_any_element(),
- "Server is starting.",
- ),
- ContextServerStatus::Running => (
- Indicator::dot().color(Color::Success).into_any_element(),
- "Server is active.",
- ),
- ContextServerStatus::Error(_) => (
- Indicator::dot().color(Color::Error).into_any_element(),
- "Server has an error.",
- ),
- ContextServerStatus::Stopped => (
- Indicator::dot().color(Color::Muted).into_any_element(),
- "Server is stopped.",
- ),
- ContextServerStatus::AuthRequired => (
- Indicator::dot().color(Color::Warning).into_any_element(),
- "Authentication required.",
- ),
- ContextServerStatus::Authenticating => (
- Icon::new(IconName::LoadCircle)
- .size(IconSize::XSmall)
- .color(Color::Accent)
- .with_keyed_rotate_animation(
- SharedString::from(format!("{}-authenticating", context_server_id.0)),
- 3,
- )
- .into_any_element(),
- "Waiting for authorization...",
- ),
+ let status = match server_status {
+ ContextServerStatus::Starting => AiSettingItemStatus::Starting,
+ ContextServerStatus::Running => AiSettingItemStatus::Running,
+ ContextServerStatus::Error(_) => AiSettingItemStatus::Error,
+ ContextServerStatus::Stopped => AiSettingItemStatus::Stopped,
+ ContextServerStatus::AuthRequired => AiSettingItemStatus::AuthRequired,
+ ContextServerStatus::Authenticating => AiSettingItemStatus::Authenticating,
};
let is_remote = server_configuration
@@ -845,232 +829,165 @@ impl AgentConfiguration {
let feedback_base_container =
|| h_flex().py_1().min_w_0().w_full().gap_1().justify_between();
- v_flex()
- .min_w_0()
- .id(item_id.clone())
- .child(
- h_flex()
- .min_w_0()
- .w_full()
- .justify_between()
+ let details: Option = if let Some(error) = error {
+ Some(
+ feedback_base_container()
.child(
h_flex()
- .flex_1()
+ .pr_4()
.min_w_0()
+ .w_full()
+ .gap_2()
.child(
- h_flex()
- .id(format!("tooltip-{}", item_id))
- .h_full()
- .w_3()
- .mr_2()
- .justify_center()
- .tooltip(Tooltip::text(tooltip_text))
- .child(status_indicator),
- )
- .child(Label::new(item_id).flex_shrink_0().truncate())
- .child(
- div()
- .id("extension-source")
- .min_w_0()
- .mt_0p5()
- .mx_1()
- .tooltip(Tooltip::text(source_tooltip))
- .child(
- Icon::new(source_icon)
- .size(IconSize::Small)
- .color(Color::Muted),
- ),
+ Icon::new(IconName::XCircle)
+ .size(IconSize::XSmall)
+ .color(Color::Error),
)
- .when(is_running, |this| {
- this.child(
- Label::new(if tool_count == 1 {
- SharedString::from("1 tool")
- } else {
- SharedString::from(format!("{} tools", tool_count))
- })
- .color(Color::Muted)
- .size(LabelSize::Small),
- )
- }),
+ .child(div().min_w_0().flex_1().child(
+ Label::new(error).color(Color::Muted).size(LabelSize::Small),
+ )),
)
- .child(
- h_flex()
- .gap_0p5()
- .flex_none()
- .child(context_server_configuration_menu)
- .child(
- Switch::new("context-server-switch", is_running.into())
+ .when(should_show_logout_button, |this| {
+ this.child(
+ Button::new("error-logout-server", "Log Out")
+ .style(ButtonStyle::Outlined)
+ .label_size(LabelSize::Small)
.on_click({
- let context_server_manager = self.context_server_store.clone();
- let fs = self.fs.clone();
+ let context_server_store = context_server_store.clone();
let context_server_id = context_server_id.clone();
-
- move |state, _window, cx| {
- let is_enabled = match state {
- ToggleState::Unselected
- | ToggleState::Indeterminate => {
- context_server_manager.update(cx, |this, cx| {
- this.stop_server(&context_server_id, cx)
- .log_err();
- });
- false
- }
- ToggleState::Selected => {
- context_server_manager.update(cx, |this, cx| {
- if let Some(server) =
- this.get_server(&context_server_id)
- {
- this.start_server(server, cx);
- }
- });
- true
- }
- };
- update_settings_file(fs.clone(), cx, {
- let context_server_id = context_server_id.clone();
-
- move |settings, _| {
- settings
- .project
- .context_servers
- .entry(context_server_id.0)
- .or_insert_with(|| {
- settings::ContextServerSettingsContent::Extension {
- enabled: is_enabled,
- remote: false,
- settings: serde_json::json!({}),
- }
- })
- .set_enabled(is_enabled);
- }
+ move |_event, _window, cx| {
+ context_server_store.update(cx, |store, cx| {
+ store.logout_server(&context_server_id, cx).log_err();
});
}
}),
- ),
- ),
+ )
+ })
+ .into_any_element(),
)
- .map(|parent| {
- if let Some(error) = error {
- return parent
- .child(
- feedback_base_container()
- .child(
- h_flex()
- .pr_4()
- .min_w_0()
- .w_full()
- .gap_2()
- .child(
- Icon::new(IconName::XCircle)
- .size(IconSize::XSmall)
- .color(Color::Error),
- )
- .child(
- div().min_w_0().flex_1().child(
- Label::new(error)
- .color(Color::Muted)
- .size(LabelSize::Small),
- ),
- ),
- )
- .when(should_show_logout_button, |this| {
- this.child(
- Button::new("error-logout-server", "Log Out")
- .style(ButtonStyle::Outlined)
- .label_size(LabelSize::Small)
- .on_click({
- let context_server_store =
- context_server_store.clone();
- let context_server_id =
- context_server_id.clone();
- move |_event, _window, cx| {
- context_server_store.update(
- cx,
- |store, cx| {
- store
- .logout_server(
- &context_server_id,
- cx,
- )
- .log_err();
- },
- );
- }
- }),
- )
- }),
- );
- }
- if auth_required {
- return parent.child(
- feedback_base_container()
- .child(
- h_flex()
- .pr_4()
- .min_w_0()
- .w_full()
- .gap_2()
- .child(
- Icon::new(IconName::Info)
- .size(IconSize::XSmall)
- .color(Color::Muted),
- )
- .child(
- Label::new("Authenticate to connect this server")
- .color(Color::Muted)
- .size(LabelSize::Small),
- ),
- )
- .child(
- Button::new("error-logout-server", "Authenticate")
- .style(ButtonStyle::Outlined)
- .label_size(LabelSize::Small)
- .on_click({
- let context_server_store = context_server_store.clone();
- let context_server_id = context_server_id.clone();
- move |_event, _window, cx| {
- context_server_store.update(cx, |store, cx| {
- store
- .authenticate_server(&context_server_id, cx)
- .log_err();
- });
- }
- }),
- ),
- );
- }
- if authenticating {
- return parent.child(
+ } else if auth_required {
+ Some(
+ feedback_base_container()
+ .child(
h_flex()
- .mt_1()
.pr_4()
.min_w_0()
.w_full()
.gap_2()
.child(
- div().size_3().flex_shrink_0(), // Alignment Div
+ Icon::new(IconName::Info)
+ .size(IconSize::XSmall)
+ .color(Color::Muted),
)
.child(
- Label::new("Authenticating…")
+ Label::new("Authenticate to connect this server")
.color(Color::Muted)
.size(LabelSize::Small),
),
+ )
+ .child(
+ Button::new("error-logout-server", "Authenticate")
+ .style(ButtonStyle::Outlined)
+ .label_size(LabelSize::Small)
+ .on_click({
+ let context_server_id = context_server_id.clone();
+ move |_event, _window, cx| {
+ context_server_store.update(cx, |store, cx| {
+ store.authenticate_server(&context_server_id, cx).log_err();
+ });
+ }
+ }),
+ )
+ .into_any_element(),
+ )
+ } else if authenticating {
+ Some(
+ h_flex()
+ .mt_1()
+ .pr_4()
+ .min_w_0()
+ .w_full()
+ .gap_2()
+ .child(div().size_3().flex_shrink_0())
+ .child(
+ Label::new("Authenticating…")
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ )
+ .into_any_element(),
+ )
+ } else {
+ None
+ };
- );
- }
- parent
+ let tool_label = if is_running {
+ Some(if tool_count == 1 {
+ SharedString::from("1 tool")
+ } else {
+ SharedString::from(format!("{} tools", tool_count))
})
+ } else {
+ None
+ };
+
+ AiSettingItem::new(item_id, display_name, status, source)
+ .action(context_server_configuration_menu)
+ .action(
+ Switch::new("context-server-switch", is_running.into()).on_click({
+ let context_server_manager = self.context_server_store.clone();
+ let fs = self.fs.clone();
+
+ move |state, _window, cx| {
+ let is_enabled = match state {
+ ToggleState::Unselected | ToggleState::Indeterminate => {
+ context_server_manager.update(cx, |this, cx| {
+ this.stop_server(&context_server_id, cx).log_err();
+ });
+ false
+ }
+ ToggleState::Selected => {
+ context_server_manager.update(cx, |this, cx| {
+ if let Some(server) = this.get_server(&context_server_id) {
+ this.start_server(server, cx);
+ }
+ });
+ true
+ }
+ };
+ update_settings_file(fs.clone(), cx, {
+ let context_server_id = context_server_id.clone();
+
+ move |settings, _| {
+ settings
+ .project
+ .context_servers
+ .entry(context_server_id.0)
+ .or_insert_with(|| {
+ settings::ContextServerSettingsContent::Extension {
+ enabled: is_enabled,
+ remote: false,
+ settings: serde_json::json!({}),
+ }
+ })
+ .set_enabled(is_enabled);
+ }
+ });
+ }
+ }),
+ )
+ .when_some(tool_label, |this, label| this.detail_label(label))
+ .when_some(details, |this, details| this.details(details))
}
fn render_agent_servers_section(&mut self, cx: &mut Context) -> impl IntoElement {
let agent_server_store = self.agent_server_store.read(cx);
- let user_defined_agents = agent_server_store
+ let agents = agent_server_store
.external_agents()
.cloned()
.collect::>();
- let user_defined_agents: Vec<_> = user_defined_agents
+ let agents: Vec<_> = agents
.into_iter()
.map(|name| {
let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
@@ -1159,24 +1076,31 @@ impl AgentConfiguration {
"All agents connected through the Agent Client Protocol.",
add_agent_popover.into_any_element(),
))
- .child(v_flex().p_4().pt_0().gap_2().map(|mut parent| {
- let mut first = true;
- for (name, icon, display_name, source) in user_defined_agents {
- if !first {
- parent = parent
- .child(Divider::horizontal().color(DividerColor::BorderFaded));
- }
- first = false;
- parent = parent.child(self.render_agent_server(
- icon,
- name,
- display_name,
- source,
- cx,
- ));
- }
- parent
- })),
+ .child(
+ v_flex()
+ .p_4()
+ .pt_0()
+ .gap_2()
+ .children(Itertools::intersperse_with(
+ agents
+ .into_iter()
+ .map(|(name, icon, display_name, source)| {
+ self.render_agent_server(
+ icon,
+ name,
+ display_name,
+ source,
+ cx,
+ )
+ .into_any_element()
+ }),
+ || {
+ Divider::horizontal()
+ .color(DividerColor::BorderFaded)
+ .into_any_element()
+ },
+ )),
+ ),
)
}
@@ -1200,27 +1124,46 @@ impl AgentConfiguration {
.color(Color::Muted),
};
- let source_badge = match source {
- ExternalAgentSource::Extension => Some((
- SharedString::new(format!("agent-source-{}", id)),
- SharedString::from(format!(
- "The {} agent was installed from an extension.",
- display_name
- )),
- IconName::ZedSrcExtension,
- )),
- ExternalAgentSource::Registry => Some((
- SharedString::new(format!("agent-source-{}", id)),
- SharedString::from(format!(
- "The {} agent was installed from the ACP registry.",
- display_name
- )),
- IconName::AcpRegistry,
- )),
- ExternalAgentSource::Custom => None,
+ let source_kind = match source {
+ ExternalAgentSource::Extension => AiSettingItemSource::Extension,
+ ExternalAgentSource::Registry => AiSettingItemSource::Registry,
+ ExternalAgentSource::Custom => AiSettingItemSource::Custom,
};
let agent_server_name = AgentId(id.clone());
+ let agent = Agent::Custom {
+ id: agent_server_name.clone(),
+ };
+
+ let connection_status = self
+ .agent_connection_store
+ .read(cx)
+ .connection_status(&agent, cx);
+
+ let restart_button = matches!(
+ connection_status,
+ AgentConnectionStatus::Connected | AgentConnectionStatus::Connecting
+ )
+ .then(|| {
+ IconButton::new(
+ SharedString::from(format!("restart-{}", id)),
+ IconName::RotateCw,
+ )
+ .disabled(connection_status == AgentConnectionStatus::Connecting)
+ .icon_color(Color::Muted)
+ .icon_size(IconSize::Small)
+ .tooltip(Tooltip::text("Restart Agent Connection"))
+ .on_click(cx.listener({
+ let agent = agent.clone();
+ move |this, _, _window, cx| {
+ let server: Rc =
+ Rc::new(agent_servers::CustomAgentServer::new(agent.id()));
+ this.agent_connection_store.update(cx, |store, cx| {
+ store.restart_connection(agent.clone(), server, cx);
+ });
+ }
+ }))
+ });
let uninstall_button = match source {
ExternalAgentSource::Extension => Some(
@@ -1301,32 +1244,16 @@ impl AgentConfiguration {
}
};
- h_flex()
- .gap_1()
- .justify_between()
- .child(
- h_flex()
- .gap_1p5()
- .child(icon)
- .child(Label::new(display_name))
- .when_some(source_badge, |this, (tooltip_id, tooltip_message, icon)| {
- this.child(
- div()
- .id(tooltip_id)
- .flex_none()
- .tooltip(Tooltip::text(tooltip_message))
- .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)),
- )
- })
- .child(
- Icon::new(IconName::Check)
- .color(Color::Success)
- .size(IconSize::Small),
- ),
- )
- .when_some(uninstall_button, |this, uninstall_button| {
- this.child(uninstall_button)
- })
+ let status = match connection_status {
+ AgentConnectionStatus::Disconnected => AiSettingItemStatus::Stopped,
+ AgentConnectionStatus::Connecting => AiSettingItemStatus::Starting,
+ AgentConnectionStatus::Connected => AiSettingItemStatus::Running,
+ };
+
+ AiSettingItem::new(id, display_name, status, source_kind)
+ .icon(icon)
+ .when_some(restart_button, |this, button| this.action(button))
+ .when_some(uninstall_button, |this, button| this.action(button))
}
}
diff --git a/crates/agent_ui/src/agent_connection_store.rs b/crates/agent_ui/src/agent_connection_store.rs
index 89b3b0ef16f46753a747b1e06a9b9e4a76e839e8..55b8c1493cc990310dc13253ce33cbc4b71a748f 100644
--- a/crates/agent_ui/src/agent_connection_store.rs
+++ b/crates/agent_ui/src/agent_connection_store.rs
@@ -5,7 +5,8 @@ use agent_servers::{AgentServer, AgentServerDelegate};
use anyhow::Result;
use collections::HashMap;
use futures::{FutureExt, future::Shared};
-use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Subscription, Task};
+use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Subscription, Task};
+
use project::{AgentServerStore, AgentServersUpdated, Project};
use watch::Receiver;
@@ -27,6 +28,13 @@ pub struct AgentConnectedState {
pub history: Option>,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum AgentConnectionStatus {
+ Disconnected,
+ Connecting,
+ Connected,
+}
+
impl AgentConnectionEntry {
pub fn wait_for_connection(&self) -> Shared>> {
match self {
@@ -42,6 +50,14 @@ impl AgentConnectionEntry {
_ => None,
}
}
+
+ pub fn status(&self) -> AgentConnectionStatus {
+ match self {
+ AgentConnectionEntry::Connecting { .. } => AgentConnectionStatus::Connecting,
+ AgentConnectionEntry::Connected(_) => AgentConnectionStatus::Connected,
+ AgentConnectionEntry::Error { .. } => AgentConnectionStatus::Disconnected,
+ }
+ }
}
pub enum AgentConnectionEntryEvent {
@@ -71,66 +87,124 @@ impl AgentConnectionStore {
self.entries.get(key)
}
+ pub fn connection_status(&self, key: &Agent, cx: &App) -> AgentConnectionStatus {
+ self.entries
+ .get(key)
+ .map(|entry| entry.read(cx).status())
+ .unwrap_or(AgentConnectionStatus::Disconnected)
+ }
+
+ pub fn restart_connection(
+ &mut self,
+ key: Agent,
+ server: Rc,
+ cx: &mut Context,
+ ) -> Entity {
+ if let Some(entry) = self.entries.get(&key) {
+ if matches!(entry.read(cx), AgentConnectionEntry::Connecting { .. }) {
+ return entry.clone();
+ }
+ }
+
+ self.entries.remove(&key);
+ self.request_connection(key, server, cx)
+ }
+
pub fn request_connection(
&mut self,
key: Agent,
server: Rc,
cx: &mut Context,
) -> Entity {
- self.entries.get(&key).cloned().unwrap_or_else(|| {
- let (mut new_version_rx, connect_task) = self.start_connection(server.clone(), cx);
- let connect_task = connect_task.shared();
-
- let entry = cx.new(|_cx| AgentConnectionEntry::Connecting {
- connect_task: connect_task.clone(),
- });
-
- self.entries.insert(key.clone(), entry.clone());
-
- cx.spawn({
- let key = key.clone();
- let entry = entry.clone();
- async move |this, cx| match connect_task.await {
- Ok(connected_state) => {
- entry.update(cx, |entry, cx| {
- if let AgentConnectionEntry::Connecting { .. } = entry {
- *entry = AgentConnectionEntry::Connected(connected_state);
- cx.notify();
- }
- });
- }
- Err(error) => {
- entry.update(cx, |entry, cx| {
- if let AgentConnectionEntry::Connecting { .. } = entry {
- *entry = AgentConnectionEntry::Error { error };
- cx.notify();
- }
- });
- this.update(cx, |this, _cx| this.entries.remove(&key)).ok();
- }
+ if let Some(entry) = self.entries.get(&key) {
+ return entry.clone();
+ }
+
+ let (mut new_version_rx, connect_task) = self.start_connection(server, cx);
+ let connect_task = connect_task.shared();
+
+ let entry = cx.new(|_cx| AgentConnectionEntry::Connecting {
+ connect_task: connect_task.clone(),
+ });
+
+ self.entries.insert(key.clone(), entry.clone());
+ cx.notify();
+
+ cx.spawn({
+ let key = key.clone();
+ let entry = entry.downgrade();
+ async move |this, cx| match connect_task.await {
+ Ok(connected_state) => {
+ this.update(cx, move |this, cx| {
+ if this.entries.get(&key) != entry.upgrade().as_ref() {
+ return;
+ }
+
+ entry
+ .update(cx, move |entry, cx| {
+ if let AgentConnectionEntry::Connecting { .. } = entry {
+ *entry = AgentConnectionEntry::Connected(connected_state);
+ cx.notify();
+ }
+ })
+ .ok();
+ })
+ .ok();
}
- })
- .detach();
-
- cx.spawn({
- let entry = entry.clone();
- async move |this, cx| {
- while let Ok(version) = new_version_rx.recv().await {
- if let Some(version) = version {
- entry.update(cx, |_entry, cx| {
- cx.emit(AgentConnectionEntryEvent::NewVersionAvailable(
- version.clone().into(),
- ));
- });
- this.update(cx, |this, _cx| this.entries.remove(&key)).ok();
+ Err(error) => {
+ this.update(cx, move |this, cx| {
+ if this.entries.get(&key) != entry.upgrade().as_ref() {
+ return;
}
- }
+
+ entry
+ .update(cx, move |entry, cx| {
+ if let AgentConnectionEntry::Connecting { .. } = entry {
+ *entry = AgentConnectionEntry::Error { error };
+ cx.notify();
+ }
+ })
+ .ok();
+ this.entries.remove(&key);
+ cx.notify();
+ })
+ .ok();
}
- })
- .detach();
+ }
+ })
+ .detach();
+
+ cx.spawn({
+ let entry = entry.downgrade();
+ async move |this, cx| {
+ while let Ok(version) = new_version_rx.recv().await {
+ let Some(version) = version else {
+ continue;
+ };
+
+ this.update(cx, move |this, cx| {
+ if this.entries.get(&key) != entry.upgrade().as_ref() {
+ return;
+ }
- entry
+ entry
+ .update(cx, move |_entry, cx| {
+ cx.emit(AgentConnectionEntryEvent::NewVersionAvailable(
+ version.into(),
+ ));
+ })
+ .ok();
+ this.entries.remove(&key);
+ cx.notify();
+ })
+ .ok();
+ break;
+ }
+ }
})
+ .detach();
+
+ entry
}
fn handle_agent_servers_updated(
diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs
index aa88773680faee1dd7b8ceb0d60f93ecc13016c7..ff819b3730bdaf8dc89d5c40e5fdad04b3342496 100644
--- a/crates/agent_ui/src/agent_panel.rs
+++ b/crates/agent_ui/src/agent_panel.rs
@@ -1179,7 +1179,8 @@ impl AgentPanel {
}
pub fn new_thread(&mut self, _action: &NewThread, window: &mut Window, cx: &mut Context) {
- self.new_agent_thread(AgentType::NativeAgent, window, cx);
+ self.reset_start_thread_in_to_default(cx);
+ self.external_thread(None, None, None, None, None, true, window, cx);
}
fn new_native_agent_thread_from_summary(
@@ -1688,6 +1689,7 @@ impl AgentPanel {
AgentConfiguration::new(
fs,
agent_server_store,
+ self.connection_store.clone(),
context_server_store,
self.context_server_registry.clone(),
self.language_registry.clone(),
@@ -3184,13 +3186,17 @@ impl Panel for AgentPanel {
}
fn activation_priority(&self) -> u32 {
- 8
+ 0
}
fn enabled(&self, cx: &App) -> bool {
AgentSettings::get_global(cx).enabled(cx)
}
+ fn is_agent_panel(&self) -> bool {
+ true
+ }
+
fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
self.zoomed
}
@@ -3821,8 +3827,6 @@ impl AgentPanel {
}
}),
)
- .separator()
- .header("External Agents")
.map(|mut menu| {
let agent_server_store = agent_server_store.read(cx);
let registry_store = project::AgentRegistryStore::try_global(cx);
@@ -3853,6 +3857,9 @@ impl AgentPanel {
.sorted_unstable_by_key(|e| e.display_name.to_lowercase())
.collect::>();
+ if !agent_items.is_empty() {
+ menu = menu.separator().header("External Agents");
+ }
for item in &agent_items {
let mut entry = ContextMenuEntry::new(item.display_name.clone());
diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs
index 8f41944723e05e5a89d5df6ccc947c0d62488ff0..2395a74c281f5b84ab1f328b175fe8385ce3fb12 100644
--- a/crates/agent_ui/src/agent_ui.rs
+++ b/crates/agent_ui/src/agent_ui.rs
@@ -502,7 +502,6 @@ fn update_command_palette_filter(cx: &mut App) {
| EditPredictionProvider::Codestral
| EditPredictionProvider::Ollama
| EditPredictionProvider::OpenAiCompatibleApi
- | EditPredictionProvider::Sweep
| EditPredictionProvider::Mercury
| EditPredictionProvider::Experimental(_) => {
filter.show_namespace("edit_prediction");
@@ -649,7 +648,6 @@ mod tests {
default_profile: AgentProfileId::default(),
default_view: DefaultAgentView::Thread,
profiles: Default::default(),
-
notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
play_sound_when_agent_done: false,
single_file_review: false,
@@ -663,6 +661,7 @@ mod tests {
tool_permissions: Default::default(),
show_turn_stats: false,
new_thread_location: Default::default(),
+ sidebar_side: Default::default(),
};
cx.update(|cx| {
diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs
index 740beabce22ab6eb476b8c60b281c3ebc9d9df12..a3c87c8d66031f553bcd4cb8dc82c681a0b79c94 100644
--- a/crates/agent_ui/src/conversation_view.rs
+++ b/crates/agent_ui/src/conversation_view.rs
@@ -237,6 +237,20 @@ impl Conversation {
))
}
+ pub fn subagents_awaiting_permission(&self, cx: &App) -> Vec<(acp::SessionId, usize)> {
+ self.permission_requests
+ .iter()
+ .filter_map(|(session_id, tool_call_ids)| {
+ let thread = self.threads.get(session_id)?;
+ if thread.read(cx).parent_session_id().is_some() && !tool_call_ids.is_empty() {
+ Some((session_id.clone(), tool_call_ids.len()))
+ } else {
+ None
+ }
+ })
+ .collect()
+ }
+
pub fn authorize_pending_tool_call(
&mut self,
session_id: &acp::SessionId,
@@ -795,7 +809,7 @@ impl ConversationView {
});
let count = thread.read(cx).entries().len();
- let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
+ let list_state = ListState::new(0, gpui::ListAlignment::Top, px(2048.0));
entry_view_state.update(cx, |view_state, cx| {
for ix in 0..count {
view_state.sync_entry(ix, &thread, window, cx);
@@ -1255,9 +1269,11 @@ impl ConversationView {
}
AcpThreadEvent::Stopped(stop_reason) => {
if let Some(active) = self.thread_view(&thread_id) {
- active.update(cx, |active, _cx| {
+ active.update(cx, |active, cx| {
active.thread_retry_status.take();
active.clear_auto_expand_tracking();
+ active.list_state.set_follow_tail(false);
+ active.sync_generating_indicator(cx);
});
}
if is_subagent {
@@ -1325,8 +1341,10 @@ impl ConversationView {
}
AcpThreadEvent::Error => {
if let Some(active) = self.thread_view(&thread_id) {
- active.update(cx, |active, _cx| {
+ active.update(cx, |active, cx| {
active.thread_retry_status.take();
+ active.list_state.set_follow_tail(false);
+ active.sync_generating_indicator(cx);
});
}
if !is_subagent {
diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs
index ad7efa94324e910ed6f5bc63efe1cdf1b26bdc9e..4ebe196e7ca7de9c6341925676423bdc4a8d8d38 100644
--- a/crates/agent_ui/src/conversation_view/thread_view.rs
+++ b/crates/agent_ui/src/conversation_view/thread_view.rs
@@ -1,7 +1,10 @@
-use crate::{DEFAULT_THREAD_TITLE, SelectPermissionGranularity};
+use crate::{
+ DEFAULT_THREAD_TITLE, SelectPermissionGranularity,
+ agent_configuration::configure_context_server_modal::default_markdown_style,
+};
use std::cell::RefCell;
-use acp_thread::ContentBlock;
+use acp_thread::{ContentBlock, PlanEntry};
use cloud_api_types::{SubmitAgentThreadFeedbackBody, SubmitAgentThreadFeedbackCommentsBody};
use editor::actions::OpenExcerpts;
@@ -285,6 +288,7 @@ pub struct ThreadView {
pub hovered_recent_history_item: Option,
pub show_external_source_prompt_warning: bool,
pub show_codex_windows_warning: bool,
+ pub generating_indicator_in_list: bool,
pub history: Option>,
pub _history_subscription: Option,
}
@@ -525,19 +529,39 @@ impl ThreadView {
history,
_history_subscription: history_subscription,
show_codex_windows_warning,
+ generating_indicator_in_list: false,
};
+
+ this.sync_generating_indicator(cx);
let list_state_for_scroll = this.list_state.clone();
let thread_view = cx.entity().downgrade();
+
this.list_state
- .set_scroll_handler(move |_event, _window, cx| {
+ .set_scroll_handler(move |event, _window, cx| {
let list_state = list_state_for_scroll.clone();
let thread_view = thread_view.clone();
+ let is_following_tail = event.is_following_tail;
// N.B. We must defer because the scroll handler is called while the
// ListState's RefCell is mutably borrowed. Reading logical_scroll_top()
// directly would panic from a double borrow.
cx.defer(move |cx| {
let scroll_top = list_state.logical_scroll_top();
let _ = thread_view.update(cx, |this, cx| {
+ if !is_following_tail {
+ let is_at_bottom = {
+ let current_offset =
+ list_state.scroll_px_offset_for_scrollbar().y.abs();
+ let max_offset = list_state.max_offset_for_scrollbar().y;
+ current_offset >= max_offset - px(1.0)
+ };
+
+ let is_generating =
+ matches!(this.thread.read(cx).status(), ThreadStatus::Generating);
+
+ if is_at_bottom && is_generating {
+ list_state.set_follow_tail(true);
+ }
+ }
if let Some(thread) = this.as_native_thread(cx) {
thread.update(cx, |thread, _cx| {
thread.set_ui_scroll_position(Some(scroll_top));
@@ -1043,7 +1067,11 @@ impl ThreadView {
this.update_in(cx, |this, _window, cx| {
this.set_editor_is_expanded(false, cx);
})?;
- let _ = this.update(cx, |this, cx| this.scroll_to_bottom(cx));
+
+ let _ = this.update(cx, |this, cx| {
+ this.list_state.set_follow_tail(true);
+ cx.notify();
+ });
let _stop_turn = defer({
let this = this.clone();
@@ -1097,6 +1125,12 @@ impl ThreadView {
thread.send(contents, cx)
})?;
+
+ let _ = this.update(cx, |this, cx| {
+ this.sync_generating_indicator(cx);
+ cx.notify();
+ });
+
let res = send.await;
let turn_time_ms = turn_start_time.elapsed().as_millis();
drop(_stop_turn);
@@ -1236,13 +1270,13 @@ impl ThreadView {
);
}
- // generation
-
pub fn cancel_generation(&mut self, cx: &mut Context) {
self.thread_retry_status.take();
self.thread_error.take();
self.user_interrupted_generation = true;
self._cancel_task = Some(self.thread.update(cx, |thread, cx| thread.cancel(cx)));
+ self.sync_generating_indicator(cx);
+ cx.notify();
}
pub fn retry_generation(&mut self, cx: &mut Context) {
@@ -1254,6 +1288,8 @@ impl ThreadView {
}
let task = thread.update(cx, |thread, cx| thread.retry(cx));
+ self.sync_generating_indicator(cx);
+ cx.notify();
cx.spawn(async move |this, cx| {
let result = task.await;
@@ -1582,11 +1618,10 @@ impl ThreadView {
}
})
};
+ self.message_editor.focus_handle(cx).focus(window, cx);
cx.notify();
}
- // tool permissions
-
pub fn authorize_tool_call(
&mut self,
session_id: acp::SessionId,
@@ -1640,6 +1675,17 @@ impl ThreadView {
Some(())
}
+ fn is_waiting_for_confirmation(entry: &AgentThreadEntry) -> bool {
+ if let AgentThreadEntry::ToolCall(tool_call) = entry {
+ matches!(
+ tool_call.status,
+ ToolCallStatus::WaitingForConfirmation { .. }
+ )
+ } else {
+ false
+ }
+ }
+
fn handle_authorize_tool_call(
&mut self,
action: &AuthorizeToolCall,
@@ -2112,7 +2158,14 @@ impl ThreadView {
let plan = thread.plan();
let queue_is_empty = !self.has_queued_messages();
- if changed_buffers.is_empty() && plan.is_empty() && queue_is_empty {
+ let subagents_awaiting_permission = self.render_subagents_awaiting_permission(cx);
+ let has_subagents_awaiting = subagents_awaiting_permission.is_some();
+
+ if changed_buffers.is_empty()
+ && plan.is_empty()
+ && queue_is_empty
+ && !has_subagents_awaiting
+ {
return None;
}
@@ -2140,6 +2193,14 @@ impl ThreadView {
blur_radius: px(2.),
spread_radius: px(0.),
}])
+ .when_some(subagents_awaiting_permission, |this, element| {
+ this.child(element)
+ })
+ .when(
+ has_subagents_awaiting
+ && (!plan.is_empty() || !changed_buffers.is_empty() || !queue_is_empty),
+ |this| this.child(Divider::horizontal().color(DividerColor::Border)),
+ )
.when(!plan.is_empty(), |this| {
this.child(self.render_plan_summary(plan, window, cx))
.when(plan_expanded, |parent| {
@@ -2399,6 +2460,119 @@ impl ThreadView {
)
}
+ fn render_subagents_awaiting_permission(&self, cx: &Context) -> Option {
+ let awaiting = self.conversation.read(cx).subagents_awaiting_permission(cx);
+
+ if awaiting.is_empty() {
+ return None;
+ }
+
+ let thread = self.thread.read(cx);
+ let entries = thread.entries();
+ let mut subagent_items: Vec<(SharedString, usize)> = Vec::new();
+
+ for (session_id, _) in &awaiting {
+ for (entry_ix, entry) in entries.iter().enumerate() {
+ if let AgentThreadEntry::ToolCall(tool_call) = entry {
+ if let Some(info) = &tool_call.subagent_session_info {
+ if &info.session_id == session_id {
+ let subagent_summary: SharedString = {
+ let summary_text = tool_call.label.read(cx).source().to_string();
+ if !summary_text.is_empty() {
+ summary_text.into()
+ } else {
+ "Subagent".into()
+ }
+ };
+ subagent_items.push((subagent_summary, entry_ix));
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if subagent_items.is_empty() {
+ return None;
+ }
+
+ let item_count = subagent_items.len();
+
+ Some(
+ v_flex()
+ .child(
+ h_flex()
+ .py_1()
+ .px_2()
+ .w_full()
+ .gap_1()
+ .border_b_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Label::new("Subagents Awaiting Permission:")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Label::new(item_count.to_string()).size(LabelSize::Small)),
+ )
+ .child(
+ v_flex().children(subagent_items.into_iter().enumerate().map(
+ |(ix, (label, entry_ix))| {
+ let is_last = ix == item_count - 1;
+ let group = format!("group-{}", entry_ix);
+
+ h_flex()
+ .cursor_pointer()
+ .id(format!("subagent-permission-{}", entry_ix))
+ .group(&group)
+ .p_1()
+ .pl_2()
+ .min_w_0()
+ .w_full()
+ .gap_1()
+ .justify_between()
+ .bg(cx.theme().colors().editor_background)
+ .hover(|s| s.bg(cx.theme().colors().element_hover))
+ .when(!is_last, |this| {
+ this.border_b_1().border_color(cx.theme().colors().border)
+ })
+ .child(
+ h_flex()
+ .gap_1p5()
+ .child(
+ Icon::new(IconName::Circle)
+ .size(IconSize::XSmall)
+ .color(Color::Warning),
+ )
+ .child(
+ Label::new(label)
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .truncate(),
+ ),
+ )
+ .child(
+ div().visible_on_hover(&group).child(
+ Label::new("Scroll to Subagent")
+ .size(LabelSize::Small)
+ .color(Color::Muted)
+ .truncate(),
+ ),
+ )
+ .on_click(cx.listener(move |this, _, _, cx| {
+ this.list_state.scroll_to(ListOffset {
+ item_ix: entry_ix,
+ offset_in_item: px(0.0),
+ });
+ cx.notify();
+ }))
+ },
+ )),
+ )
+ .into_any(),
+ )
+ }
+
fn render_message_queue_summary(
&self,
_window: &mut Window,
@@ -2540,7 +2714,17 @@ impl ThreadView {
this.border_b_1().border_color(cx.theme().colors().border)
})
.child(Disclosure::new("plan_disclosure", plan_expanded))
- .child(title)
+ .child(title.flex_1())
+ .child(
+ IconButton::new("dismiss-plan", IconName::Close)
+ .icon_size(IconSize::XSmall)
+ .shape(ui::IconButtonShape::Square)
+ .tooltip(Tooltip::text("Clear plan"))
+ .on_click(cx.listener(|this, _, _, cx| {
+ this.thread.update(cx, |thread, cx| thread.clear_plan(cx));
+ cx.stop_propagation();
+ })),
+ )
.on_click(cx.listener(|this, _, _, cx| {
this.plan_expanded = !this.plan_expanded;
cx.notify();
@@ -2608,6 +2792,76 @@ impl ThreadView {
.into_any_element()
}
+ fn render_completed_plan(
+ &self,
+ entries: &[PlanEntry],
+ window: &Window,
+ cx: &Context,
+ ) -> AnyElement {
+ v_flex()
+ .px_5()
+ .py_1p5()
+ .w_full()
+ .child(
+ v_flex()
+ .w_full()
+ .rounded_md()
+ .border_1()
+ .border_color(self.tool_card_border_color(cx))
+ .child(
+ h_flex()
+ .px_2()
+ .py_1()
+ .gap_1()
+ .bg(self.tool_card_header_bg(cx))
+ .border_b_1()
+ .border_color(self.tool_card_border_color(cx))
+ .child(
+ Label::new("Completed Plan")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(
+ Label::new(format!(
+ "— {} {}",
+ entries.len(),
+ if entries.len() == 1 { "step" } else { "steps" }
+ ))
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .child(
+ v_flex().children(entries.iter().enumerate().map(|(index, entry)| {
+ h_flex()
+ .py_1()
+ .px_2()
+ .gap_1p5()
+ .when(index < entries.len() - 1, |this| {
+ this.border_b_1().border_color(cx.theme().colors().border)
+ })
+ .child(
+ Icon::new(IconName::TodoComplete)
+ .size(IconSize::Small)
+ .color(Color::Success),
+ )
+ .child(
+ div()
+ .max_w_full()
+ .overflow_x_hidden()
+ .text_xs()
+ .text_color(cx.theme().colors().text_muted)
+ .child(MarkdownElement::new(
+ entry.content.clone(),
+ default_markdown_style(window, cx),
+ )),
+ )
+ })),
+ ),
+ )
+ .into_any()
+ }
+
fn render_edits_summary(
&self,
changed_buffers: &BTreeMap, Entity>,
@@ -3197,22 +3451,98 @@ impl ThreadView {
})
};
- if show_split {
- let max_output_tokens = self
- .as_native_thread(cx)
- .and_then(|thread| thread.read(cx).model())
- .and_then(|model| model.max_output_tokens())
- .unwrap_or(0);
+ let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens);
+ let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens);
+ let input_tokens_label =
+ crate::text_thread_editor::humanize_token_count(usage.input_tokens);
+ let output_tokens_label =
+ crate::text_thread_editor::humanize_token_count(usage.output_tokens);
+
+ let progress_ratio = if usage.max_tokens > 0 {
+ usage.used_tokens as f32 / usage.max_tokens as f32
+ } else {
+ 0.0
+ };
+ let percentage = format!("{}%", (progress_ratio * 100.0).round() as u32);
+
+ let tooltip_separator_color = Color::Custom(cx.theme().colors().text_disabled.opacity(0.6));
+
+ let (user_rules_count, first_user_rules_id, project_rules_count, project_entry_ids) = self
+ .as_native_thread(cx)
+ .map(|thread| {
+ let project_context = thread.read(cx).project_context().read(cx);
+ let user_rules_count = project_context.user_rules.len();
+ let first_user_rules_id = project_context.user_rules.first().map(|r| r.uuid.0);
+ let project_entry_ids = project_context
+ .worktrees
+ .iter()
+ .filter_map(|wt| wt.rules_file.as_ref())
+ .map(|rf| ProjectEntryId::from_usize(rf.project_entry_id))
+ .collect::>();
+ let project_rules_count = project_entry_ids.len();
+ (
+ user_rules_count,
+ first_user_rules_id,
+ project_rules_count,
+ project_entry_ids,
+ )
+ })
+ .unwrap_or_default();
+
+ let workspace = self.workspace.clone();
+
+ let max_output_tokens = self
+ .as_native_thread(cx)
+ .and_then(|thread| thread.read(cx).model())
+ .and_then(|model| model.max_output_tokens())
+ .unwrap_or(0);
+ let input_max_label = crate::text_thread_editor::humanize_token_count(
+ usage.max_tokens.saturating_sub(max_output_tokens),
+ );
+ let output_max_label = crate::text_thread_editor::humanize_token_count(max_output_tokens);
+
+ let build_tooltip = {
+ let input_max_label = input_max_label.clone();
+ let output_max_label = output_max_label.clone();
+ move |_window: &mut Window, cx: &mut App| {
+ let percentage = percentage.clone();
+ let used = used.clone();
+ let max = max.clone();
+ let input_tokens_label = input_tokens_label.clone();
+ let output_tokens_label = output_tokens_label.clone();
+ let input_max_label = input_max_label.clone();
+ let output_max_label = output_max_label.clone();
+ let project_entry_ids = project_entry_ids.clone();
+ let workspace = workspace.clone();
+ cx.new(move |_cx| TokenUsageTooltip {
+ percentage,
+ used,
+ max,
+ input_tokens: input_tokens_label,
+ output_tokens: output_tokens_label,
+ input_max: input_max_label,
+ output_max: output_max_label,
+ show_split,
+ separator_color: tooltip_separator_color,
+ user_rules_count,
+ first_user_rules_id,
+ project_rules_count,
+ project_entry_ids,
+ workspace,
+ })
+ .into()
+ }
+ };
+ if show_split {
let input = crate::text_thread_editor::humanize_token_count(usage.input_tokens);
- let input_max = crate::text_thread_editor::humanize_token_count(
- usage.max_tokens.saturating_sub(max_output_tokens),
- );
+ let input_max = input_max_label;
let output = crate::text_thread_editor::humanize_token_count(usage.output_tokens);
- let output_max = crate::text_thread_editor::humanize_token_count(max_output_tokens);
+ let output_max = output_max_label;
Some(
h_flex()
+ .id("split_token_usage")
.flex_shrink_0()
.gap_1()
.mr_1p5()
@@ -3256,39 +3586,15 @@ impl ThreadView {
.color(Color::Muted),
),
)
+ .hoverable_tooltip(build_tooltip)
.into_any_element(),
)
} else {
- let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens);
- let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens);
- let progress_ratio = if usage.max_tokens > 0 {
- usage.used_tokens as f32 / usage.max_tokens as f32
- } else {
- 0.0
- };
-
let progress_color = if progress_ratio >= 0.85 {
cx.theme().status().warning
} else {
cx.theme().colors().text_muted
};
- let separator_color = Color::Custom(cx.theme().colors().text_disabled.opacity(0.6));
-
- let percentage = format!("{}%", (progress_ratio * 100.0).round() as u32);
-
- let (user_rules_count, project_rules_count) = self
- .as_native_thread(cx)
- .map(|thread| {
- let project_context = thread.read(cx).project_context().read(cx);
- let user_rules = project_context.user_rules.len();
- let project_rules = project_context
- .worktrees
- .iter()
- .filter(|wt| wt.rules_file.is_some())
- .count();
- (user_rules, project_rules)
- })
- .unwrap_or((0, 0));
Some(
h_flex()
@@ -3305,53 +3611,7 @@ impl ThreadView {
.stroke_width(px(2.))
.progress_color(progress_color),
)
- .tooltip(Tooltip::element({
- move |_, cx| {
- v_flex()
- .min_w_40()
- .child(
- Label::new("Context")
- .color(Color::Muted)
- .size(LabelSize::Small),
- )
- .child(
- h_flex()
- .gap_0p5()
- .child(Label::new(percentage.clone()))
- .child(Label::new("•").color(separator_color).mx_1())
- .child(Label::new(used.clone()))
- .child(Label::new("/").color(separator_color))
- .child(Label::new(max.clone()).color(Color::Muted)),
- )
- .when(user_rules_count > 0 || project_rules_count > 0, |this| {
- this.child(
- v_flex()
- .mt_1p5()
- .pt_1p5()
- .border_t_1()
- .border_color(cx.theme().colors().border_variant)
- .child(
- Label::new("Rules")
- .color(Color::Muted)
- .size(LabelSize::Small),
- )
- .when(user_rules_count > 0, |this| {
- this.child(Label::new(format!(
- "{} user rules",
- user_rules_count
- )))
- })
- .when(project_rules_count > 0, |this| {
- this.child(Label::new(format!(
- "{} project rules",
- project_rules_count
- )))
- }),
- )
- })
- .into_any_element()
- }
- }))
+ .hoverable_tooltip(build_tooltip)
.into_any_element(),
)
}
@@ -3900,16 +4160,184 @@ impl ThreadView {
}
}
+struct TokenUsageTooltip {
+ percentage: String,
+ used: String,
+ max: String,
+ input_tokens: String,
+ output_tokens: String,
+ input_max: String,
+ output_max: String,
+ show_split: bool,
+ separator_color: Color,
+ user_rules_count: usize,
+ first_user_rules_id: Option,
+ project_rules_count: usize,
+ project_entry_ids: Vec,
+ workspace: WeakEntity,
+}
+
+impl Render for TokenUsageTooltip {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ let separator_color = self.separator_color;
+ let percentage = self.percentage.clone();
+ let used = self.used.clone();
+ let max = self.max.clone();
+ let input_tokens = self.input_tokens.clone();
+ let output_tokens = self.output_tokens.clone();
+ let input_max = self.input_max.clone();
+ let output_max = self.output_max.clone();
+ let show_split = self.show_split;
+ let user_rules_count = self.user_rules_count;
+ let first_user_rules_id = self.first_user_rules_id;
+ let project_rules_count = self.project_rules_count;
+ let project_entry_ids = self.project_entry_ids.clone();
+ let workspace = self.workspace.clone();
+
+ ui::tooltip_container(cx, move |container, cx| {
+ container
+ .min_w_40()
+ .when(!show_split, |this| {
+ this.child(
+ Label::new("Context")
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ )
+ .child(
+ h_flex()
+ .gap_0p5()
+ .child(Label::new(percentage.clone()))
+ .child(Label::new("\u{2022}").color(separator_color).mx_1())
+ .child(Label::new(used.clone()))
+ .child(Label::new("/").color(separator_color))
+ .child(Label::new(max.clone()).color(Color::Muted)),
+ )
+ })
+ .when(show_split, |this| {
+ this.child(
+ v_flex()
+ .gap_0p5()
+ .child(
+ h_flex()
+ .gap_0p5()
+ .child(Label::new("Input:").color(Color::Muted).mr_0p5())
+ .child(Label::new(input_tokens))
+ .child(Label::new("/").color(separator_color))
+ .child(Label::new(input_max).color(Color::Muted)),
+ )
+ .child(
+ h_flex()
+ .gap_0p5()
+ .child(Label::new("Output:").color(Color::Muted).mr_0p5())
+ .child(Label::new(output_tokens))
+ .child(Label::new("/").color(separator_color))
+ .child(Label::new(output_max).color(Color::Muted)),
+ ),
+ )
+ })
+ .when(
+ user_rules_count > 0 || project_rules_count > 0,
+ move |this| {
+ this.child(
+ v_flex()
+ .mt_1p5()
+ .pt_1p5()
+ .pb_0p5()
+ .gap_0p5()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(
+ Label::new("Rules")
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ )
+ .child(
+ v_flex()
+ .mx_neg_1()
+ .when(user_rules_count > 0, move |this| {
+ this.child(
+ Button::new(
+ "open-user-rules",
+ format!("{} user rules", user_rules_count),
+ )
+ .end_icon(
+ Icon::new(IconName::ArrowUpRight)
+ .color(Color::Muted)
+ .size(IconSize::XSmall),
+ )
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(
+ Box::new(OpenRulesLibrary {
+ prompt_to_select: first_user_rules_id,
+ }),
+ cx,
+ );
+ }),
+ )
+ })
+ .when(project_rules_count > 0, move |this| {
+ let workspace = workspace.clone();
+ let project_entry_ids = project_entry_ids.clone();
+ this.child(
+ Button::new(
+ "open-project-rules",
+ format!(
+ "{} project rules",
+ project_rules_count
+ ),
+ )
+ .end_icon(
+ Icon::new(IconName::ArrowUpRight)
+ .color(Color::Muted)
+ .size(IconSize::XSmall),
+ )
+ .on_click(move |_, window, cx| {
+ let _ =
+ workspace.update(cx, |workspace, cx| {
+ let project =
+ workspace.project().read(cx);
+ let paths = project_entry_ids
+ .iter()
+ .flat_map(|id| {
+ project.path_for_entry(*id, cx)
+ })
+ .collect::>();
+ for path in paths {
+ workspace
+ .open_path(
+ path, None, true, window,
+ cx,
+ )
+ .detach_and_log_err(cx);
+ }
+ });
+ }),
+ )
+ }),
+ ),
+ )
+ },
+ )
+ })
+ }
+}
+
impl ThreadView {
pub(crate) fn render_entries(&mut self, cx: &mut Context) -> List {
list(
self.list_state.clone(),
cx.processor(|this, index: usize, window, cx| {
let entries = this.thread.read(cx).entries();
- let Some(entry) = entries.get(index) else {
- return Empty.into_any();
- };
- this.render_entry(index, entries.len(), entry, window, cx)
+ if let Some(entry) = entries.get(index) {
+ this.render_entry(index, entries.len(), entry, window, cx)
+ } else if this.generating_indicator_in_list {
+ let confirmation = entries
+ .last()
+ .is_some_and(|entry| Self::is_waiting_for_confirmation(entry));
+ this.render_generating(confirmation, cx).into_any_element()
+ } else {
+ Empty.into_any()
+ }
}),
)
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
@@ -3949,12 +4377,6 @@ impl ThreadView {
let editor_focus = editor.focus_handle(cx).is_focused(window);
let focus_border = cx.theme().colors().border_focused;
- let rules_item = if entry_ix == 0 {
- self.render_rules_item(cx)
- } else {
- None
- };
-
let has_checkpoint_button = message
.checkpoint
.as_ref()
@@ -3973,10 +4395,6 @@ impl ThreadView {
.map(|this| {
if is_first_indented {
this.pt_0p5()
- } else 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()
} else {
this.pt_2()
}
@@ -3985,7 +4403,6 @@ impl ThreadView {
.px_2()
.gap_1p5()
.w_full()
- .children(rules_item)
.when(is_editable && has_checkpoint_button, |this| {
this.children(message.id.clone().map(|message_id| {
h_flex()
@@ -4202,6 +4619,9 @@ impl ThreadView {
cx,
)
.into_any(),
+ AgentThreadEntry::CompletedPlan(entries) => {
+ self.render_completed_plan(entries, window, cx)
+ }
};
let is_subagent_output = self.is_subagent()
@@ -4240,6 +4660,8 @@ impl ThreadView {
primary
};
+ let thread = self.thread.clone();
+
let primary = if is_indented {
let line_top = if is_first_indented {
rems_from_px(-12.0)
@@ -4267,28 +4689,16 @@ impl ThreadView {
primary
};
- let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry {
- matches!(
- tool_call.status,
- ToolCallStatus::WaitingForConfirmation { .. }
- )
- } else {
- false
- };
+ let needs_confirmation = Self::is_waiting_for_confirmation(entry);
- let thread = self.thread.clone();
let comments_editor = self.thread_feedback.comments_editor.clone();
let primary = if entry_ix + 1 == total_entries {
v_flex()
.w_full()
.child(primary)
- .map(|this| {
- if needs_confirmation {
- this.child(self.render_generating(true, cx))
- } else {
- this.child(self.render_thread_controls(&thread, cx))
- }
+ .when(!needs_confirmation, |this| {
+ this.child(self.render_thread_controls(&thread, cx))
})
.when_some(comments_editor, |this, editor| {
this.child(Self::render_feedback_feedback_editor(editor, cx))
@@ -4372,7 +4782,7 @@ impl ThreadView {
) -> impl IntoElement {
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if is_generating {
- return self.render_generating(false, cx).into_any_element();
+ return Empty.into_any_element();
}
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
@@ -4572,13 +4982,12 @@ impl ThreadView {
});
cx.notify();
} else {
- self.scroll_to_bottom(cx);
+ self.scroll_to_end(cx);
}
}
- pub fn scroll_to_bottom(&mut self, cx: &mut Context) {
- let entry_count = self.thread.read(cx).entries().len();
- self.list_state.reset(entry_count);
+ pub fn scroll_to_end(&mut self, cx: &mut Context) {
+ self.list_state.scroll_to_end();
cx.notify();
}
@@ -4659,6 +5068,21 @@ impl ThreadView {
})
}
+ /// Ensures the list item count includes (or excludes) an extra item for the generating indicator
+ pub(crate) fn sync_generating_indicator(&mut self, cx: &App) {
+ let is_generating = matches!(self.thread.read(cx).status(), ThreadStatus::Generating);
+
+ if is_generating && !self.generating_indicator_in_list {
+ let entries_count = self.thread.read(cx).entries().len();
+ self.list_state.splice(entries_count..entries_count, 1);
+ self.generating_indicator_in_list = true;
+ } else if !is_generating && self.generating_indicator_in_list {
+ let entries_count = self.thread.read(cx).entries().len();
+ self.list_state.splice(entries_count..entries_count + 1, 0);
+ self.generating_indicator_in_list = false;
+ }
+ }
+
fn render_generating(&self, confirmation: bool, cx: &App) -> impl IntoElement {
let show_stats = AgentSettings::get_global(cx).show_turn_stats;
let elapsed_label = show_stats
@@ -4942,7 +5366,7 @@ impl ThreadView {
let entity = entity.clone();
move |_, cx| {
entity.update(cx, |this, cx| {
- this.scroll_to_bottom(cx);
+ this.scroll_to_end(cx);
});
}
})
@@ -5063,7 +5487,9 @@ impl ThreadView {
return false;
}
}
- AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
+ AgentThreadEntry::ToolCall(_)
+ | AgentThreadEntry::AssistantMessage(_)
+ | AgentThreadEntry::CompletedPlan(_) => {}
}
}
@@ -7413,113 +7839,6 @@ impl ThreadView {
}
}
- fn render_rules_item(&self, cx: &Context) -> Option {
- let project_context = self
- .as_native_thread(cx)?
- .read(cx)
- .project_context()
- .read(cx);
-
- let user_rules_text = if project_context.user_rules.is_empty() {
- None
- } else if project_context.user_rules.len() == 1 {
- let user_rules = &project_context.user_rules[0];
-
- match user_rules.title.as_ref() {
- Some(title) => Some(format!("Using \"{title}\" user rule")),
- None => Some("Using user rule".into()),
- }
- } else {
- Some(format!(
- "Using {} user rules",
- project_context.user_rules.len()
- ))
- };
-
- let first_user_rules_id = project_context
- .user_rules
- .first()
- .map(|user_rules| user_rules.uuid.0);
-
- let rules_files = project_context
- .worktrees
- .iter()
- .filter_map(|worktree| worktree.rules_file.as_ref())
- .collect::>();
-
- let rules_file_text = match rules_files.as_slice() {
- &[] => None,
- &[rules_file] => Some(format!(
- "Using project {:?} file",
- rules_file.path_in_worktree
- )),
- rules_files => Some(format!("Using {} project rules files", rules_files.len())),
- };
-
- if user_rules_text.is_none() && rules_file_text.is_none() {
- return None;
- }
-
- let has_both = user_rules_text.is_some() && rules_file_text.is_some();
-
- Some(
- h_flex()
- .px_2p5()
- .child(
- Icon::new(IconName::Attach)
- .size(IconSize::XSmall)
- .color(Color::Disabled),
- )
- .when_some(user_rules_text, |parent, user_rules_text| {
- parent.child(
- h_flex()
- .id("user-rules")
- .ml_1()
- .mr_1p5()
- .child(
- Label::new(user_rules_text)
- .size(LabelSize::XSmall)
- .color(Color::Muted)
- .truncate(),
- )
- .hover(|s| s.bg(cx.theme().colors().element_hover))
- .tooltip(Tooltip::text("View User Rules"))
- .on_click(move |_event, window, cx| {
- window.dispatch_action(
- Box::new(OpenRulesLibrary {
- prompt_to_select: first_user_rules_id,
- }),
- cx,
- )
- }),
- )
- })
- .when(has_both, |this| {
- this.child(
- Label::new("•")
- .size(LabelSize::XSmall)
- .color(Color::Disabled),
- )
- })
- .when_some(rules_file_text, |parent, rules_file_text| {
- parent.child(
- h_flex()
- .id("project-rules")
- .ml_1p5()
- .child(
- Label::new(rules_file_text)
- .size(LabelSize::XSmall)
- .color(Color::Muted),
- )
- .hover(|s| s.bg(cx.theme().colors().element_hover))
- .tooltip(Tooltip::text("View Project Rules"))
- .on_click(cx.listener(Self::handle_open_rules)),
- )
- })
- .into_any(),
- )
- }
-
fn tool_card_header_bg(&self, cx: &Context) -> Hsla {
cx.theme()
.colors()
diff --git a/crates/agent_ui/src/entry_view_state.rs b/crates/agent_ui/src/entry_view_state.rs
index ef5e8a9812e8266566f027365e4b270177aab71c..dfa76e3716f0b938e8ff53e0799c12dd1a657a88 100644
--- a/crates/agent_ui/src/entry_view_state.rs
+++ b/crates/agent_ui/src/entry_view_state.rs
@@ -235,6 +235,11 @@ impl EntryViewState {
};
entry.sync(message);
}
+ AgentThreadEntry::CompletedPlan(_) => {
+ if !matches!(self.entries.get(index), Some(Entry::CompletedPlan)) {
+ self.set_entry(index, Entry::CompletedPlan);
+ }
+ }
};
}
@@ -253,7 +258,9 @@ impl EntryViewState {
pub fn agent_ui_font_size_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
- Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
+ Entry::UserMessage { .. }
+ | Entry::AssistantMessage { .. }
+ | Entry::CompletedPlan => {}
Entry::ToolCall(ToolCallEntry { content }) => {
for view in content.values() {
if let Ok(diff_editor) = view.clone().downcast::() {
@@ -320,6 +327,7 @@ pub enum Entry {
UserMessage(Entity),
AssistantMessage(AssistantMessageEntry),
ToolCall(ToolCallEntry),
+ CompletedPlan,
}
impl Entry {
@@ -327,14 +335,14 @@ impl Entry {
match self {
Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)),
Self::AssistantMessage(message) => Some(message.focus_handle.clone()),
- Self::ToolCall(_) => None,
+ Self::ToolCall(_) | Self::CompletedPlan => None,
}
}
pub fn message_editor(&self) -> Option<&Entity> {
match self {
Self::UserMessage(editor) => Some(editor),
- Self::AssistantMessage(_) | Self::ToolCall(_) => None,
+ Self::AssistantMessage(_) | Self::ToolCall(_) | Self::CompletedPlan => None,
}
}
@@ -361,7 +369,7 @@ impl Entry {
) -> Option {
match self {
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
- Self::UserMessage(_) | Self::ToolCall(_) => None,
+ Self::UserMessage(_) | Self::ToolCall(_) | Self::CompletedPlan => None,
}
}
@@ -376,7 +384,7 @@ impl Entry {
pub fn has_content(&self) -> bool {
match self {
Self::ToolCall(ToolCallEntry { content }) => !content.is_empty(),
- Self::UserMessage(_) | Self::AssistantMessage(_) => false,
+ Self::UserMessage(_) | Self::AssistantMessage(_) | Self::CompletedPlan => false,
}
}
}
diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs
index ef4e3ab5393b1045b4de15b348c3e01e07c366bc..a5c83d48b71c08f183c842cb3b5a3d8b75db4d89 100644
--- a/crates/agent_ui/src/threads_archive_view.rs
+++ b/crates/agent_ui/src/threads_archive_view.rs
@@ -7,6 +7,7 @@ use crate::{
use acp_thread::AgentSessionInfo;
use agent::ThreadStore;
use agent_client_protocol as acp;
+use agent_settings::AgentSettings;
use chrono::{DateTime, Datelike as _, Local, NaiveDate, TimeDelta, Utc};
use editor::Editor;
use fs::Fs;
@@ -17,6 +18,7 @@ use gpui::{
use itertools::Itertools as _;
use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use project::{AgentId, AgentServerStore};
+use settings::Settings as _;
use theme::ActiveTheme;
use ui::{
ButtonLike, CommonAnimationExt, ContextMenu, ContextMenuEntry, Divider, HighlightedLabel,
@@ -795,7 +797,12 @@ impl ThreadsArchiveView {
fn render_header(&self, window: &Window, cx: &mut Context) -> impl IntoElement {
let has_query = !self.filter_editor.read(cx).text(cx).is_empty();
- let traffic_lights = cfg!(target_os = "macos") && !window.is_fullscreen();
+ let sidebar_on_left = matches!(
+ AgentSettings::get_global(cx).sidebar_side(),
+ settings::SidebarSide::Left
+ );
+ let traffic_lights =
+ cfg!(target_os = "macos") && !window.is_fullscreen() && sidebar_on_left;
let header_height = platform_title_bar_height(window);
let show_focus_keybinding =
self.selection.is_some() && !self.filter_editor.focus_handle(cx).is_focused(window);
@@ -804,15 +811,21 @@ impl ThreadsArchiveView {
.h(header_height)
.mt_px()
.pb_px()
- .when(traffic_lights, |this| {
- this.pl(px(ui::utils::TRAFFIC_LIGHT_PADDING))
+ .map(|this| {
+ if traffic_lights {
+ this.pl(px(ui::utils::TRAFFIC_LIGHT_PADDING))
+ } else {
+ this.pl_1p5()
+ }
})
.pr_1p5()
.gap_1()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border)
- .child(Divider::vertical().color(ui::DividerColor::Border))
+ .when(traffic_lights, |this| {
+ this.child(Divider::vertical().color(ui::DividerColor::Border))
+ })
.child(
h_flex()
.ml_1()
diff --git a/crates/call/src/call_impl/mod.rs b/crates/call/src/call_impl/mod.rs
index e060ec5edae6277a92c2c09ab54ded449bc56e11..b4bad6d2f350c3caa03eccbb8ca6582a71c6128c 100644
--- a/crates/call/src/call_impl/mod.rs
+++ b/crates/call/src/call_impl/mod.rs
@@ -17,8 +17,8 @@ use room::Event;
use settings::Settings;
use std::sync::Arc;
use workspace::{
- ActiveCallEvent, AnyActiveCall, GlobalAnyActiveCall, Pane, RemoteCollaborator, SharedScreen,
- Workspace,
+ ActiveCallEvent, AnyActiveCall, GlobalAnyActiveCall, MultiWorkspace, MultiWorkspaceEvent, Pane,
+ RemoteCollaborator, SharedScreen, Workspace,
};
pub use livekit_client::{RemoteVideoTrack, RemoteVideoTrackView, RemoteVideoTrackViewEvent};
@@ -28,6 +28,36 @@ use crate::call_settings::CallSettings;
pub fn init(client: Arc, user_store: Entity, cx: &mut App) {
let active_call = cx.new(|cx| ActiveCall::new(client, user_store, cx));
+ let active_call_handle = active_call.downgrade();
+
+ cx.observe_new(move |_multi_workspace: &mut MultiWorkspace, window, cx| {
+ let Some(window) = window else {
+ return;
+ };
+
+ let active_call_handle = active_call_handle.clone();
+ cx.subscribe_in(
+ &cx.entity(),
+ window,
+ move |multi_workspace, _, event: &MultiWorkspaceEvent, window, cx| {
+ if !matches!(event, MultiWorkspaceEvent::ActiveWorkspaceChanged)
+ && window.is_window_active()
+ {
+ return;
+ }
+
+ let project = multi_workspace.workspace().read(cx).project().clone();
+ if let Ok(task) = active_call_handle.update(cx, |active_call, cx| {
+ active_call.set_location(Some(&project), cx)
+ }) {
+ task.detach_and_log_err(cx);
+ }
+ },
+ )
+ .detach();
+ })
+ .detach();
+
cx.set_global(GlobalAnyActiveCall(Arc::new(ActiveCallEntity(active_call))))
}
diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs
index 3c32e76fea6dcd64b2a8b74c565544954af28c44..4e3e1ec1bfac253f7d9dae3b01fdc9a17b9acd34 100644
--- a/crates/collab_ui/src/collab_panel.rs
+++ b/crates/collab_ui/src/collab_panel.rs
@@ -61,6 +61,8 @@ actions!(
///
/// Use `collab::OpenChannelNotes` to open the channel notes for the current call.
OpenSelectedChannelNotes,
+ /// Toggles whether the selected channel is in the Favorites section.
+ ToggleSelectedChannelFavorite,
/// Starts moving a channel to a new location.
StartMoveChannel,
/// Moves the selected item to the current location.
@@ -256,6 +258,7 @@ pub struct CollabPanel {
subscriptions: Vec,
collapsed_sections: Vec,
collapsed_channels: Vec,
+ favorite_channels: Vec,
filter_active_channels: bool,
workspace: WeakEntity,
}
@@ -263,11 +266,14 @@ pub struct CollabPanel {
#[derive(Serialize, Deserialize)]
struct SerializedCollabPanel {
collapsed_channels: Option>,
+ #[serde(default)]
+ favorite_channels: Option>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
enum Section {
ActiveCall,
+ FavoriteChannels,
Channels,
ChannelInvites,
ContactRequests,
@@ -387,6 +393,7 @@ impl CollabPanel {
match_candidates: Vec::default(),
collapsed_sections: vec![Section::Offline],
collapsed_channels: Vec::default(),
+ favorite_channels: Vec::default(),
filter_active_channels: false,
workspace: workspace.weak_handle(),
client: workspace.app_state().client.clone(),
@@ -460,7 +467,13 @@ impl CollabPanel {
panel.update(cx, |panel, cx| {
panel.collapsed_channels = serialized_panel
.collapsed_channels
- .unwrap_or_else(Vec::new)
+ .unwrap_or_default()
+ .iter()
+ .map(|cid| ChannelId(*cid))
+ .collect();
+ panel.favorite_channels = serialized_panel
+ .favorite_channels
+ .unwrap_or_default()
.iter()
.map(|cid| ChannelId(*cid))
.collect();
@@ -493,12 +506,22 @@ impl CollabPanel {
} else {
Some(self.collapsed_channels.iter().map(|id| id.0).collect())
};
+
+ let favorite_channels = if self.favorite_channels.is_empty() {
+ None
+ } else {
+ Some(self.favorite_channels.iter().map(|id| id.0).collect())
+ };
+
let kvp = KeyValueStore::global(cx);
self.pending_serialization = cx.background_spawn(
async move {
kvp.write_kvp(
serialization_key,
- serde_json::to_string(&SerializedCollabPanel { collapsed_channels })?,
+ serde_json::to_string(&SerializedCollabPanel {
+ collapsed_channels,
+ favorite_channels,
+ })?,
)
.await?;
anyhow::Ok(())
@@ -512,10 +535,8 @@ impl CollabPanel {
}
fn update_entries(&mut self, select_same_item: bool, cx: &mut Context) {
- let channel_store = self.channel_store.read(cx);
- let user_store = self.user_store.read(cx);
let query = self.filter_editor.read(cx).text(cx);
- let fg_executor = cx.foreground_executor();
+ let fg_executor = cx.foreground_executor().clone();
let executor = cx.background_executor().clone();
let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
@@ -541,7 +562,7 @@ impl CollabPanel {
}
// Populate the active user.
- if let Some(user) = user_store.current_user() {
+ if let Some(user) = self.user_store.read(cx).current_user() {
self.match_candidates.clear();
self.match_candidates
.push(StringMatchCandidate::new(0, &user.github_login));
@@ -662,6 +683,64 @@ impl CollabPanel {
let mut request_entries = Vec::new();
+ if self.channel_store.read(cx).channel_count() > 0 {
+ let previous_len = self.favorite_channels.len();
+ self.favorite_channels
+ .retain(|id| self.channel_store.read(cx).channel_for_id(*id).is_some());
+ if self.favorite_channels.len() != previous_len {
+ self.serialize(cx);
+ }
+ }
+
+ let channel_store = self.channel_store.read(cx);
+ let user_store = self.user_store.read(cx);
+
+ if !self.favorite_channels.is_empty() {
+ let favorite_channels: Vec<_> = self
+ .favorite_channels
+ .iter()
+ .filter_map(|id| channel_store.channel_for_id(*id))
+ .collect();
+
+ self.match_candidates.clear();
+ self.match_candidates.extend(
+ favorite_channels
+ .iter()
+ .enumerate()
+ .map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)),
+ );
+
+ let matches = fg_executor.block_on(match_strings(
+ &self.match_candidates,
+ &query,
+ true,
+ true,
+ usize::MAX,
+ &Default::default(),
+ executor.clone(),
+ ));
+
+ if !matches.is_empty() || query.is_empty() {
+ self.entries
+ .push(ListEntry::Header(Section::FavoriteChannels));
+
+ let matches_by_candidate: HashMap =
+ matches.iter().map(|mat| (mat.candidate_id, mat)).collect();
+
+ for (ix, channel) in favorite_channels.iter().enumerate() {
+ if !query.is_empty() && !matches_by_candidate.contains_key(&ix) {
+ continue;
+ }
+ self.entries.push(ListEntry::Channel {
+ channel: (*channel).clone(),
+ depth: 0,
+ has_children: false,
+ string_match: matches_by_candidate.get(&ix).cloned().cloned(),
+ });
+ }
+ }
+ }
+
self.entries.push(ListEntry::Header(Section::Channels));
if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
@@ -1359,6 +1438,18 @@ impl CollabPanel {
window.handler_for(&this, move |this, _, cx| {
this.copy_channel_notes_link(channel_id, cx)
}),
+ )
+ .separator()
+ .entry(
+ if self.is_channel_favorited(channel_id) {
+ "Remove from Favorites"
+ } else {
+ "Add to Favorites"
+ },
+ None,
+ window.handler_for(&this, move |this, _window, cx| {
+ this.toggle_favorite_channel(channel_id, cx)
+ }),
);
let mut has_destructive_actions = false;
@@ -1608,7 +1699,8 @@ impl CollabPanel {
Section::ActiveCall => Self::leave_call(window, cx),
Section::Channels => self.new_root_channel(window, cx),
Section::Contacts => self.toggle_contact_finder(window, cx),
- Section::ContactRequests
+ Section::FavoriteChannels
+ | Section::ContactRequests
| Section::Online
| Section::Offline
| Section::ChannelInvites => {
@@ -1838,6 +1930,24 @@ impl CollabPanel {
self.collapsed_channels.binary_search(&channel_id).is_ok()
}
+ fn toggle_favorite_channel(&mut self, channel_id: ChannelId, cx: &mut Context) {
+ match self.favorite_channels.binary_search(&channel_id) {
+ Ok(ix) => {
+ self.favorite_channels.remove(ix);
+ }
+ Err(ix) => {
+ self.favorite_channels.insert(ix, channel_id);
+ }
+ };
+ self.serialize(cx);
+ self.update_entries(true, cx);
+ cx.notify();
+ }
+
+ fn is_channel_favorited(&self, channel_id: ChannelId) -> bool {
+ self.favorite_channels.binary_search(&channel_id).is_ok()
+ }
+
fn leave_call(window: &mut Window, cx: &mut App) {
ActiveCall::global(cx)
.update(cx, |call, cx| call.hang_up(cx))
@@ -1954,6 +2064,17 @@ impl CollabPanel {
}
}
+ fn toggle_selected_channel_favorite(
+ &mut self,
+ _: &ToggleSelectedChannelFavorite,
+ _window: &mut Window,
+ cx: &mut Context,
+ ) {
+ if let Some(channel) = self.selected_channel() {
+ self.toggle_favorite_channel(channel.id, cx);
+ }
+ }
+
fn set_channel_visibility(
&mut self,
channel_id: ChannelId,
@@ -2340,46 +2461,57 @@ impl CollabPanel {
fn render_signed_out(&mut self, cx: &mut Context) -> Div {
let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more.";
- let is_signing_in = self.client.status().borrow().is_signing_in();
- let button_label = if is_signing_in {
- "Signing in…"
+
+ // Two distinct "not connected" states:
+ // - Authenticated (has credentials): user just needs to connect.
+ // - Unauthenticated (no credentials): user needs to sign in via GitHub.
+ let is_authenticated = self.client.user_id().is_some();
+ let status = *self.client.status().borrow();
+ let is_busy = status.is_signing_in();
+
+ let (button_id, button_label, button_icon) = if is_authenticated {
+ (
+ "connect",
+ if is_busy { "Connecting…" } else { "Connect" },
+ IconName::Public,
+ )
} else {
- "Sign in"
+ (
+ "sign_in",
+ if is_busy {
+ "Signing in…"
+ } else {
+ "Sign In with GitHub"
+ },
+ IconName::Github,
+ )
};
v_flex()
- .gap_6()
.p_4()
+ .gap_4()
+ .size_full()
+ .text_center()
+ .justify_center()
.child(Label::new(collab_blurb))
.child(
- v_flex()
- .gap_2()
- .child(
- Button::new("sign_in", button_label)
- .start_icon(Icon::new(IconName::Github).color(Color::Muted))
- .style(ButtonStyle::Filled)
- .full_width()
- .disabled(is_signing_in)
- .on_click(cx.listener(|this, _, window, cx| {
- let client = this.client.clone();
- let workspace = this.workspace.clone();
- cx.spawn_in(window, async move |_, mut cx| {
- client
- .connect(true, &mut cx)
- .await
- .into_response()
- .notify_workspace_async_err(workspace, &mut cx);
- })
- .detach()
- })),
- )
- .child(
- v_flex().w_full().items_center().child(
- Label::new("Sign in to enable collaboration.")
- .color(Color::Muted)
- .size(LabelSize::Small),
- ),
- ),
+ Button::new(button_id, button_label)
+ .full_width()
+ .start_icon(Icon::new(button_icon).color(Color::Muted))
+ .style(ButtonStyle::Outlined)
+ .disabled(is_busy)
+ .on_click(cx.listener(|this, _, window, cx| {
+ let client = this.client.clone();
+ let workspace = this.workspace.clone();
+ cx.spawn_in(window, async move |_, mut cx| {
+ client
+ .connect(true, &mut cx)
+ .await
+ .into_response()
+ .notify_workspace_async_err(workspace, &mut cx);
+ })
+ .detach()
+ })),
)
}
@@ -2578,6 +2710,7 @@ impl CollabPanel {
SharedString::from("Current Call")
}
}
+ Section::FavoriteChannels => SharedString::from("Favorites"),
Section::ContactRequests => SharedString::from("Requests"),
Section::Contacts => SharedString::from("Contacts"),
Section::Channels => SharedString::from("Channels"),
@@ -2595,6 +2728,7 @@ impl CollabPanel {
}),
Section::Contacts => Some(
IconButton::new("add-contact", IconName::Plus)
+ .icon_size(IconSize::Small)
.on_click(
cx.listener(|this, _, window, cx| this.toggle_contact_finder(window, cx)),
)
@@ -2608,9 +2742,6 @@ impl CollabPanel {
IconButton::new("filter-active-channels", IconName::ListFilter)
.icon_size(IconSize::Small)
.toggle_state(self.filter_active_channels)
- .when(!self.filter_active_channels, |button| {
- button.visible_on_hover("section-header")
- })
.on_click(cx.listener(|this, _, _window, cx| {
this.filter_active_channels = !this.filter_active_channels;
this.update_entries(true, cx);
@@ -2623,10 +2754,11 @@ impl CollabPanel {
)
.child(
IconButton::new("add-channel", IconName::Plus)
+ .icon_size(IconSize::Small)
.on_click(cx.listener(|this, _, window, cx| {
this.new_root_channel(window, cx)
}))
- .tooltip(Tooltip::text("Create a channel")),
+ .tooltip(Tooltip::text("Create Channel")),
)
.into_any_element(),
)
@@ -2635,7 +2767,11 @@ impl CollabPanel {
};
let can_collapse = match section {
- Section::ActiveCall | Section::Channels | Section::Contacts => false,
+ Section::ActiveCall
+ | Section::Channels
+ | Section::Contacts
+ | Section::FavoriteChannels => false,
+
Section::ChannelInvites
| Section::ContactRequests
| Section::Online
@@ -2921,11 +3057,17 @@ impl CollabPanel {
.unwrap_or(px(240.));
let root_id = channel.root_id();
- div()
- .h_6()
+ let is_favorited = self.is_channel_favorited(channel_id);
+ let (favorite_icon, favorite_color, favorite_tooltip) = if is_favorited {
+ (IconName::StarFilled, Color::Accent, "Remove from Favorites")
+ } else {
+ (IconName::Star, Color::Muted, "Add to Favorites")
+ };
+
+ h_flex()
.id(channel_id.0 as usize)
.group("")
- .flex()
+ .h_6()
.w_full()
.when(!channel.is_root_channel(), |el| {
el.on_drag(channel.clone(), move |channel, _, _, cx| {
@@ -2955,6 +3097,7 @@ impl CollabPanel {
.child(
ListItem::new(channel_id.0 as usize)
// Add one level of depth for the disclosure arrow.
+ .height(px(26.))
.indent_level(depth + 1)
.indent_step_size(px(20.))
.toggle_state(is_selected || is_active)
@@ -2980,78 +3123,105 @@ impl CollabPanel {
)
},
))
- .start_slot(
- div()
- .relative()
- .child(
- Icon::new(if is_public {
- IconName::Public
- } else {
- IconName::Hash
- })
- .size(IconSize::Small)
- .color(Color::Muted),
- )
- .children(has_notes_notification.then(|| {
- div()
- .w_1p5()
- .absolute()
- .right(px(-1.))
- .top(px(-1.))
- .child(Indicator::dot().color(Color::Info))
- })),
- )
.child(
h_flex()
- .id(channel_id.0 as usize)
- .child(match string_match {
- None => Label::new(channel.name.clone()).into_any_element(),
- Some(string_match) => HighlightedLabel::new(
- channel.name.clone(),
- string_match.positions.clone(),
- )
- .into_any_element(),
- })
- .children(face_pile.map(|face_pile| face_pile.p_1())),
+ .id(format!("inside-{}", channel_id.0))
+ .w_full()
+ .gap_1()
+ .child(
+ div()
+ .relative()
+ .child(
+ Icon::new(if is_public {
+ IconName::Public
+ } else {
+ IconName::Hash
+ })
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .children(has_notes_notification.then(|| {
+ div()
+ .w_1p5()
+ .absolute()
+ .right(px(-1.))
+ .top(px(-1.))
+ .child(Indicator::dot().color(Color::Info))
+ })),
+ )
+ .child(
+ h_flex()
+ .id(channel_id.0 as usize)
+ .child(match string_match {
+ None => Label::new(channel.name.clone()).into_any_element(),
+ Some(string_match) => HighlightedLabel::new(
+ channel.name.clone(),
+ string_match.positions.clone(),
+ )
+ .into_any_element(),
+ })
+ .children(face_pile.map(|face_pile| face_pile.p_1())),
+ )
+ .tooltip({
+ let channel_store = self.channel_store.clone();
+ move |_window, cx| {
+ cx.new(|_| JoinChannelTooltip {
+ channel_store: channel_store.clone(),
+ channel_id,
+ has_notes_notification,
+ })
+ .into()
+ }
+ }),
),
)
.child(
- h_flex().absolute().right(rems(0.)).h_full().child(
- h_flex()
- .h_full()
- .bg(cx.theme().colors().background)
- .rounded_l_sm()
- .gap_1()
- .px_1()
- .child(
- IconButton::new("channel_notes", IconName::Reader)
- .style(ButtonStyle::Filled)
- .shape(ui::IconButtonShape::Square)
- .icon_size(IconSize::Small)
- .icon_color(if has_notes_notification {
- Color::Default
- } else {
- Color::Muted
- })
- .on_click(cx.listener(move |this, _, window, cx| {
- this.open_channel_notes(channel_id, window, cx)
- }))
- .tooltip(Tooltip::text("Open channel notes")),
- )
- .visible_on_hover(""),
- ),
- )
- .tooltip({
- let channel_store = self.channel_store.clone();
- move |_window, cx| {
- cx.new(|_| JoinChannelTooltip {
- channel_store: channel_store.clone(),
- channel_id,
- has_notes_notification,
+ h_flex()
+ .absolute()
+ .right_0()
+ .visible_on_hover("")
+ .h_full()
+ .pl_1()
+ .pr_1p5()
+ .gap_0p5()
+ .bg(cx.theme().colors().background.opacity(0.5))
+ .child({
+ let focus_handle = self.focus_handle.clone();
+ IconButton::new("channel_favorite", favorite_icon)
+ .icon_size(IconSize::Small)
+ .icon_color(favorite_color)
+ .on_click(cx.listener(move |this, _, _window, cx| {
+ this.toggle_favorite_channel(channel_id, cx)
+ }))
+ .tooltip(move |_window, cx| {
+ Tooltip::for_action_in(
+ favorite_tooltip,
+ &ToggleSelectedChannelFavorite,
+ &focus_handle,
+ cx,
+ )
+ })
})
- .into()
- }
- })
+ .child({
+ let focus_handle = self.focus_handle.clone();
+ IconButton::new("channel_notes", IconName::Reader)
+ .icon_size(IconSize::Small)
+ .when(!has_notes_notification, |this| {
+ this.icon_color(Color::Muted)
+ })
+ .on_click(cx.listener(move |this, _, window, cx| {
+ this.open_channel_notes(channel_id, window, cx)
+ }))
+ .tooltip(move |_window, cx| {
+ Tooltip::for_action_in(
+ "Open Channel Notes",
+ &OpenSelectedChannelNotes,
+ &focus_handle,
+ cx,
+ )
+ })
+ }),
+ )
}
fn render_channel_editor(
@@ -3150,6 +3320,7 @@ impl Render for CollabPanel {
.on_action(cx.listener(CollabPanel::show_inline_context_menu))
.on_action(cx.listener(CollabPanel::rename_selected_channel))
.on_action(cx.listener(CollabPanel::open_selected_channel_notes))
+ .on_action(cx.listener(CollabPanel::toggle_selected_channel_favorite))
.on_action(cx.listener(CollabPanel::collapse_selected_channel))
.on_action(cx.listener(CollabPanel::expand_selected_channel))
.on_action(cx.listener(CollabPanel::start_move_selected_channel))
@@ -3371,7 +3542,7 @@ impl Render for JoinChannelTooltip {
.channel_participants(self.channel_id);
container
- .child(Label::new("Join channel"))
+ .child(Label::new("Join Channel"))
.children(participants.iter().map(|participant| {
h_flex()
.gap_2()
diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs
index a40dc154b0f4e498f435080572ed5a7161917ab3..d7fef4873c687ab23a25b3144ba902cf4c42c137 100644
--- a/crates/collab_ui/src/notification_panel.rs
+++ b/crates/collab_ui/src/notification_panel.rs
@@ -640,7 +640,7 @@ impl Panel for NotificationPanel {
}
fn activation_priority(&self) -> u32 {
- 3
+ 4
}
}
diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs
index 3f2ba98de7519e8343f4bc1791a6d8f7f36b3e86..c2d8a7a5478cfc9eae53f9e7a6018864865a4d1a 100644
--- a/crates/debugger_ui/src/debugger_panel.rs
+++ b/crates/debugger_ui/src/debugger_panel.rs
@@ -1601,7 +1601,7 @@ impl Panel for DebugPanel {
}
fn activation_priority(&self) -> u32 {
- 9
+ 7
}
fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context) {}
diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs
index 8329a6baf04061cc33e8130a4e6b3a33b35267b6..fd8fd736b9e5194d34df3928c0c2983bb40be954 100644
--- a/crates/debugger_ui/src/session/running/variable_list.rs
+++ b/crates/debugger_ui/src/session/running/variable_list.rs
@@ -1076,7 +1076,12 @@ impl VariableList {
presentation_hint: Option<&VariablePresentationHint>,
cx: &Context,
) -> VariableColor {
- let syntax_color_for = |name| cx.theme().syntax().get(name).color;
+ let syntax_color_for = |name| {
+ cx.theme()
+ .syntax()
+ .style_for_name(name)
+ .and_then(|style| style.color)
+ };
let name = if self.disabled {
Some(Color::Disabled.color(cx))
} else {
diff --git a/crates/debugger_ui/src/tests/inline_values.rs b/crates/debugger_ui/src/tests/inline_values.rs
index 379bc4c98f5341b089b5936ed8571da5a6280723..3ca29f7cc3e99514d8a664d9218593c3640b27dc 100644
--- a/crates/debugger_ui/src/tests/inline_values.rs
+++ b/crates/debugger_ui/src/tests/inline_values.rs
@@ -1826,7 +1826,7 @@ def process_data(untyped_param, typed_param: int, another_typed: str):
}
fn python_lang() -> Language {
- let debug_variables_query = include_str!("../../../languages/src/python/debugger.scm");
+ let debug_variables_query = include_str!("../../../grammars/src/python/debugger.scm");
Language::new(
LanguageConfig {
name: "Python".into(),
@@ -1843,7 +1843,7 @@ fn python_lang() -> Language {
}
fn go_lang() -> Arc {
- let debug_variables_query = include_str!("../../../languages/src/go/debugger.scm");
+ let debug_variables_query = include_str!("../../../grammars/src/go/debugger.scm");
Arc::new(
Language::new(
LanguageConfig {
@@ -2262,7 +2262,7 @@ fn main() {
}
fn javascript_lang() -> Arc {
- let debug_variables_query = include_str!("../../../languages/src/javascript/debugger.scm");
+ let debug_variables_query = include_str!("../../../grammars/src/javascript/debugger.scm");
Arc::new(
Language::new(
LanguageConfig {
@@ -2281,7 +2281,7 @@ fn javascript_lang() -> Arc {
}
fn typescript_lang() -> Arc {
- let debug_variables_query = include_str!("../../../languages/src/typescript/debugger.scm");
+ let debug_variables_query = include_str!("../../../grammars/src/typescript/debugger.scm");
Arc::new(
Language::new(
LanguageConfig {
@@ -2300,7 +2300,7 @@ fn typescript_lang() -> Arc {
}
fn tsx_lang() -> Arc {
- let debug_variables_query = include_str!("../../../languages/src/tsx/debugger.scm");
+ let debug_variables_query = include_str!("../../../grammars/src/tsx/debugger.scm");
Arc::new(
Language::new(
LanguageConfig {
diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs
index 43efbeea0b0310cf70cd9bdb560b1b0d2b0c14ef..fc1bc404244a4896e7d13fbb0e9c81674438568f 100644
--- a/crates/docs_preprocessor/src/main.rs
+++ b/crates/docs_preprocessor/src/main.rs
@@ -22,8 +22,45 @@ static KEYMAP_WINDOWS: LazyLock = LazyLock::new(|| {
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
});
+static KEYMAP_JETBRAINS_MACOS: LazyLock = LazyLock::new(|| {
+ load_keymap("keymaps/macos/jetbrains.json").expect("Failed to load JetBrains macOS keymap")
+});
+
+static KEYMAP_JETBRAINS_LINUX: LazyLock = LazyLock::new(|| {
+ load_keymap("keymaps/linux/jetbrains.json").expect("Failed to load JetBrains Linux keymap")
+});
+
static ALL_ACTIONS: LazyLock = LazyLock::new(load_all_actions);
+#[derive(Clone, Copy)]
+#[allow(dead_code)]
+enum Os {
+ MacOs,
+ Linux,
+ Windows,
+}
+
+#[derive(Clone, Copy)]
+enum KeymapOverlay {
+ JetBrains,
+}
+
+impl KeymapOverlay {
+ fn parse(name: &str) -> Option {
+ match name {
+ "jetbrains" => Some(Self::JetBrains),
+ _ => None,
+ }
+ }
+
+ fn keymap(self, os: Os) -> &'static KeymapFile {
+ match (self, os) {
+ (Self::JetBrains, Os::MacOs) => &KEYMAP_JETBRAINS_MACOS,
+ (Self::JetBrains, Os::Linux | Os::Windows) => &KEYMAP_JETBRAINS_LINUX,
+ }
+ }
+}
+
const FRONT_MATTER_COMMENT: &str = "";
fn main() -> Result<()> {
@@ -64,6 +101,9 @@ enum PreprocessorError {
snippet: String,
error: String,
},
+ UnknownKeymapOverlay {
+ overlay_name: String,
+ },
}
impl PreprocessorError {
@@ -125,6 +165,13 @@ impl std::fmt::Display for PreprocessorError {
snippet
)
}
+ PreprocessorError::UnknownKeymapOverlay { overlay_name } => {
+ write!(
+ f,
+ "Unknown keymap overlay: '{}'. Supported overlays: jetbrains",
+ overlay_name
+ )
+ }
}
}
}
@@ -205,20 +252,39 @@ fn format_binding(binding: String) -> String {
}
fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet) {
- let regex = Regex::new(r"\{#kb (.*?)\}").unwrap();
+ let regex = Regex::new(r"\{#kb(?::(\w+))?\s+(.*?)\}").unwrap();
for_each_chapter_mut(book, |chapter| {
chapter.content = regex
.replace_all(&chapter.content, |caps: ®ex::Captures| {
- let action = caps[1].trim();
+ let overlay_name = caps.get(1).map(|m| m.as_str());
+ let action = caps[2].trim();
+
if is_missing_action(action) {
errors.insert(PreprocessorError::new_for_not_found_action(
action.to_string(),
));
return String::new();
}
- let macos_binding = find_binding("macos", action).unwrap_or_default();
- let linux_binding = find_binding("linux", action).unwrap_or_default();
+
+ let overlay = if let Some(name) = overlay_name {
+ let Some(overlay) = KeymapOverlay::parse(name) else {
+ errors.insert(PreprocessorError::UnknownKeymapOverlay {
+ overlay_name: name.to_string(),
+ });
+ return String::new();
+ };
+ Some(overlay)
+ } else {
+ None
+ };
+
+ let macos_binding =
+ find_binding_with_overlay(Os::MacOs, action, overlay)
+ .unwrap_or_default();
+ let linux_binding =
+ find_binding_with_overlay(Os::Linux, action, overlay)
+ .unwrap_or_default();
if macos_binding.is_empty() && linux_binding.is_empty() {
return "No default binding
".to_string();
@@ -227,7 +293,7 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet{formatted_macos_binding}|{formatted_linux_binding}")
+ format!("{formatted_macos_binding}|{formatted_linux_binding}")
})
.into_owned()
});
@@ -270,15 +336,8 @@ fn is_missing_action(name: &str) -> bool {
actions_available() && find_action_by_name(name).is_none()
}
-fn find_binding(os: &str, action: &str) -> Option {
- let keymap = match os {
- "macos" => &KEYMAP_MACOS,
- "linux" | "freebsd" => &KEYMAP_LINUX,
- "windows" => &KEYMAP_WINDOWS,
- _ => unreachable!("Not a valid OS: {}", os),
- };
-
- // Find the binding in reverse order, as the last binding takes precedence.
+// Find the binding in reverse order, as the last binding takes precedence.
+fn find_binding_in_keymap(keymap: &KeymapFile, action: &str) -> Option {
keymap.sections().rev().find_map(|section| {
section.bindings().rev().find_map(|(keystroke, a)| {
if name_for_action(a.to_string()) == action {
@@ -290,6 +349,25 @@ fn find_binding(os: &str, action: &str) -> Option {
})
}
+fn find_binding(os: Os, action: &str) -> Option {
+ let keymap = match os {
+ Os::MacOs => &KEYMAP_MACOS,
+ Os::Linux => &KEYMAP_LINUX,
+ Os::Windows => &KEYMAP_WINDOWS,
+ };
+ find_binding_in_keymap(keymap, action)
+}
+
+fn find_binding_with_overlay(
+ os: Os,
+ action: &str,
+ overlay: Option,
+) -> Option {
+ overlay
+ .and_then(|overlay| find_binding_in_keymap(overlay.keymap(os), action))
+ .or_else(|| find_binding(os, action))
+}
+
fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet) {
let settings_schema = SettingsStore::json_schema(&Default::default());
let settings_validator = jsonschema::validator_for(&settings_schema)
diff --git a/crates/edit_prediction/Cargo.toml b/crates/edit_prediction/Cargo.toml
index a6a7d8777cbf0d52575489e91a5ae03be2d031ea..75a589dea8f9c7fefe7bf13400cbdde54bf90bf1 100644
--- a/crates/edit_prediction/Cargo.toml
+++ b/crates/edit_prediction/Cargo.toml
@@ -18,7 +18,6 @@ cli-support = []
ai_onboarding.workspace = true
anyhow.workspace = true
heapless.workspace = true
-brotli.workspace = true
buffer_diff.workspace = true
client.workspace = true
clock.workspace = true
diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs
index 421a51b055693617a915e622b617298f5f8a01c5..3ae4eb72b3a60ab56d865a235c43e2f0e3adab31 100644
--- a/crates/edit_prediction/src/edit_prediction.rs
+++ b/crates/edit_prediction/src/edit_prediction.rs
@@ -50,7 +50,8 @@ use std::path::Path;
use std::rc::Rc;
use std::str::FromStr as _;
use std::sync::Arc;
-use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
+use std::time::{Duration, Instant};
+
use thiserror::Error;
use util::{RangeExt as _, ResultExt as _};
@@ -63,7 +64,6 @@ pub mod ollama;
mod onboarding_modal;
pub mod open_ai_response;
mod prediction;
-pub mod sweep_ai;
pub mod udiff;
@@ -83,7 +83,6 @@ use crate::onboarding_modal::ZedPredictModal;
pub use crate::prediction::EditPrediction;
pub use crate::prediction::EditPredictionId;
use crate::prediction::EditPredictionResult;
-pub use crate::sweep_ai::SweepAi;
pub use capture_example::capture_example;
pub use language_model::ApiKeyState;
pub use telemetry_events::EditPredictionRating;
@@ -143,7 +142,6 @@ pub struct EditPredictionStore {
zeta2_raw_config: Option,
preferred_experiment: Option,
available_experiments: Vec,
- pub sweep_ai: SweepAi,
pub mercury: Mercury,
data_collection_choice: DataCollectionChoice,
reject_predictions_tx: mpsc::UnboundedSender,
@@ -163,7 +161,6 @@ pub(crate) struct EditPredictionRejectionPayload {
pub enum EditPredictionModel {
Zeta,
Fim { format: EditPredictionPromptFormat },
- Sweep,
Mercury,
}
@@ -175,13 +172,11 @@ pub struct EditPredictionModelInput {
position: Anchor,
events: Vec>,
related_files: Vec,
- recent_paths: VecDeque,
trigger: PredictEditsRequestTrigger,
diagnostic_search_range: Range,
debug_tx: Option>,
can_collect_data: bool,
is_open_source: bool,
- pub user_actions: Vec,
}
#[derive(Debug)]
@@ -220,26 +215,6 @@ pub struct EditPredictionFinishedDebugEvent {
pub model_output: Option,
}
-const USER_ACTION_HISTORY_SIZE: usize = 16;
-
-#[derive(Clone, Debug)]
-pub struct UserActionRecord {
- pub action_type: UserActionType,
- pub buffer_id: EntityId,
- pub line_number: u32,
- pub offset: usize,
- pub timestamp_epoch_ms: u64,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum UserActionType {
- InsertChar,
- InsertSelection,
- DeleteChar,
- DeleteSelection,
- CursorMovement,
-}
-
/// An event with associated metadata for reconstructing buffer state.
#[derive(Clone)]
pub struct StoredEvent {
@@ -339,19 +314,11 @@ struct ProjectState {
cancelled_predictions: HashSet,
context: Entity,
license_detection_watchers: HashMap>,
- user_actions: VecDeque,
_subscriptions: [gpui::Subscription; 2],
copilot: Option>,
}
impl ProjectState {
- fn record_user_action(&mut self, action: UserActionRecord) {
- if self.user_actions.len() >= USER_ACTION_HISTORY_SIZE {
- self.user_actions.pop_front();
- }
- self.user_actions.push_back(action);
- }
-
pub fn events(&self, cx: &App) -> Vec {
self.events
.iter()
@@ -828,7 +795,6 @@ impl EditPredictionStore {
zeta2_raw_config: Self::zeta2_raw_config_from_env(),
preferred_experiment: None,
available_experiments: Vec::new(),
- sweep_ai: SweepAi::new(cx),
mercury: Mercury::new(cx),
data_collection_choice,
@@ -939,13 +905,6 @@ impl EditPredictionStore {
pub fn icons(&self, cx: &App) -> edit_prediction_types::EditPredictionIconSet {
use ui::IconName;
match self.edit_prediction_model {
- EditPredictionModel::Sweep => {
- edit_prediction_types::EditPredictionIconSet::new(IconName::SweepAi)
- .with_disabled(IconName::SweepAiDisabled)
- .with_up(IconName::SweepAiUp)
- .with_down(IconName::SweepAiDown)
- .with_error(IconName::SweepAiError)
- }
EditPredictionModel::Mercury => {
edit_prediction_types::EditPredictionIconSet::new(IconName::Inception)
}
@@ -970,10 +929,6 @@ impl EditPredictionStore {
}
}
- pub fn has_sweep_api_token(&self, cx: &App) -> bool {
- self.sweep_ai.api_token.read(cx).has_key()
- }
-
pub fn has_mercury_api_token(&self, cx: &App) -> bool {
self.mercury.api_token.read(cx).has_key()
}
@@ -1132,7 +1087,6 @@ impl EditPredictionStore {
last_edit_prediction_refresh: None,
last_jump_prediction_refresh: None,
license_detection_watchers: HashMap::default(),
- user_actions: VecDeque::with_capacity(USER_ACTION_HISTORY_SIZE),
_subscriptions: [
cx.subscribe(&project, Self::handle_project_event),
cx.observe_release(&project, move |this, _, cx| {
@@ -1347,24 +1301,16 @@ impl EditPredictionStore {
}
let old_file = mem::replace(&mut registered_buffer.file, new_file.clone());
let old_snapshot = mem::replace(&mut registered_buffer.snapshot, new_snapshot.clone());
- let mut num_edits = 0usize;
- let mut total_deleted = 0usize;
- let mut total_inserted = 0usize;
let mut edit_range: Option> = None;
- let mut last_offset: Option = None;
let now = cx.background_executor().now();
- for (edit, anchor_range) in
+ for (_edit, anchor_range) in
new_snapshot.anchored_edits_since::(&old_snapshot.version)
{
- num_edits += 1;
- total_deleted += edit.old.len();
- total_inserted += edit.new.len();
edit_range = Some(match edit_range {
None => anchor_range,
Some(acc) => acc.start..anchor_range.end,
});
- last_offset = Some(edit.new.end);
}
let Some(edit_range) = edit_range else {
@@ -1387,32 +1333,6 @@ impl EditPredictionStore {
cx,
);
- if is_local {
- let action_type = match (total_deleted, total_inserted, num_edits) {
- (0, ins, n) if ins == n => UserActionType::InsertChar,
- (0, _, _) => UserActionType::InsertSelection,
- (del, 0, n) if del == n => UserActionType::DeleteChar,
- (_, 0, _) => UserActionType::DeleteSelection,
- (_, ins, n) if ins == n => UserActionType::InsertChar,
- (_, _, _) => UserActionType::InsertSelection,
- };
-
- if let Some(offset) = last_offset {
- let point = new_snapshot.offset_to_point(offset);
- let timestamp_epoch_ms = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|d| d.as_millis() as u64)
- .unwrap_or(0);
- project_state.record_user_action(UserActionRecord {
- action_type,
- buffer_id: buffer.entity_id(),
- line_number: point.row,
- offset,
- timestamp_epoch_ms,
- });
- }
- }
-
if !include_in_history {
return;
}
@@ -1562,9 +1482,6 @@ impl EditPredictionStore {
}
match self.edit_prediction_model {
- EditPredictionModel::Sweep => {
- sweep_ai::edit_prediction_accepted(self, current_prediction, cx)
- }
EditPredictionModel::Mercury => {
mercury::edit_prediction_accepted(
current_prediction.prediction.id,
@@ -1792,7 +1709,7 @@ impl EditPredictionStore {
&mut self,
project: &Entity,
display_type: edit_prediction_types::SuggestionDisplayType,
- cx: &mut Context,
+ _cx: &mut Context,
) {
let Some(project_state) = self.projects.get_mut(&project.entity_id()) else {
return;
@@ -1815,18 +1732,6 @@ impl EditPredictionStore {
current_prediction.was_shown = true;
}
- let display_type_changed = previous_shown_with != Some(display_type);
-
- if self.edit_prediction_model == EditPredictionModel::Sweep && display_type_changed {
- sweep_ai::edit_prediction_shown(
- &self.sweep_ai,
- self.client.clone(),
- ¤t_prediction.prediction,
- display_type,
- cx,
- );
- }
-
if is_first_non_jump_show {
self.shown_predictions
.push_front(current_prediction.prediction.clone());
@@ -1883,7 +1788,7 @@ impl EditPredictionStore {
cx,
);
}
- EditPredictionModel::Sweep | EditPredictionModel::Fim { .. } => {}
+ EditPredictionModel::Fim { .. } => {}
}
}
@@ -2108,7 +2013,6 @@ fn currently_following(project: &Entity, cx: &App) -> bool {
fn is_ep_store_provider(provider: EditPredictionProvider) -> bool {
match provider {
EditPredictionProvider::Zed
- | EditPredictionProvider::Sweep
| EditPredictionProvider::Mercury
| EditPredictionProvider::Ollama
| EditPredictionProvider::OpenAiCompatibleApi
@@ -2148,7 +2052,6 @@ impl EditPredictionStore {
let (needs_acceptance_tracking, max_pending_predictions) =
match all_language_settings(None, cx).edit_predictions.provider {
EditPredictionProvider::Zed
- | EditPredictionProvider::Sweep
| EditPredictionProvider::Mercury
| EditPredictionProvider::Experimental(_) => (true, 2),
EditPredictionProvider::Ollama => (false, 1),
@@ -2370,28 +2273,6 @@ impl EditPredictionStore {
let snapshot = active_buffer.read(cx).snapshot();
let cursor_point = position.to_point(&snapshot);
- let current_offset = position.to_offset(&snapshot);
-
- let mut user_actions: Vec =
- project_state.user_actions.iter().cloned().collect();
-
- if let Some(last_action) = user_actions.last() {
- if last_action.buffer_id == active_buffer.entity_id()
- && current_offset != last_action.offset
- {
- let timestamp_epoch_ms = SystemTime::now()
- .duration_since(UNIX_EPOCH)
- .map(|d| d.as_millis() as u64)
- .unwrap_or(0);
- user_actions.push(UserActionRecord {
- action_type: UserActionType::CursorMovement,
- buffer_id: active_buffer.entity_id(),
- line_number: cursor_point.row,
- offset: current_offset,
- timestamp_epoch_ms,
- });
- }
- }
let diagnostic_search_start = cursor_point.row.saturating_sub(DIAGNOSTIC_LINES_RANGE);
let diagnostic_search_end = cursor_point.row + DIAGNOSTIC_LINES_RANGE;
let diagnostic_search_range =
@@ -2410,8 +2291,6 @@ impl EditPredictionStore {
&& self.is_data_collection_enabled(cx)
&& matches!(self.edit_prediction_model, EditPredictionModel::Zeta);
- let recent_paths = project_state.recent_paths.clone();
-
let inputs = EditPredictionModelInput {
project: project.clone(),
buffer: active_buffer,
@@ -2419,11 +2298,9 @@ impl EditPredictionStore {
position,
events,
related_files,
- recent_paths,
trigger,
diagnostic_search_range: diagnostic_search_range,
debug_tx,
- user_actions,
can_collect_data,
is_open_source,
};
@@ -2435,7 +2312,6 @@ impl EditPredictionStore {
zeta::request_prediction_with_zeta(self, inputs, capture_data, cx)
}
EditPredictionModel::Fim { format } => fim::request_prediction(inputs, format, cx),
- EditPredictionModel::Sweep => self.sweep_ai.request_prediction_with_sweep(inputs, cx),
EditPredictionModel::Mercury => self.mercury.request_prediction(inputs, cx),
};
diff --git a/crates/edit_prediction/src/sweep_ai.rs b/crates/edit_prediction/src/sweep_ai.rs
deleted file mode 100644
index 93a9a34340cfe0b55e40d35bb4c8980dff983fa5..0000000000000000000000000000000000000000
--- a/crates/edit_prediction/src/sweep_ai.rs
+++ /dev/null
@@ -1,669 +0,0 @@
-use crate::{
- CurrentEditPrediction, DebugEvent, EditPrediction, EditPredictionFinishedDebugEvent,
- EditPredictionId, EditPredictionModelInput, EditPredictionStartedDebugEvent,
- EditPredictionStore, UserActionRecord, UserActionType, prediction::EditPredictionResult,
-};
-use anyhow::{Result, bail};
-use client::Client;
-use edit_prediction_types::SuggestionDisplayType;
-use futures::{AsyncReadExt as _, channel::mpsc};
-use gpui::{
- App, AppContext as _, Entity, Global, SharedString, Task,
- http_client::{self, AsyncBody, Method},
-};
-use language::language_settings::all_language_settings;
-use language::{Anchor, Buffer, BufferSnapshot, Point, ToOffset as _};
-use language_model::{ApiKeyState, EnvVar, env_var};
-use lsp::DiagnosticSeverity;
-use serde::{Deserialize, Serialize};
-use std::{
- fmt::{self, Write as _},
- ops::Range,
- path::Path,
- sync::Arc,
-};
-
-const SWEEP_API_URL: &str = "https://autocomplete.sweep.dev/backend/next_edit_autocomplete";
-const SWEEP_METRICS_URL: &str = "https://backend.app.sweep.dev/backend/track_autocomplete_metrics";
-
-pub struct SweepAi {
- pub api_token: Entity,
- pub debug_info: Arc,
-}
-
-impl SweepAi {
- pub fn new(cx: &mut App) -> Self {
- SweepAi {
- api_token: sweep_api_token(cx),
- debug_info: debug_info(cx),
- }
- }
-
- pub fn request_prediction_with_sweep(
- &self,
- inputs: EditPredictionModelInput,
- cx: &mut App,
- ) -> Task>> {
- let privacy_mode_enabled = all_language_settings(None, cx)
- .edit_predictions
- .sweep
- .privacy_mode;
- let debug_info = self.debug_info.clone();
- let request_start = cx.background_executor().now();
- self.api_token.update(cx, |key_state, cx| {
- _ = key_state.load_if_needed(SWEEP_CREDENTIALS_URL, |s| s, cx);
- });
-
- let buffer = inputs.buffer.clone();
- let debug_tx = inputs.debug_tx.clone();
-
- let Some(api_token) = self.api_token.read(cx).key(&SWEEP_CREDENTIALS_URL) else {
- return Task::ready(Ok(None));
- };
- let full_path: Arc = inputs
- .snapshot
- .file()
- .map(|file| file.full_path(cx))
- .unwrap_or_else(|| "untitled".into())
- .into();
-
- let project_file = project::File::from_dyn(inputs.snapshot.file());
- let repo_name = project_file
- .map(|file| file.worktree.read(cx).root_name_str())
- .unwrap_or("untitled")
- .into();
- let offset = inputs.position.to_offset(&inputs.snapshot);
- let buffer_entity_id = inputs.buffer.entity_id();
-
- let recent_buffers = inputs.recent_paths.iter().cloned();
- let http_client = cx.http_client();
-
- let recent_buffer_snapshots = recent_buffers
- .filter_map(|project_path| {
- let buffer = inputs.project.read(cx).get_open_buffer(&project_path, cx)?;
- if inputs.buffer == buffer {
- None
- } else {
- Some(buffer.read(cx).snapshot())
- }
- })
- .take(3)
- .collect::>();
-
- let result = cx.background_spawn(async move {
- let text = inputs.snapshot.text();
-
- let mut recent_changes = String::new();
- for event in &inputs.events {
- write_event(event.as_ref(), &mut recent_changes).unwrap();
- }
-
- let file_chunks = recent_buffer_snapshots
- .into_iter()
- .map(|snapshot| {
- let end_point = Point::new(30, 0).min(snapshot.max_point());
- FileChunk {
- content: snapshot.text_for_range(Point::zero()..end_point).collect(),
- file_path: snapshot
- .file()
- .map(|f| f.path().as_unix_str())
- .unwrap_or("untitled")
- .to_string(),
- start_line: 0,
- end_line: end_point.row as usize,
- timestamp: snapshot.file().and_then(|file| {
- Some(
- file.disk_state()
- .mtime()?
- .to_seconds_and_nanos_for_persistence()?
- .0,
- )
- }),
- }
- })
- .collect::>();
-
- let mut retrieval_chunks: Vec = inputs
- .related_files
- .iter()
- .flat_map(|related_file| {
- related_file.excerpts.iter().map(|excerpt| FileChunk {
- file_path: related_file.path.to_string_lossy().to_string(),
- start_line: excerpt.row_range.start as usize,
- end_line: excerpt.row_range.end as usize,
- content: excerpt.text.to_string(),
- timestamp: None,
- })
- })
- .collect();
-
- let diagnostic_entries = inputs
- .snapshot
- .diagnostics_in_range(inputs.diagnostic_search_range, false);
- let mut diagnostic_content = String::new();
- let mut diagnostic_count = 0;
-
- for entry in diagnostic_entries {
- let start_point: Point = entry.range.start;
-
- let severity = match entry.diagnostic.severity {
- DiagnosticSeverity::ERROR => "error",
- DiagnosticSeverity::WARNING => "warning",
- DiagnosticSeverity::INFORMATION => "info",
- DiagnosticSeverity::HINT => "hint",
- _ => continue,
- };
-
- diagnostic_count += 1;
-
- writeln!(
- &mut diagnostic_content,
- "{}:{}:{}: {}: {}",
- full_path.display(),
- start_point.row + 1,
- start_point.column + 1,
- severity,
- entry.diagnostic.message
- )?;
- }
-
- if !diagnostic_content.is_empty() {
- retrieval_chunks.push(FileChunk {
- file_path: "diagnostics".to_string(),
- start_line: 1,
- end_line: diagnostic_count,
- content: diagnostic_content,
- timestamp: None,
- });
- }
-
- let file_path_str = full_path.display().to_string();
- let recent_user_actions = inputs
- .user_actions
- .iter()
- .filter(|r| r.buffer_id == buffer_entity_id)
- .map(|r| to_sweep_user_action(r, &file_path_str))
- .collect();
-
- let request_body = AutocompleteRequest {
- debug_info,
- repo_name,
- file_path: full_path.clone(),
- file_contents: text.clone(),
- original_file_contents: text,
- cursor_position: offset,
- recent_changes: recent_changes.clone(),
- changes_above_cursor: true,
- multiple_suggestions: false,
- branch: None,
- file_chunks,
- retrieval_chunks,
- recent_user_actions,
- use_bytes: true,
- privacy_mode_enabled,
- };
-
- let mut buf: Vec = Vec::new();
- let writer = brotli::CompressorWriter::new(&mut buf, 4096, 1, 22);
- serde_json::to_writer(writer, &request_body)?;
- let body: AsyncBody = buf.into();
-
- let ep_inputs = zeta_prompt::ZetaPromptInput {
- events: inputs.events,
- related_files: Some(inputs.related_files.clone()),
- active_buffer_diagnostics: vec![],
- cursor_path: full_path.clone(),
- cursor_excerpt: request_body.file_contents.clone().into(),
- cursor_offset_in_excerpt: request_body.cursor_position,
- excerpt_start_row: Some(0),
- excerpt_ranges: zeta_prompt::ExcerptRanges {
- editable_150: 0..inputs.snapshot.len(),
- editable_180: 0..inputs.snapshot.len(),
- editable_350: 0..inputs.snapshot.len(),
- editable_150_context_350: 0..inputs.snapshot.len(),
- editable_180_context_350: 0..inputs.snapshot.len(),
- editable_350_context_150: 0..inputs.snapshot.len(),
- ..Default::default()
- },
- syntax_ranges: None,
- experiment: None,
- in_open_source_repo: false,
- can_collect_data: false,
- repo_url: None,
- };
-
- send_started_event(
- &debug_tx,
- &buffer,
- inputs.position,
- serde_json::to_string(&request_body).unwrap_or_default(),
- );
-
- let request = http_client::Request::builder()
- .uri(SWEEP_API_URL)
- .header("Content-Type", "application/json")
- .header("Authorization", format!("Bearer {}", api_token))
- .header("Connection", "keep-alive")
- .header("Content-Encoding", "br")
- .method(Method::POST)
- .body(body)?;
-
- let mut response = http_client.send(request).await?;
-
- let mut body = String::new();
- response.body_mut().read_to_string(&mut body).await?;
-
- if !response.status().is_success() {
- let message = format!(
- "Request failed with status: {:?}\nBody: {}",
- response.status(),
- body,
- );
- send_finished_event(&debug_tx, &buffer, inputs.position, message.clone());
- bail!(message);
- };
-
- let response: AutocompleteResponse = serde_json::from_str(&body)?;
-
- send_finished_event(&debug_tx, &buffer, inputs.position, body);
-
- let old_text = inputs
- .snapshot
- .text_for_range(response.start_index..response.end_index)
- .collect::();
- let edits = language::text_diff(&old_text, &response.completion)
- .into_iter()
- .map(|(range, text)| {
- (
- inputs
- .snapshot
- .anchor_after(response.start_index + range.start)
- ..inputs
- .snapshot
- .anchor_before(response.start_index + range.end),
- text,
- )
- })
- .collect::>();
-
- anyhow::Ok((response.autocomplete_id, edits, inputs.snapshot, ep_inputs))
- });
-
- let buffer = inputs.buffer.clone();
-
- cx.spawn(async move |cx| {
- let (id, edits, old_snapshot, inputs) = result.await?;
- anyhow::Ok(Some(
- EditPredictionResult::new(
- EditPredictionId(id.into()),
- &buffer,
- &old_snapshot,
- edits.into(),
- None,
- inputs,
- None,
- cx.background_executor().now() - request_start,
- cx,
- )
- .await,
- ))
- })
- }
-}
-
-fn send_started_event(
- debug_tx: &Option>,
- buffer: &Entity,
- position: Anchor,
- prompt: String,
-) {
- if let Some(debug_tx) = debug_tx {
- _ = debug_tx.unbounded_send(DebugEvent::EditPredictionStarted(
- EditPredictionStartedDebugEvent {
- buffer: buffer.downgrade(),
- position,
- prompt: Some(prompt),
- },
- ));
- }
-}
-
-fn send_finished_event(
- debug_tx: &Option>,
- buffer: &Entity,
- position: Anchor,
- model_output: String,
-) {
- if let Some(debug_tx) = debug_tx {
- _ = debug_tx.unbounded_send(DebugEvent::EditPredictionFinished(
- EditPredictionFinishedDebugEvent {
- buffer: buffer.downgrade(),
- position,
- model_output: Some(model_output),
- },
- ));
- }
-}
-
-pub const SWEEP_CREDENTIALS_URL: SharedString =
- SharedString::new_static("https://autocomplete.sweep.dev");
-pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
-pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock = env_var!("SWEEP_AI_TOKEN");
-
-struct GlobalSweepApiKey(Entity);
-
-impl Global for GlobalSweepApiKey {}
-
-pub fn sweep_api_token(cx: &mut App) -> Entity {
- if let Some(global) = cx.try_global::() {
- return global.0.clone();
- }
- let entity =
- cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
- cx.set_global(GlobalSweepApiKey(entity.clone()));
- entity
-}
-
-pub fn load_sweep_api_token(cx: &mut App) -> Task> {
- sweep_api_token(cx).update(cx, |key_state, cx| {
- key_state.load_if_needed(SWEEP_CREDENTIALS_URL, |s| s, cx)
- })
-}
-
-#[derive(Debug, Clone, Serialize)]
-struct AutocompleteRequest {
- pub debug_info: Arc,
- pub repo_name: String,
- pub branch: Option,
- pub file_path: Arc,
- pub file_contents: String,
- pub recent_changes: String,
- pub cursor_position: usize,
- pub original_file_contents: String,
- pub file_chunks: Vec,
- pub retrieval_chunks: Vec,
- pub recent_user_actions: Vec,
- pub multiple_suggestions: bool,
- pub privacy_mode_enabled: bool,
- pub changes_above_cursor: bool,
- pub use_bytes: bool,
-}
-
-#[derive(Debug, Clone, Serialize)]
-struct FileChunk {
- pub file_path: String,
- pub start_line: usize,
- pub end_line: usize,
- pub content: String,
- pub timestamp: Option,
-}
-
-#[derive(Debug, Clone, Serialize)]
-struct UserAction {
- pub action_type: ActionType,
- pub line_number: usize,
- pub offset: usize,
- pub file_path: String,
- pub timestamp: u64,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
-#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
-enum ActionType {
- CursorMovement,
- InsertChar,
- DeleteChar,
- InsertSelection,
- DeleteSelection,
-}
-
-fn to_sweep_user_action(record: &UserActionRecord, file_path: &str) -> UserAction {
- UserAction {
- action_type: match record.action_type {
- UserActionType::InsertChar => ActionType::InsertChar,
- UserActionType::InsertSelection => ActionType::InsertSelection,
- UserActionType::DeleteChar => ActionType::DeleteChar,
- UserActionType::DeleteSelection => ActionType::DeleteSelection,
- UserActionType::CursorMovement => ActionType::CursorMovement,
- },
- line_number: record.line_number as usize,
- offset: record.offset,
- file_path: file_path.to_string(),
- timestamp: record.timestamp_epoch_ms,
- }
-}
-
-#[derive(Debug, Clone, Deserialize)]
-struct AutocompleteResponse {
- pub autocomplete_id: String,
- pub start_index: usize,
- pub end_index: usize,
- pub completion: String,
- #[allow(dead_code)]
- pub confidence: f64,
- #[allow(dead_code)]
- pub logprobs: Option,
- #[allow(dead_code)]
- pub finish_reason: Option,
- #[allow(dead_code)]
- pub elapsed_time_ms: u64,
- #[allow(dead_code)]
- #[serde(default, rename = "completions")]
- pub additional_completions: Vec,
-}
-
-#[allow(dead_code)]
-#[derive(Debug, Clone, Deserialize)]
-struct AdditionalCompletion {
- pub start_index: usize,
- pub end_index: usize,
- pub completion: String,
- pub confidence: f64,
- pub autocomplete_id: String,
- pub logprobs: Option,
- pub finish_reason: Option,
-}
-
-fn write_event(event: &zeta_prompt::Event, f: &mut impl fmt::Write) -> fmt::Result {
- match event {
- zeta_prompt::Event::BufferChange {
- old_path,
- path,
- diff,
- ..
- } => {
- if old_path != path {
- // TODO confirm how to do this for sweep
- // writeln!(f, "User renamed {:?} to {:?}\n", old_path, new_path)?;
- }
-
- if !diff.is_empty() {
- write!(f, "File: {}:\n{}\n", path.display(), diff)?
- }
-
- fmt::Result::Ok(())
- }
- }
-}
-
-fn debug_info(cx: &gpui::App) -> Arc {
- format!(
- "Zed v{version} ({sha}) - OS: {os} - Zed v{version}",
- version = release_channel::AppVersion::global(cx),
- sha = release_channel::AppCommitSha::try_global(cx)
- .map_or("unknown".to_string(), |sha| sha.full()),
- os = client::telemetry::os_name(),
- )
- .into()
-}
-
-#[derive(Debug, Clone, Copy, Serialize)]
-#[serde(rename_all = "snake_case")]
-pub enum SweepEventType {
- AutocompleteSuggestionShown,
- AutocompleteSuggestionAccepted,
-}
-
-#[derive(Debug, Clone, Copy, Serialize)]
-#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
-pub enum SweepSuggestionType {
- GhostText,
- Popup,
- JumpToEdit,
-}
-
-#[derive(Debug, Clone, Serialize)]
-struct AutocompleteMetricsRequest {
- event_type: SweepEventType,
- suggestion_type: SweepSuggestionType,
- additions: u32,
- deletions: u32,
- autocomplete_id: String,
- edit_tracking: String,
- edit_tracking_line: Option,
- lifespan: Option,
- debug_info: Arc,
- device_id: String,
- privacy_mode_enabled: bool,
-}
-
-fn send_autocomplete_metrics_request(
- cx: &App,
- client: Arc,
- api_token: Arc,
- request_body: AutocompleteMetricsRequest,
-) {
- let http_client = client.http_client();
- cx.background_spawn(async move {
- let body: AsyncBody = serde_json::to_string(&request_body)?.into();
-
- let request = http_client::Request::builder()
- .uri(SWEEP_METRICS_URL)
- .header("Content-Type", "application/json")
- .header("Authorization", format!("Bearer {}", api_token))
- .method(Method::POST)
- .body(body)?;
-
- let mut response = http_client.send(request).await?;
-
- if !response.status().is_success() {
- let mut body = String::new();
- response.body_mut().read_to_string(&mut body).await?;
- anyhow::bail!(
- "Failed to send autocomplete metrics for sweep_ai: {:?}\nBody: {}",
- response.status(),
- body,
- );
- }
-
- Ok(())
- })
- .detach_and_log_err(cx);
-}
-
-pub(crate) fn edit_prediction_accepted(
- store: &EditPredictionStore,
- current_prediction: CurrentEditPrediction,
- cx: &App,
-) {
- let Some(api_token) = store
- .sweep_ai
- .api_token
- .read(cx)
- .key(&SWEEP_CREDENTIALS_URL)
- else {
- return;
- };
- let debug_info = store.sweep_ai.debug_info.clone();
-
- let prediction = current_prediction.prediction;
-
- let (additions, deletions) = compute_edit_metrics(&prediction.edits, &prediction.snapshot);
- let autocomplete_id = prediction.id.to_string();
-
- let device_id = store
- .client
- .user_id()
- .as_ref()
- .map(ToString::to_string)
- .unwrap_or_default();
-
- let suggestion_type = match current_prediction.shown_with {
- Some(SuggestionDisplayType::DiffPopover) => SweepSuggestionType::Popup,
- Some(SuggestionDisplayType::Jump) => return, // should'nt happen
- Some(SuggestionDisplayType::GhostText) | None => SweepSuggestionType::GhostText,
- };
-
- let request_body = AutocompleteMetricsRequest {
- event_type: SweepEventType::AutocompleteSuggestionAccepted,
- suggestion_type,
- additions,
- deletions,
- autocomplete_id,
- edit_tracking: String::new(),
- edit_tracking_line: None,
- lifespan: None,
- debug_info,
- device_id,
- privacy_mode_enabled: false,
- };
-
- send_autocomplete_metrics_request(cx, store.client.clone(), api_token, request_body);
-}
-
-pub fn edit_prediction_shown(
- sweep_ai: &SweepAi,
- client: Arc,
- prediction: &EditPrediction,
- display_type: SuggestionDisplayType,
- cx: &App,
-) {
- let Some(api_token) = sweep_ai.api_token.read(cx).key(&SWEEP_CREDENTIALS_URL) else {
- return;
- };
- let debug_info = sweep_ai.debug_info.clone();
-
- let (additions, deletions) = compute_edit_metrics(&prediction.edits, &prediction.snapshot);
- let autocomplete_id = prediction.id.to_string();
-
- let suggestion_type = match display_type {
- SuggestionDisplayType::GhostText => SweepSuggestionType::GhostText,
- SuggestionDisplayType::DiffPopover => SweepSuggestionType::Popup,
- SuggestionDisplayType::Jump => SweepSuggestionType::JumpToEdit,
- };
-
- let request_body = AutocompleteMetricsRequest {
- event_type: SweepEventType::AutocompleteSuggestionShown,
- suggestion_type,
- additions,
- deletions,
- autocomplete_id,
- edit_tracking: String::new(),
- edit_tracking_line: None,
- lifespan: None,
- debug_info,
- device_id: String::new(),
- privacy_mode_enabled: false,
- };
-
- send_autocomplete_metrics_request(cx, client, api_token, request_body);
-}
-
-fn compute_edit_metrics(
- edits: &[(Range, Arc)],
- snapshot: &BufferSnapshot,
-) -> (u32, u32) {
- let mut additions = 0u32;
- let mut deletions = 0u32;
-
- for (range, new_text) in edits {
- let old_text = snapshot.text_for_range(range.clone());
- deletions += old_text
- .map(|chunk| chunk.lines().count())
- .sum::()
- .max(1) as u32;
- additions += new_text.lines().count().max(1) as u32;
- }
-
- (additions, deletions)
-}
diff --git a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs
index b5ae954e84ca84505a47761235be71655477a9f7..c5e97fd87eaad9b98aeb9b946a9a69b1c1071db2 100644
--- a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs
+++ b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs
@@ -10,7 +10,7 @@ use gpui::{App, Entity, prelude::*};
use language::{Buffer, ToPoint as _};
use project::Project;
-use crate::{BufferEditPrediction, EditPredictionModel, EditPredictionStore};
+use crate::{BufferEditPrediction, EditPredictionStore};
pub struct ZedEditPredictionDelegate {
store: Entity,
@@ -103,14 +103,9 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
&self,
_buffer: &Entity,
_cursor_position: language::Anchor,
- cx: &App,
+ _cx: &App,
) -> bool {
- let store = self.store.read(cx);
- if store.edit_prediction_model == EditPredictionModel::Sweep {
- store.has_sweep_api_token(cx)
- } else {
- true
- }
+ true
}
fn is_refreshing(&self, cx: &App) -> bool {
diff --git a/crates/edit_prediction_cli/src/example.rs b/crates/edit_prediction_cli/src/example.rs
index 196f4f96d99b64aed2ff3ae2d7a9897295a60b29..4827337d37a211056d04cf9ca13f8d49fb91c392 100644
--- a/crates/edit_prediction_cli/src/example.rs
+++ b/crates/edit_prediction_cli/src/example.rs
@@ -1,4 +1,5 @@
use crate::PredictionProvider;
+use crate::metrics::ClassificationMetrics;
use crate::paths::WORKTREES_DIR;
use crate::qa::QaResult;
use anyhow::{Context as _, Result};
@@ -150,6 +151,18 @@ where
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExampleScore {
pub delta_chr_f: f32,
+ #[serde(default)]
+ pub delta_chr_f_true_positives: usize,
+ #[serde(default)]
+ pub delta_chr_f_false_positives: usize,
+ #[serde(default)]
+ pub delta_chr_f_false_negatives: usize,
+ #[serde(default)]
+ pub delta_chr_f_precision: f64,
+ #[serde(default)]
+ pub delta_chr_f_recall: f64,
+ #[serde(default)]
+ pub delta_chr_f_beta: f64,
pub braces_disbalance: usize,
#[serde(default)]
pub exact_lines_tp: usize,
@@ -176,6 +189,24 @@ pub struct ExampleScore {
pub avg_logprob: Option,
}
+impl ExampleScore {
+ pub fn delta_chr_f_counts(&self) -> ClassificationMetrics {
+ ClassificationMetrics {
+ true_positives: self.delta_chr_f_true_positives,
+ false_positives: self.delta_chr_f_false_positives,
+ false_negatives: self.delta_chr_f_false_negatives,
+ }
+ }
+
+ pub fn exact_lines_counts(&self) -> ClassificationMetrics {
+ ClassificationMetrics {
+ true_positives: self.exact_lines_tp,
+ false_positives: self.exact_lines_fp,
+ false_negatives: self.exact_lines_fn,
+ }
+ }
+}
+
impl Example {
pub fn repo_name(&self) -> Result> {
// git@github.com:owner/repo.git
diff --git a/crates/edit_prediction_cli/src/filter_languages.rs b/crates/edit_prediction_cli/src/filter_languages.rs
index 355b5708d43c35c74bf62608726309389a1bfe32..989a112a50aa2dd2d922df6895be275a58ff6336 100644
--- a/crates/edit_prediction_cli/src/filter_languages.rs
+++ b/crates/edit_prediction_cli/src/filter_languages.rs
@@ -13,7 +13,7 @@
//!
//! Language is detected based on file extension of the `cursor_path` field.
//! The extension-to-language mapping is built from the embedded language
-//! config files in the `languages` crate.
+//! config files in the `grammars` crate.
use anyhow::{Context as _, Result, bail};
use clap::Args;
@@ -29,7 +29,7 @@ mod language_configs_embedded {
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
- #[folder = "../languages/src/"]
+ #[folder = "../grammars/src/"]
#[include = "*/config.toml"]
pub struct LanguageConfigs;
}
@@ -123,7 +123,7 @@ fn build_extension_to_language_map() -> HashMap {
#[cfg(feature = "dynamic_prompts")]
fn build_extension_to_language_map() -> HashMap {
- const LANGUAGES_SRC_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../languages/src");
+ const LANGUAGES_SRC_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../grammars/src");
let mut map = HashMap::default();
diff --git a/crates/edit_prediction_cli/src/main.rs b/crates/edit_prediction_cli/src/main.rs
index 06fdbadbf53ce0f9f84b909081691c0097c4c5a4..cf9232a04a40df507c187d53becfedcd8db03188 100644
--- a/crates/edit_prediction_cli/src/main.rs
+++ b/crates/edit_prediction_cli/src/main.rs
@@ -358,7 +358,6 @@ impl TeacherBackend {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum PredictionProvider {
- Sweep,
Mercury,
Zeta1,
Zeta2(ZetaFormat),
@@ -379,7 +378,6 @@ impl Default for PredictionProvider {
impl std::fmt::Display for PredictionProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
- PredictionProvider::Sweep => write!(f, "sweep"),
PredictionProvider::Mercury => write!(f, "mercury"),
PredictionProvider::Zeta1 => write!(f, "zeta1"),
PredictionProvider::Zeta2(format) => write!(f, "zeta2:{format}"),
@@ -407,7 +405,6 @@ impl std::str::FromStr for PredictionProvider {
let provider_lower = provider.to_lowercase();
match provider_lower.as_str() {
- "sweep" => Ok(PredictionProvider::Sweep),
"mercury" => Ok(PredictionProvider::Mercury),
"zeta1" => Ok(PredictionProvider::Zeta1),
"zeta2" => {
@@ -452,7 +449,7 @@ impl std::str::FromStr for PredictionProvider {
}
_ => {
anyhow::bail!(
- "unknown provider `{provider}`. Valid options: sweep, mercury, zeta1, zeta2, zeta2:, teacher, teacher:, teacher-multi-region, teacher-multi-region:, teacher-non-batching, teacher-multi-region-non-batching, repair\n\
+ "unknown provider `{provider}`. Valid options: mercury, zeta1, zeta2, zeta2:, teacher, teacher:, teacher-multi-region, teacher-multi-region:, teacher-non-batching, teacher-multi-region-non-batching, repair\n\
For zeta2, you can optionally specify a version like `zeta2:ordered` or `zeta2:V0113_Ordered`.\n\
For teacher providers, you can specify a backend like `teacher:sonnet46`, `teacher-multi-region:sonnet46`, `teacher-multi-region-non-batching:sonnet46`, or `teacher:gpt52`.\n\
Available zeta versions:\n{}",
diff --git a/crates/edit_prediction_cli/src/metrics.rs b/crates/edit_prediction_cli/src/metrics.rs
index 1bfd8e542fa3d74b55f091d2ac13aa22883f6a2f..8037699f4bb6f851fdadb05b435b090b911b010a 100644
--- a/crates/edit_prediction_cli/src/metrics.rs
+++ b/crates/edit_prediction_cli/src/metrics.rs
@@ -48,6 +48,12 @@ impl ClassificationMetrics {
}
}
+ pub fn accumulate(&mut self, other: &ClassificationMetrics) {
+ self.true_positives += other.true_positives;
+ self.false_positives += other.false_positives;
+ self.false_negatives += other.false_negatives;
+ }
+
pub fn precision(&self) -> f64 {
if self.true_positives + self.false_positives == 0 {
0.0
@@ -89,10 +95,23 @@ enum ChrfWhitespace {
}
const CHR_F_CHAR_ORDER: usize = 6;
-const CHR_F_BETA: f64 = 2.0;
+const CHR_F_BETA: f64 = 0.5;
const CHR_F_WHITESPACE: ChrfWhitespace = ChrfWhitespace::Collapse;
-/// Computes a delta-chrF score that compares two sets of edits.
+pub fn delta_chr_f_beta() -> f64 {
+ CHR_F_BETA
+}
+
+#[derive(Default, Debug, Clone)]
+pub struct DeltaChrFMetrics {
+ pub score: f64,
+ pub beta: f64,
+ pub counts: ClassificationMetrics,
+ pub precision: f64,
+ pub recall: f64,
+}
+
+/// Computes delta-chrF metrics that compare two sets of edits.
///
/// This metric works by:
/// 1. Computing n-gram count differences (deltas) between original→expected and original→actual
@@ -100,13 +119,17 @@ const CHR_F_WHITESPACE: ChrfWhitespace = ChrfWhitespace::Collapse;
///
/// Returns a score from 0.0 to 100.0, where 100.0 means the actual edits perfectly match
/// the expected edits.
-pub fn delta_chr_f(original: &str, expected: &str, actual: &str) -> f64 {
- // Edge case: if all texts are identical, the edits match perfectly
+pub fn delta_chr_f(original: &str, expected: &str, actual: &str) -> DeltaChrFMetrics {
if original == expected && expected == actual {
- return 100.0;
+ return DeltaChrFMetrics {
+ score: 100.0,
+ beta: CHR_F_BETA,
+ precision: 1.0,
+ recall: 1.0,
+ ..DeltaChrFMetrics::default()
+ };
}
- // Pre-filter whitespace once for all texts
let orig_chars: Vec = filter_whitespace_chars(original);
let exp_chars: Vec = filter_whitespace_chars(expected);
let act_chars: Vec = filter_whitespace_chars(actual);
@@ -118,9 +141,9 @@ pub fn delta_chr_f(original: &str, expected: &str, actual: &str) -> f64 {
let mut total_precision = 0.0;
let mut total_recall = 0.0;
+ let mut total_counts = ClassificationMetrics::default();
for order in 1..=CHR_F_CHAR_ORDER {
- // Compute n-grams only on the affected regions
let orig_ngrams_for_exp = count_ngrams_from_chars(&orig_for_exp, order);
let exp_ngrams = count_ngrams_from_chars(&exp_region, order);
let expected_delta = compute_ngram_delta(&exp_ngrams, &orig_ngrams_for_exp);
@@ -138,28 +161,43 @@ pub fn delta_chr_f(original: &str, expected: &str, actual: &str) -> f64 {
let expected_counts = ngram_delta_to_counts(&expected_delta);
let actual_counts = ngram_delta_to_counts(&actual_delta);
- let score = ClassificationMetrics::from_counts(&expected_counts, &actual_counts);
- total_precision += score.precision();
- total_recall += score.recall();
+ let counts = ClassificationMetrics::from_counts(&expected_counts, &actual_counts);
+ total_precision += counts.precision();
+ total_recall += counts.recall();
+ total_counts.accumulate(&counts);
}
- let prec = total_precision / CHR_F_CHAR_ORDER as f64;
- let recall = total_recall / CHR_F_CHAR_ORDER as f64;
- let f_score = if prec + recall == 0.0 {
+ let average_precision = total_precision / CHR_F_CHAR_ORDER as f64;
+ let average_recall = total_recall / CHR_F_CHAR_ORDER as f64;
+ let score = if average_precision + average_recall == 0.0 {
0.0
} else {
- (1.0 + CHR_F_BETA * CHR_F_BETA) * prec * recall / (CHR_F_BETA * CHR_F_BETA * prec + recall)
+ (1.0 + CHR_F_BETA * CHR_F_BETA) * average_precision * average_recall
+ / (CHR_F_BETA * CHR_F_BETA * average_precision + average_recall)
+ * 100.0
};
- f_score * 100.0
+ DeltaChrFMetrics {
+ score,
+ beta: CHR_F_BETA,
+ counts: total_counts,
+ precision: average_precision,
+ recall: average_recall,
+ }
}
-/// Reference implementation of delta_chr_f (original, non-optimized version).
+/// Reference implementation of delta-chrF metrics (original, non-optimized version).
/// Used for testing that the optimized version produces identical results.
#[cfg(test)]
-fn delta_chr_f_reference(original: &str, expected: &str, actual: &str) -> f64 {
+fn delta_chr_f_reference(original: &str, expected: &str, actual: &str) -> DeltaChrFMetrics {
if original == expected && expected == actual {
- return 100.0;
+ return DeltaChrFMetrics {
+ score: 100.0,
+ beta: CHR_F_BETA,
+ precision: 1.0,
+ recall: 1.0,
+ ..DeltaChrFMetrics::default()
+ };
}
let original_ngrams = chr_f_ngram_counts(original);
@@ -168,6 +206,7 @@ fn delta_chr_f_reference(original: &str, expected: &str, actual: &str) -> f64 {
let mut total_precision = 0.0;
let mut total_recall = 0.0;
+ let mut total_counts = ClassificationMetrics::default();
for order in 0..CHR_F_CHAR_ORDER {
let expected_delta = compute_ngram_delta(&expected_ngrams[order], &original_ngrams[order]);
@@ -182,20 +221,29 @@ fn delta_chr_f_reference(original: &str, expected: &str, actual: &str) -> f64 {
let expected_counts = ngram_delta_to_counts(&expected_delta);
let actual_counts = ngram_delta_to_counts(&actual_delta);
- let score = ClassificationMetrics::from_counts(&expected_counts, &actual_counts);
- total_precision += score.precision();
- total_recall += score.recall();
+ let counts = ClassificationMetrics::from_counts(&expected_counts, &actual_counts);
+ total_precision += counts.precision();
+ total_recall += counts.recall();
+ total_counts.accumulate(&counts);
}
- let prec = total_precision / CHR_F_CHAR_ORDER as f64;
- let recall = total_recall / CHR_F_CHAR_ORDER as f64;
- let f_score = if prec + recall == 0.0 {
+ let average_precision = total_precision / CHR_F_CHAR_ORDER as f64;
+ let average_recall = total_recall / CHR_F_CHAR_ORDER as f64;
+ let score = if average_precision + average_recall == 0.0 {
0.0
} else {
- (1.0 + CHR_F_BETA * CHR_F_BETA) * prec * recall / (CHR_F_BETA * CHR_F_BETA * prec + recall)
+ (1.0 + CHR_F_BETA * CHR_F_BETA) * average_precision * average_recall
+ / (CHR_F_BETA * CHR_F_BETA * average_precision + average_recall)
+ * 100.0
};
- f_score * 100.0
+ DeltaChrFMetrics {
+ score,
+ beta: CHR_F_BETA,
+ counts: total_counts,
+ precision: average_precision,
+ recall: average_recall,
+ }
}
/// Filter whitespace from a string and return as Vec
@@ -664,7 +712,7 @@ mod test_optimization {
];
for (original, expected, actual) in test_cases {
- let score = delta_chr_f(original, expected, actual);
+ let score = delta_chr_f(original, expected, actual).score;
// Just verify it produces a reasonable score (0-100)
assert!(
score >= 0.0 && score <= 100.0,
@@ -733,20 +781,51 @@ mod test_optimization {
];
for (original, expected, actual) in test_cases {
- let optimized_score = delta_chr_f(original, expected, actual);
- let reference_score = delta_chr_f_reference(original, expected, actual);
+ let optimized_metrics = delta_chr_f(original, expected, actual);
+ let reference_metrics = delta_chr_f_reference(original, expected, actual);
assert!(
- (optimized_score - reference_score).abs() < 1e-10,
- "Mismatch for ({:?}, {:?}, {:?}):\n optimized: {}\n reference: {}",
+ (optimized_metrics.score - reference_metrics.score).abs() < 1e-10,
+ "Score mismatch for ({:?}, {:?}, {:?}):\n optimized: {}\n reference: {}",
original,
expected,
actual,
- optimized_score,
- reference_score
+ optimized_metrics.score,
+ reference_metrics.score
+ );
+ assert_eq!(
+ optimized_metrics.counts.true_positives,
+ reference_metrics.counts.true_positives
+ );
+ assert_eq!(
+ optimized_metrics.counts.false_positives,
+ reference_metrics.counts.false_positives
);
+ assert_eq!(
+ optimized_metrics.counts.false_negatives,
+ reference_metrics.counts.false_negatives
+ );
+ assert!((optimized_metrics.precision - reference_metrics.precision).abs() < 1e-10);
+ assert!((optimized_metrics.recall - reference_metrics.recall).abs() < 1e-10);
}
}
+
+ #[test]
+ fn test_delta_chr_f_metrics_include_counts_and_rates() {
+ let original = "one two three";
+ let expected = "one three";
+ let actual = "one two four";
+
+ let metrics = delta_chr_f(original, expected, actual);
+
+ assert!(metrics.score > 20.0 && metrics.score < 40.0);
+ assert!(metrics.counts.true_positives > 0);
+ assert!(metrics.counts.false_positives > 0);
+ assert!(metrics.counts.false_negatives > 0);
+ assert!(metrics.precision > 0.0 && metrics.precision < 1.0);
+ assert!(metrics.recall > 0.0 && metrics.recall < 1.0);
+ assert_eq!(metrics.beta, CHR_F_BETA);
+ }
}
#[cfg(test)]
@@ -770,7 +849,7 @@ mod test {
let original = "fn main() { println!(\"Hello\");}";
let expected = "fn main() { println!(\"Hello, World!\");}";
- let score = delta_chr_f(original, expected, expected);
+ let score = delta_chr_f(original, expected, expected).score;
assert!((score - 100.0).abs() < 1e-2);
}
@@ -782,7 +861,7 @@ mod test {
let actual = "one two four"; // deleted "three", added "four"
// Then the score should be low
- let score = delta_chr_f(original, expected, actual);
+ let score = delta_chr_f(original, expected, actual).score;
assert!(score > 20.0 && score < 40.0);
}
@@ -794,7 +873,7 @@ mod test {
// We got the edit location right, but the replacement text is wrong.
// Deleted ngrams will match, bringing the score somewhere in the middle.
- let score = delta_chr_f(original, expected, actual);
+ let score = delta_chr_f(original, expected, actual).score;
assert!(score > 40.0 && score < 60.0);
}
@@ -806,7 +885,7 @@ mod test {
let actual = "prefix old suffix"; // no change
// Then the score should be low (all expected changes are false negatives)
- let score = delta_chr_f(original, expected, actual);
+ let score = delta_chr_f(original, expected, actual).score;
assert!(score < 20.0);
}
@@ -818,14 +897,14 @@ mod test {
let actual = "helloextraworld"; // added "extra"
// Then the score should be low (all actual changes are false positives)
- let score = delta_chr_f(original, expected, actual);
+ let score = delta_chr_f(original, expected, actual).score;
assert!(score < 20.0);
}
#[test]
fn test_delta_chr_f_no_changes() {
let text = "unchanged text";
- let score = delta_chr_f(text, text, text);
+ let score = delta_chr_f(text, text, text).score;
assert!((score - 100.0).abs() < 1e-2);
}
diff --git a/crates/edit_prediction_cli/src/predict.rs b/crates/edit_prediction_cli/src/predict.rs
index 1effca9d21a297d28ebf1eab738beead9f1af837..f2a55455b36326b58daa0adada7ec39124ffc317 100644
--- a/crates/edit_prediction_cli/src/predict.rs
+++ b/crates/edit_prediction_cli/src/predict.rs
@@ -137,7 +137,6 @@ pub async fn run_prediction(
let model = match provider {
PredictionProvider::Zeta1 => edit_prediction::EditPredictionModel::Zeta,
PredictionProvider::Zeta2(_) => edit_prediction::EditPredictionModel::Zeta,
- PredictionProvider::Sweep => edit_prediction::EditPredictionModel::Sweep,
PredictionProvider::Mercury => edit_prediction::EditPredictionModel::Mercury,
PredictionProvider::Teacher(..)
| PredictionProvider::TeacherMultiRegion(..)
diff --git a/crates/edit_prediction_cli/src/score.rs b/crates/edit_prediction_cli/src/score.rs
index d75cf55e85b198bc28469e83d8f9209a8a59a83f..be9b185809e6e0cd49e0befbeecec0f317339342 100644
--- a/crates/edit_prediction_cli/src/score.rs
+++ b/crates/edit_prediction_cli/src/score.rs
@@ -67,6 +67,12 @@ pub async fn run_scoring(
let zero_scores = ExampleScore {
delta_chr_f: 0.0,
+ delta_chr_f_true_positives: 0,
+ delta_chr_f_false_positives: 0,
+ delta_chr_f_false_negatives: 0,
+ delta_chr_f_precision: 0.0,
+ delta_chr_f_recall: 0.0,
+ delta_chr_f_beta: metrics::delta_chr_f_beta(),
braces_disbalance: 0,
exact_lines_tp: 0,
exact_lines_fp: 0,
@@ -111,14 +117,14 @@ pub async fn run_scoring(
}
};
- let mut best_delta_chr_f = 0.0f32;
+ let mut best_delta_chr_f_metrics = metrics::DeltaChrFMetrics::default();
let mut best_expected_cursor: Option = None;
let mut best_patch_idx: Option = None;
for (idx, expected) in expected_texts.iter().enumerate() {
- let delta_chr_f = metrics::delta_chr_f(original_text, expected, &actual_text) as f32;
- if delta_chr_f > best_delta_chr_f {
- best_delta_chr_f = delta_chr_f;
+ let delta_chr_f_metrics = metrics::delta_chr_f(original_text, expected, &actual_text);
+ if delta_chr_f_metrics.score > best_delta_chr_f_metrics.score {
+ best_delta_chr_f_metrics = delta_chr_f_metrics;
best_patch_idx = Some(idx);
}
}
@@ -179,7 +185,13 @@ pub async fn run_scoring(
);
scores.push(ExampleScore {
- delta_chr_f: best_delta_chr_f,
+ delta_chr_f: best_delta_chr_f_metrics.score as f32,
+ delta_chr_f_true_positives: best_delta_chr_f_metrics.counts.true_positives,
+ delta_chr_f_false_positives: best_delta_chr_f_metrics.counts.false_positives,
+ delta_chr_f_false_negatives: best_delta_chr_f_metrics.counts.false_negatives,
+ delta_chr_f_precision: best_delta_chr_f_metrics.precision,
+ delta_chr_f_recall: best_delta_chr_f_metrics.recall,
+ delta_chr_f_beta: best_delta_chr_f_metrics.beta,
braces_disbalance,
exact_lines_tp: best_exact_lines.true_positives,
exact_lines_fp: best_exact_lines.false_positives,
@@ -238,6 +250,10 @@ pub fn print_report(examples: &[Example], verbose: bool) {
let mut all_delta_chr_f_scores = Vec::new();
let mut all_reversal_ratios = Vec::new();
let mut braces_disbalance_sum: usize = 0;
+ let mut total_delta_chr_f = ClassificationMetrics::default();
+ let mut total_delta_chr_f_precision = 0.0;
+ let mut total_delta_chr_f_recall = 0.0;
+ let mut delta_chr_f_beta = 0.0;
let mut total_exact_lines = ClassificationMetrics::default();
let mut total_scores: usize = 0;
let mut qa_reverts_count: usize = 0;
@@ -260,11 +276,7 @@ pub fn print_report(examples: &[Example], verbose: bool) {
for example in examples {
for (score_idx, score) in example.score.iter().enumerate() {
- let exact_lines = ClassificationMetrics {
- true_positives: score.exact_lines_tp,
- false_positives: score.exact_lines_fp,
- false_negatives: score.exact_lines_fn,
- };
+ let exact_lines = score.exact_lines_counts();
// Get QA results for this prediction if available
let qa_result = example.qa.get(score_idx).and_then(|q| q.as_ref());
@@ -314,9 +326,11 @@ pub fn print_report(examples: &[Example], verbose: bool) {
all_reversal_ratios.push(score.reversal_ratio);
total_scores += 1;
braces_disbalance_sum += score.braces_disbalance;
- total_exact_lines.true_positives += score.exact_lines_tp;
- total_exact_lines.false_positives += score.exact_lines_fp;
- total_exact_lines.false_negatives += score.exact_lines_fn;
+ total_delta_chr_f.accumulate(&score.delta_chr_f_counts());
+ total_delta_chr_f_precision += score.delta_chr_f_precision;
+ total_delta_chr_f_recall += score.delta_chr_f_recall;
+ delta_chr_f_beta = score.delta_chr_f_beta;
+ total_exact_lines.accumulate(&score.exact_lines_counts());
// Accumulate QA metrics
if let Some(qa) = qa_result {
@@ -448,6 +462,15 @@ pub fn print_report(examples: &[Example], verbose: bool) {
wrong_er_str
);
println!("{}", separator);
+ println!(
+ "Delta chrF (β={:.1}): TP={}, FP={}, FN={}, P={:.1}%, R={:.1}%",
+ delta_chr_f_beta,
+ total_delta_chr_f.true_positives,
+ total_delta_chr_f.false_positives,
+ total_delta_chr_f.false_negatives,
+ total_delta_chr_f_precision / total_scores as f64 * 100.0,
+ total_delta_chr_f_recall / total_scores as f64 * 100.0
+ );
// Print additional cursor metrics if available
if let Some(avg_dist) = avg_cursor_distance {
@@ -540,6 +563,12 @@ fn truncate_name(name: &str, max_len: usize) -> String {
pub struct SummaryJson {
pub total_examples: usize,
pub avg_delta_chr_f: f32,
+ pub delta_chr_f_beta: f64,
+ pub delta_chr_f_true_positives: usize,
+ pub delta_chr_f_false_positives: usize,
+ pub delta_chr_f_false_negatives: usize,
+ pub delta_chr_f_precision: f64,
+ pub delta_chr_f_recall: f64,
pub avg_braces_disbalance: f32,
pub exact_lines_true_positives: usize,
pub exact_lines_false_positives: usize,
@@ -569,6 +598,10 @@ pub fn compute_summary(examples: &[Example]) -> SummaryJson {
let mut all_delta_chr_f_scores = Vec::new();
let mut all_reversal_ratios = Vec::new();
let mut braces_disbalance_sum: usize = 0;
+ let mut total_delta_chr_f = ClassificationMetrics::default();
+ let mut total_delta_chr_f_precision = 0.0;
+ let mut total_delta_chr_f_recall = 0.0;
+ let mut delta_chr_f_beta = 0.0;
let mut total_exact_lines = ClassificationMetrics::default();
let mut total_scores: usize = 0;
let mut qa_reverts_count: usize = 0;
@@ -589,9 +622,11 @@ pub fn compute_summary(examples: &[Example]) -> SummaryJson {
all_reversal_ratios.push(score.reversal_ratio);
total_scores += 1;
braces_disbalance_sum += score.braces_disbalance;
- total_exact_lines.true_positives += score.exact_lines_tp;
- total_exact_lines.false_positives += score.exact_lines_fp;
- total_exact_lines.false_negatives += score.exact_lines_fn;
+ total_delta_chr_f.accumulate(&score.delta_chr_f_counts());
+ total_delta_chr_f_precision += score.delta_chr_f_precision;
+ total_delta_chr_f_recall += score.delta_chr_f_recall;
+ delta_chr_f_beta = score.delta_chr_f_beta;
+ total_exact_lines.accumulate(&score.exact_lines_counts());
// Accumulate QA metrics
if let Some(Some(qa)) = example.qa.get(score_idx) {
@@ -697,6 +732,20 @@ pub fn compute_summary(examples: &[Example]) -> SummaryJson {
SummaryJson {
total_examples: total_scores,
avg_delta_chr_f,
+ delta_chr_f_beta,
+ delta_chr_f_true_positives: total_delta_chr_f.true_positives,
+ delta_chr_f_false_positives: total_delta_chr_f.false_positives,
+ delta_chr_f_false_negatives: total_delta_chr_f.false_negatives,
+ delta_chr_f_precision: if total_scores == 0 {
+ 0.0
+ } else {
+ total_delta_chr_f_precision / total_scores as f64
+ },
+ delta_chr_f_recall: if total_scores == 0 {
+ 0.0
+ } else {
+ total_delta_chr_f_recall / total_scores as f64
+ },
avg_braces_disbalance,
exact_lines_true_positives: total_exact_lines.true_positives,
exact_lines_false_positives: total_exact_lines.false_positives,
diff --git a/crates/edit_prediction_context/src/edit_prediction_context_tests.rs b/crates/edit_prediction_context/src/edit_prediction_context_tests.rs
index 78ded78b7eb558c9bb5d1839a8c8c82290a13d9a..32dc37b953207e4d034287642f0e91fa400867dd 100644
--- a/crates/edit_prediction_context/src/edit_prediction_context_tests.rs
+++ b/crates/edit_prediction_context/src/edit_prediction_context_tests.rs
@@ -160,7 +160,7 @@ async fn test_edit_prediction_context(cx: &mut TestAppContext) {
}
#[gpui::test]
-fn test_assemble_excerpts(cx: &mut TestAppContext) {
+async fn test_assemble_excerpts(cx: &mut TestAppContext) {
let table = [
(
indoc! {r#"
@@ -289,6 +289,9 @@ fn test_assemble_excerpts(cx: &mut TestAppContext) {
for (input, expected_output) in table {
let (input, ranges) = marked_text_ranges(&input, false);
let buffer = cx.new(|cx| Buffer::local(input, cx).with_language(rust_lang(), cx));
+ buffer
+ .read_with(cx, |buffer, _| buffer.parsing_idle())
+ .await;
buffer.read_with(cx, |buffer, _cx| {
let ranges: Vec<(Range, usize)> = ranges
.into_iter()
diff --git a/crates/edit_prediction_ui/src/edit_prediction_button.rs b/crates/edit_prediction_ui/src/edit_prediction_button.rs
index d85ccded26058331c787f89ad74721d9572db623..e6e65012123c0fdf3571115bded43f8840f997ee 100644
--- a/crates/edit_prediction_ui/src/edit_prediction_button.rs
+++ b/crates/edit_prediction_ui/src/edit_prediction_button.rs
@@ -325,7 +325,6 @@ impl Render for EditPredictionButton {
}
provider @ (EditPredictionProvider::Experimental(_)
| EditPredictionProvider::Zed
- | EditPredictionProvider::Sweep
| EditPredictionProvider::Mercury) => {
let enabled = self.editor_enabled.unwrap_or(true);
let file = self.file.clone();
@@ -349,16 +348,6 @@ impl Render for EditPredictionButton {
let mut missing_token = false;
match provider {
- EditPredictionProvider::Sweep => {
- missing_token = edit_prediction::EditPredictionStore::try_global(cx)
- .is_some_and(|ep_store| !ep_store.read(cx).has_sweep_api_token(cx));
- ep_icon = if enabled { icons.base } else { icons.disabled };
- tooltip_meta = if missing_token {
- "Missing API key for Sweep"
- } else {
- "Powered by Sweep"
- };
- }
EditPredictionProvider::Mercury => {
ep_icon = if enabled { icons.base } else { icons.disabled };
let mercury_has_error =
@@ -548,17 +537,12 @@ impl EditPredictionButton {
.detach();
edit_prediction::ollama::ensure_authenticated(cx);
- let sweep_api_token_task = edit_prediction::sweep_ai::load_sweep_api_token(cx);
let mercury_api_token_task = edit_prediction::mercury::load_mercury_api_token(cx);
let open_ai_compatible_api_token_task =
edit_prediction::open_ai_compatible::load_open_ai_compatible_api_token(cx);
cx.spawn(async move |this, cx| {
- _ = futures::join!(
- sweep_api_token_task,
- mercury_api_token_task,
- open_ai_compatible_api_token_task
- );
+ _ = futures::join!(mercury_api_token_task, open_ai_compatible_api_token_task);
this.update(cx, |_, cx| {
cx.notify();
})
@@ -1457,13 +1441,6 @@ pub fn get_available_providers(cx: &mut App) -> Vec {
providers.push(EditPredictionProvider::OpenAiCompatibleApi);
}
- if edit_prediction::sweep_ai::sweep_api_token(cx)
- .read(cx)
- .has_key()
- {
- providers.push(EditPredictionProvider::Sweep);
- }
-
if edit_prediction::mercury::mercury_api_token(cx)
.read(cx)
.has_key()
diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs
index e22cafb00bd446b94d3b8eda4d7e3afd20c449ae..b9fa0a49b1b77f9e5fcf4ace7d83155628afba20 100644
--- a/crates/editor/src/display_map.rs
+++ b/crates/editor/src/display_map.rs
@@ -101,6 +101,7 @@ use language::{
Point, Subscription as BufferSubscription,
language_settings::{AllLanguageSettings, LanguageSettings},
};
+
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16,
MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
@@ -1905,7 +1906,7 @@ impl DisplaySnapshot {
.flat_map(|chunk| {
let syntax_highlight_style = chunk
.syntax_highlight_id
- .and_then(|id| id.style(&editor_style.syntax));
+ .and_then(|id| editor_style.syntax.get(id).cloned());
let chunk_highlight = chunk.highlight_style.map(|chunk_highlight| {
HighlightStyle {
@@ -1999,7 +2000,8 @@ impl DisplaySnapshot {
let syntax_style = chunk
.syntax_highlight_id
- .and_then(|id| id.style(syntax_theme));
+ .and_then(|id| syntax_theme.get(id).cloned());
+
let overlay_style = chunk.highlight_style;
let combined = match (syntax_style, overlay_style) {
@@ -4015,7 +4017,8 @@ pub mod tests {
for chunk in snapshot.chunks(rows, true, HighlightStyles::default()) {
let syntax_color = chunk
.syntax_highlight_id
- .and_then(|id| id.style(theme)?.color);
+ .and_then(|id| theme.get(id)?.color);
+
let highlight_color = chunk.highlight_style.and_then(|style| style.color);
if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut()
&& syntax_color == *last_syntax_color
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index ee659f2870502a96d1e052035d974b10213f5604..1786013a4a4d746c0580813c3e9b9962b1baa72d 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -601,7 +601,11 @@ pub fn make_inlay_hints_style(cx: &App) -> HighlightStyle {
.inlay_hints
.show_background;
- let mut style = cx.theme().syntax().get("hint");
+ let mut style = cx
+ .theme()
+ .syntax()
+ .style_for_name("hint")
+ .unwrap_or_default();
if style.color.is_none() {
style.color = Some(cx.theme().status().hint);
@@ -19156,7 +19160,7 @@ impl Editor {
move |cx: &mut BlockContext| {
let mut text_style = cx.editor_style.text.clone();
if let Some(highlight_style) = old_highlight_id
- .and_then(|h| h.style(&cx.editor_style.syntax))
+ .and_then(|h| cx.editor_style.syntax.get(h).cloned())
{
text_style = text_style.highlight(highlight_style);
}
@@ -25035,7 +25039,8 @@ impl Editor {
for chunk in chunks {
let highlight = chunk
.syntax_highlight_id
- .and_then(|id| id.name(&style.syntax));
+ .and_then(|id| style.syntax.get_capture_name(id));
+
let mut chunk_lines = chunk.text.split('\n').peekable();
while let Some(text) = chunk_lines.next() {
let mut merged_with_last_token = false;
@@ -28843,49 +28848,58 @@ pub fn styled_runs_for_code_label<'a>(
..Default::default()
};
+ if label.runs.is_empty() {
+ let desc_start = label.filter_range.end;
+ let fade_run =
+ (desc_start < label.text.len()).then(|| (desc_start..label.text.len(), fade_out));
+ return Either::Left(fade_run.into_iter());
+ }
+
let mut prev_end = label.filter_range.end;
- label
- .runs
- .iter()
- .enumerate()
- .flat_map(move |(ix, (range, highlight_id))| {
- let style = if *highlight_id == language::HighlightId::TABSTOP_INSERT_ID {
- HighlightStyle {
- color: Some(local_player.cursor),
- ..Default::default()
- }
- } else if *highlight_id == language::HighlightId::TABSTOP_REPLACE_ID {
- HighlightStyle {
- background_color: Some(local_player.selection),
- ..Default::default()
- }
- } else if let Some(style) = highlight_id.style(syntax_theme) {
- style
- } else {
- return Default::default();
- };
- let muted_style = style.highlight(fade_out);
+ Either::Right(
+ label
+ .runs
+ .iter()
+ .enumerate()
+ .flat_map(move |(ix, (range, highlight_id))| {
+ let style = if *highlight_id == language::HighlightId::TABSTOP_INSERT_ID {
+ HighlightStyle {
+ color: Some(local_player.cursor),
+ ..Default::default()
+ }
+ } else if *highlight_id == language::HighlightId::TABSTOP_REPLACE_ID {
+ HighlightStyle {
+ background_color: Some(local_player.selection),
+ ..Default::default()
+ }
+ } else if let Some(style) = syntax_theme.get(*highlight_id).cloned() {
+ style
+ } else {
+ return Default::default();
+ };
- let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new();
- if range.start >= label.filter_range.end {
- if range.start > prev_end {
- runs.push((prev_end..range.start, fade_out));
+ let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new();
+ let muted_style = style.highlight(fade_out);
+ if range.start >= label.filter_range.end {
+ if range.start > prev_end {
+ runs.push((prev_end..range.start, fade_out));
+ }
+ runs.push((range.clone(), muted_style));
+ } else if range.end <= label.filter_range.end {
+ runs.push((range.clone(), style));
+ } else {
+ runs.push((range.start..label.filter_range.end, style));
+ runs.push((label.filter_range.end..range.end, muted_style));
}
- runs.push((range.clone(), muted_style));
- } else if range.end <= label.filter_range.end {
- runs.push((range.clone(), style));
- } else {
- runs.push((range.start..label.filter_range.end, style));
- runs.push((label.filter_range.end..range.end, muted_style));
- }
- prev_end = cmp::max(prev_end, range.end);
+ prev_end = cmp::max(prev_end, range.end);
- if ix + 1 == label.runs.len() && label.text.len() > prev_end {
- runs.push((prev_end..label.text.len(), fade_out));
- }
+ if ix + 1 == label.runs.len() && label.text.len() > prev_end {
+ runs.push((prev_end..label.text.len(), fade_out));
+ }
- runs
- })
+ runs
+ }),
+ )
}
pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator- + '_ {
diff --git a/crates/editor/src/semantic_tokens.rs b/crates/editor/src/semantic_tokens.rs
index 6a82068410f074c3246f2d84eab9a3576f2e8848..1a895465277d02078f1bf23da21f061a94f94be7 100644
--- a/crates/editor/src/semantic_tokens.rs
+++ b/crates/editor/src/semantic_tokens.rs
@@ -377,7 +377,10 @@ fn convert_token(
for rule in matching {
empty = false;
- let style = rule.style.iter().find_map(|style| theme.get_opt(style));
+ let style = rule
+ .style
+ .iter()
+ .find_map(|style| theme.style_for_name(style));
macro_rules! overwrite {
(
diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs
index 8f246089299f6f35bca14867c298e1f159765c6e..67f482339f501f46a4475bb9e9534437d9f9f1cf 100644
--- a/crates/editor/src/signature_help.rs
+++ b/crates/editor/src/signature_help.rs
@@ -6,6 +6,7 @@ use gpui::{
TextStyle, Window, combine_highlights,
};
use language::BufferSnapshot;
+
use markdown::{Markdown, MarkdownElement};
use multi_buffer::{Anchor, MultiBufferOffset, ToOffset};
use settings::Settings;
@@ -236,7 +237,7 @@ impl Editor {
.highlight_text(&text, 0..signature.label.len())
.into_iter()
.flat_map(|(range, highlight_id)| {
- Some((range, highlight_id.style(cx.theme().syntax())?))
+ Some((range, *cx.theme().syntax().get(highlight_id)?))
});
signature.highlights =
combine_highlights(signature.highlights.clone(), highlights)
diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs
index d0a533bfeb331c196d802df9894e726201794ce7..4d290992f318dc8fec78dad0e40d347d4826ed65 100644
--- a/crates/extension_cli/src/main.rs
+++ b/crates/extension_cli/src/main.rs
@@ -1,3 +1,4 @@
+use std::collections::BTreeSet;
use std::collections::HashMap;
use std::env;
use std::fs;
@@ -7,6 +8,7 @@ use std::sync::Arc;
use ::fs::{CopyOptions, Fs, RealFs, copy_recursive};
use anyhow::{Context as _, Result, anyhow, bail};
use clap::Parser;
+use cloud_api_types::ExtensionProvides;
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
use extension::{ExtensionManifest, ExtensionSnippets};
use language::LanguageConfig;
@@ -80,10 +82,7 @@ async fn main() -> Result<()> {
.context("failed to compile extension")?;
let extension_provides = manifest.provides();
-
- if extension_provides.is_empty() {
- bail!("extension does not provide any features");
- }
+ validate_extension_features(&extension_provides)?;
let grammars = test_grammars(&manifest, &extension_path, &mut wasm_store)?;
test_languages(&manifest, &extension_path, &grammars)?;
@@ -203,7 +202,7 @@ async fn copy_extension_resources(
},
)
.await
- .with_context(|| "failed to copy icons")?;
+ .context("failed to copy icons")?;
}
for (_, agent_entry) in &manifest.agent_servers {
@@ -297,6 +296,22 @@ async fn copy_extension_resources(
Ok(())
}
+fn validate_extension_features(provides: &BTreeSet) -> Result<()> {
+ if provides.is_empty() {
+ bail!("extension does not provide any features");
+ }
+
+ if provides.contains(&ExtensionProvides::Themes) && provides.len() != 1 {
+ bail!("extension must not provide other features along with themes");
+ }
+
+ if provides.contains(&ExtensionProvides::IconThemes) && provides.len() != 1 {
+ bail!("extension must not provide other features along with icon themes");
+ }
+
+ Ok(())
+}
+
fn test_grammars(
manifest: &ExtensionManifest,
extension_path: &Path,
diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs
index 77d05f52f7b799bfdbb4b081e3ca2368de6fd45f..00b287f7f3d724e0fcc5275ea302c44983c9a61b 100644
--- a/crates/git_ui/src/git_panel.rs
+++ b/crates/git_ui/src/git_panel.rs
@@ -5818,7 +5818,7 @@ impl Panel for GitPanel {
}
fn activation_priority(&self) -> u32 {
- 2
+ 3
}
}
diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml
index b3812bb7cb5747ff40bd6d05a39b9ee7bebbdda1..9eb2de936c2e1db1d80cc3627db5594152e7223e 100644
--- a/crates/gpui/Cargo.toml
+++ b/crates/gpui/Cargo.toml
@@ -235,6 +235,10 @@ path = "examples/window_shadow.rs"
name = "grid_layout"
path = "examples/grid_layout.rs"
+[[example]]
+name = "list_example"
+path = "examples/list_example.rs"
+
[[example]]
name = "mouse_pressure"
path = "examples/mouse_pressure.rs"
diff --git a/crates/gpui/examples/list_example.rs b/crates/gpui/examples/list_example.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7aeff7c24ec3755edf1e37f5ff1cc496c9fb597e
--- /dev/null
+++ b/crates/gpui/examples/list_example.rs
@@ -0,0 +1,170 @@
+#![cfg_attr(target_family = "wasm", no_main)]
+
+use gpui::{
+ App, Bounds, Context, ListAlignment, ListState, Render, Window, WindowBounds, WindowOptions,
+ div, list, prelude::*, px, rgb, size,
+};
+use gpui_platform::application;
+
+const ITEM_COUNT: usize = 40;
+const SCROLLBAR_WIDTH: f32 = 12.;
+
+struct BottomListDemo {
+ list_state: ListState,
+}
+
+impl BottomListDemo {
+ fn new() -> Self {
+ Self {
+ list_state: ListState::new(ITEM_COUNT, ListAlignment::Bottom, px(500.)).measure_all(),
+ }
+ }
+}
+
+impl Render for BottomListDemo {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement {
+ let max_offset = self.list_state.max_offset_for_scrollbar().y;
+ let current_offset = -self.list_state.scroll_px_offset_for_scrollbar().y;
+
+ let viewport_height = self.list_state.viewport_bounds().size.height;
+
+ let raw_fraction = if max_offset > px(0.) {
+ current_offset / max_offset
+ } else {
+ 0.
+ };
+
+ let total_height = viewport_height + max_offset;
+ let thumb_height = if total_height > px(0.) {
+ px(viewport_height.as_f32() * viewport_height.as_f32() / total_height.as_f32())
+ .max(px(30.))
+ } else {
+ px(30.)
+ };
+
+ let track_space = viewport_height - thumb_height;
+ let thumb_top = track_space * raw_fraction;
+
+ let bug_detected = raw_fraction > 1.0;
+
+ div()
+ .size_full()
+ .bg(rgb(0xFFFFFF))
+ .flex()
+ .flex_col()
+ .p_4()
+ .gap_2()
+ .child(
+ div()
+ .text_sm()
+ .flex()
+ .flex_col()
+ .gap_1()
+ .child(format!(
+ "offset: {:.0} / max: {:.0} | fraction: {:.3}",
+ current_offset.as_f32(),
+ max_offset.as_f32(),
+ raw_fraction,
+ ))
+ .child(
+ div()
+ .text_color(if bug_detected {
+ rgb(0xCC0000)
+ } else {
+ rgb(0x008800)
+ })
+ .child(if bug_detected {
+ format!(
+ "BUG: fraction is {:.3} (> 1.0) — thumb is off-track!",
+ raw_fraction
+ )
+ } else {
+ "OK: fraction <= 1.0 — thumb is within track.".to_string()
+ }),
+ ),
+ )
+ .child(
+ div()
+ .flex_1()
+ .flex()
+ .flex_row()
+ .overflow_hidden()
+ .border_1()
+ .border_color(rgb(0xCCCCCC))
+ .rounded_sm()
+ .child(
+ list(self.list_state.clone(), |index, _window, _cx| {
+ let height = px(30. + (index % 5) as f32 * 10.);
+ div()
+ .h(height)
+ .w_full()
+ .flex()
+ .items_center()
+ .px_3()
+ .border_b_1()
+ .border_color(rgb(0xEEEEEE))
+ .bg(if index % 2 == 0 {
+ rgb(0xFAFAFA)
+ } else {
+ rgb(0xFFFFFF)
+ })
+ .text_sm()
+ .child(format!("Item {index}"))
+ .into_any()
+ })
+ .flex_1(),
+ )
+ // Scrollbar track
+ .child(
+ div()
+ .w(px(SCROLLBAR_WIDTH))
+ .h_full()
+ .flex_shrink_0()
+ .bg(rgb(0xE0E0E0))
+ .relative()
+ .child(
+ // Thumb — position is unclamped to expose the bug
+ div()
+ .absolute()
+ .top(thumb_top)
+ .w_full()
+ .h(thumb_height)
+ .bg(if bug_detected {
+ rgb(0xCC0000)
+ } else {
+ rgb(0x888888)
+ })
+ .rounded_sm(),
+ ),
+ ),
+ )
+ }
+}
+
+fn run_example() {
+ application().run(|cx: &mut App| {
+ let bounds = Bounds::centered(None, size(px(400.), px(500.)), cx);
+ cx.open_window(
+ WindowOptions {
+ focus: true,
+ window_bounds: Some(WindowBounds::Windowed(bounds)),
+ ..Default::default()
+ },
+ |_, cx| cx.new(|_| BottomListDemo::new()),
+ )
+ .unwrap();
+ cx.activate(true);
+ });
+}
+
+#[cfg(not(target_family = "wasm"))]
+fn main() {
+ run_example();
+}
+
+#[cfg(target_family = "wasm")]
+#[wasm_bindgen::prelude::wasm_bindgen(start)]
+pub fn start() {
+ gpui_platform::web_init();
+ run_example();
+}
diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs
index b84241e9e0f79fe5cf8a24514cbf57982247a76b..ed441e3b40534690d02b31109e719c60dd5802e0 100644
--- a/crates/gpui/src/elements/list.rs
+++ b/crates/gpui/src/elements/list.rs
@@ -72,6 +72,7 @@ struct StateInner {
scrollbar_drag_start_height: Option,
measuring_behavior: ListMeasuringBehavior,
pending_scroll: Option,
+ follow_tail: bool,
}
/// Keeps track of a fractional scroll position within an item for restoration
@@ -102,6 +103,9 @@ pub struct ListScrollEvent {
/// Whether the list has been scrolled.
pub is_scrolled: bool,
+
+ /// Whether the list is currently in follow-tail mode (auto-scrolling to end).
+ pub is_following_tail: bool,
}
/// The sizing behavior to apply during layout.
@@ -236,6 +240,7 @@ impl ListState {
scrollbar_drag_start_height: None,
measuring_behavior: ListMeasuringBehavior::default(),
pending_scroll: None,
+ follow_tail: false,
})));
this.splice(0..0, item_count);
this
@@ -394,6 +399,34 @@ impl ListState {
});
}
+ /// Scroll the list to the very end (past the last item).
+ ///
+ /// Unlike [`scroll_to_reveal_item`], this uses the total item count as the
+ /// anchor, so the list's layout pass will walk backwards from the end and
+ /// always show the bottom of the last item — even when that item is still
+ /// growing (e.g. during streaming).
+ pub fn scroll_to_end(&self) {
+ let state = &mut *self.0.borrow_mut();
+ let item_count = state.items.summary().count;
+ state.logical_scroll_top = Some(ListOffset {
+ item_ix: item_count,
+ offset_in_item: px(0.),
+ });
+ }
+
+ /// Set whether the list should automatically follow the tail (auto-scroll to the end).
+ pub fn set_follow_tail(&self, follow: bool) {
+ self.0.borrow_mut().follow_tail = follow;
+ if follow {
+ self.scroll_to_end();
+ }
+ }
+
+ /// Returns whether the list is currently in follow-tail mode (auto-scrolling to the end).
+ pub fn is_following_tail(&self) -> bool {
+ self.0.borrow().follow_tail
+ }
+
/// Scroll the list to the given offset
pub fn scroll_to(&self, mut scroll_top: ListOffset) {
let state = &mut *self.0.borrow_mut();
@@ -493,18 +526,17 @@ impl ListState {
/// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
pub fn max_offset_for_scrollbar(&self) -> Point {
let state = self.0.borrow();
- let bounds = state.last_layout_bounds.unwrap_or_default();
-
- let height = state
- .scrollbar_drag_start_height
- .unwrap_or_else(|| state.items.summary().height);
-
- point(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height))
+ point(Pixels::ZERO, state.max_scroll_offset())
}
/// Returns the current scroll offset adjusted for the scrollbar
pub fn scroll_px_offset_for_scrollbar(&self) -> Point {
let state = &self.0.borrow();
+
+ if state.logical_scroll_top.is_none() && state.alignment == ListAlignment::Bottom {
+ return Point::new(px(0.), -state.max_scroll_offset());
+ }
+
let logical_scroll_top = state.logical_scroll_top();
let mut cursor = state.items.cursor::(());
@@ -526,6 +558,14 @@ impl ListState {
}
impl StateInner {
+ fn max_scroll_offset(&self) -> Pixels {
+ let bounds = self.last_layout_bounds.unwrap_or_default();
+ let height = self
+ .scrollbar_drag_start_height
+ .unwrap_or_else(|| self.items.summary().height);
+ (height - bounds.size.height).max(px(0.))
+ }
+
fn visible_range(
items: &SumTree,
height: Pixels,
@@ -552,7 +592,6 @@ impl StateInner {
if self.reset {
return;
}
-
let padding = self.last_padding.unwrap_or_default();
let scroll_max =
(self.items.summary().height + padding.top + padding.bottom - height).max(px(0.));
@@ -574,6 +613,10 @@ impl StateInner {
});
}
+ if self.follow_tail && delta.y > px(0.) {
+ self.follow_tail = false;
+ }
+
if let Some(handler) = self.scroll_handler.as_mut() {
let visible_range = Self::visible_range(&self.items, height, scroll_top);
handler(
@@ -581,6 +624,7 @@ impl StateInner {
visible_range,
count: self.items.summary().count,
is_scrolled: self.logical_scroll_top.is_some(),
+ is_following_tail: self.follow_tail,
},
window,
cx,
@@ -670,6 +714,15 @@ impl StateInner {
let mut rendered_height = padding.top;
let mut max_item_width = px(0.);
let mut scroll_top = self.logical_scroll_top();
+
+ if self.follow_tail {
+ scroll_top = ListOffset {
+ item_ix: self.items.summary().count,
+ offset_in_item: px(0.),
+ };
+ self.logical_scroll_top = Some(scroll_top);
+ }
+
let mut rendered_focused_item = false;
let available_item_space = size(
@@ -951,6 +1004,8 @@ impl StateInner {
content_height - self.scrollbar_drag_start_height.unwrap_or(content_height);
let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max);
+ self.follow_tail = false;
+
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
self.logical_scroll_top = None;
} else {
@@ -1449,4 +1504,257 @@ mod test {
assert_eq!(offset.item_ix, 2);
assert_eq!(offset.offset_in_item, px(20.));
}
+
+ #[gpui::test]
+ fn test_follow_tail_stays_at_bottom_as_items_grow(cx: &mut TestAppContext) {
+ let cx = cx.add_empty_window();
+
+ // 10 items, each 50px tall → 500px total content, 200px viewport.
+ // With follow-tail on, the list should always show the bottom.
+ let item_height = Rc::new(Cell::new(50usize));
+ let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
+
+ struct TestView {
+ state: ListState,
+ item_height: Rc
| >,
+ }
+ impl Render for TestView {
+ fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
+ let height = self.item_height.get();
+ list(self.state.clone(), move |_, _, _| {
+ div().h(px(height as f32)).w_full().into_any()
+ })
+ .w_full()
+ .h_full()
+ }
+ }
+
+ let state_clone = state.clone();
+ let item_height_clone = item_height.clone();
+ let view = cx.update(|_, cx| {
+ cx.new(|_| TestView {
+ state: state_clone,
+ item_height: item_height_clone,
+ })
+ });
+
+ state.set_follow_tail(true);
+
+ // First paint — items are 50px, total 500px, viewport 200px.
+ // Follow-tail should anchor to the end.
+ cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
+ view.clone().into_any_element()
+ });
+
+ // The scroll should be at the bottom: the last visible items fill the
+ // 200px viewport from the end of 500px of content (offset 300px).
+ let offset = state.logical_scroll_top();
+ assert_eq!(offset.item_ix, 6);
+ assert_eq!(offset.offset_in_item, px(0.));
+ assert!(state.is_following_tail());
+
+ // Simulate items growing (e.g. streaming content makes each item taller).
+ // 10 items × 80px = 800px total.
+ item_height.set(80);
+ state.remeasure();
+
+ cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
+ view.into_any_element()
+ });
+
+ // After growth, follow-tail should have re-anchored to the new end.
+ // 800px total − 200px viewport = 600px offset → item 7 at offset 40px,
+ // but follow-tail anchors to item_count (10), and layout walks back to
+ // fill 200px, landing at item 7 (7 × 80 = 560, 800 − 560 = 240 > 200,
+ // so item 8: 8 × 80 = 640, 800 − 640 = 160 < 200 → keeps walking →
+ // item 7: offset = 800 − 200 = 600, item_ix = 600/80 = 7, remainder 40).
+ let offset = state.logical_scroll_top();
+ assert_eq!(offset.item_ix, 7);
+ assert_eq!(offset.offset_in_item, px(40.));
+ assert!(state.is_following_tail());
+ }
+
+ #[gpui::test]
+ fn test_follow_tail_disengages_on_user_scroll(cx: &mut TestAppContext) {
+ let cx = cx.add_empty_window();
+
+ // 10 items × 50px = 500px total, 200px viewport.
+ let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
+
+ struct TestView(ListState);
+ impl Render for TestView {
+ fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
+ list(self.0.clone(), |_, _, _| {
+ div().h(px(50.)).w_full().into_any()
+ })
+ .w_full()
+ .h_full()
+ }
+ }
+
+ state.set_follow_tail(true);
+
+ // Paint with follow-tail — scroll anchored to the bottom.
+ cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, cx| {
+ cx.new(|_| TestView(state.clone())).into_any_element()
+ });
+ assert!(state.is_following_tail());
+
+ // Simulate the user scrolling up.
+ // This should disengage follow-tail.
+ cx.simulate_event(ScrollWheelEvent {
+ position: point(px(50.), px(100.)),
+ delta: ScrollDelta::Pixels(point(px(0.), px(100.))),
+ ..Default::default()
+ });
+
+ assert!(
+ !state.is_following_tail(),
+ "follow-tail should disengage when the user scrolls toward the start"
+ );
+ }
+
+ #[gpui::test]
+ fn test_follow_tail_disengages_on_scrollbar_reposition(cx: &mut TestAppContext) {
+ let cx = cx.add_empty_window();
+
+ // 10 items × 50px = 500px total, 200px viewport.
+ let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all();
+
+ struct TestView(ListState);
+ impl Render for TestView {
+ fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
+ list(self.0.clone(), |_, _, _| {
+ div().h(px(50.)).w_full().into_any()
+ })
+ .w_full()
+ .h_full()
+ }
+ }
+
+ let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
+
+ state.set_follow_tail(true);
+
+ // Paint with follow-tail — scroll anchored to the bottom.
+ cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
+ view.clone().into_any_element()
+ });
+ assert!(state.is_following_tail());
+
+ // Simulate the scrollbar moving the viewport to the middle.
+ // `set_offset_from_scrollbar` accepts a positive distance from the start.
+ state.set_offset_from_scrollbar(point(px(0.), px(150.)));
+
+ let offset = state.logical_scroll_top();
+ assert_eq!(offset.item_ix, 3);
+ assert_eq!(offset.offset_in_item, px(0.));
+ assert!(
+ !state.is_following_tail(),
+ "follow-tail should disengage when the scrollbar manually repositions the list"
+ );
+
+ // A subsequent draw should preserve the user's manual position instead
+ // of snapping back to the end.
+ cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
+ view.into_any_element()
+ });
+
+ let offset = state.logical_scroll_top();
+ assert_eq!(offset.item_ix, 3);
+ assert_eq!(offset.offset_in_item, px(0.));
+ }
+
+ #[gpui::test]
+ fn test_set_follow_tail_snaps_to_bottom(cx: &mut TestAppContext) {
+ let cx = cx.add_empty_window();
+
+ // 10 items × 50px = 500px total, 200px viewport.
+ let state = ListState::new(10, crate::ListAlignment::Top, px(0.));
+
+ struct TestView(ListState);
+ impl Render for TestView {
+ fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
+ list(self.0.clone(), |_, _, _| {
+ div().h(px(50.)).w_full().into_any()
+ })
+ .w_full()
+ .h_full()
+ }
+ }
+
+ let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone())));
+
+ // Scroll to the middle of the list (item 3).
+ state.scroll_to(gpui::ListOffset {
+ item_ix: 3,
+ offset_in_item: px(0.),
+ });
+
+ cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
+ view.clone().into_any_element()
+ });
+
+ let offset = state.logical_scroll_top();
+ assert_eq!(offset.item_ix, 3);
+ assert_eq!(offset.offset_in_item, px(0.));
+ assert!(!state.is_following_tail());
+
+ // Enable follow-tail — this should immediately snap the scroll anchor
+ // to the end, like the user just sent a prompt.
+ state.set_follow_tail(true);
+
+ cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| {
+ view.into_any_element()
+ });
+
+ // After paint, scroll should be at the bottom.
+ // 500px total − 200px viewport = 300px offset → item 6, offset 0.
+ let offset = state.logical_scroll_top();
+ assert_eq!(offset.item_ix, 6);
+ assert_eq!(offset.offset_in_item, px(0.));
+ assert!(state.is_following_tail());
+ }
+
+ #[gpui::test]
+ fn test_bottom_aligned_scrollbar_offset_at_end(cx: &mut TestAppContext) {
+ let cx = cx.add_empty_window();
+
+ const ITEMS: usize = 10;
+ const ITEM_SIZE: f32 = 50.0;
+
+ let state = ListState::new(
+ ITEMS,
+ crate::ListAlignment::Bottom,
+ px(ITEMS as f32 * ITEM_SIZE),
+ );
+
+ struct TestView(ListState);
+ impl Render for TestView {
+ fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
+ list(self.0.clone(), |_, _, _| {
+ div().h(px(ITEM_SIZE)).w_full().into_any()
+ })
+ .w_full()
+ .h_full()
+ }
+ }
+
+ cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| {
+ cx.new(|_| TestView(state.clone())).into_any_element()
+ });
+
+ // Bottom-aligned lists start pinned to the end: logical_scroll_top returns
+ // item_ix == item_count, meaning no explicit scroll position has been set.
+ assert_eq!(state.logical_scroll_top().item_ix, ITEMS);
+
+ let max_offset = state.max_offset_for_scrollbar();
+ let scroll_offset = state.scroll_px_offset_for_scrollbar();
+
+ assert_eq!(
+ -scroll_offset.y, max_offset.y,
+ "scrollbar offset ({}) should equal max offset ({}) when list is pinned to bottom",
+ -scroll_offset.y, max_offset.y,
+ );
+ }
}
diff --git a/crates/grammars/Cargo.toml b/crates/grammars/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..13b3bf5c94bb459e49e5b1f337fe95b1b216829a
--- /dev/null
+++ b/crates/grammars/Cargo.toml
@@ -0,0 +1,60 @@
+[package]
+name = "grammars"
+version = "0.1.0"
+edition = "2024"
+publish = false
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/grammars.rs"
+
+[dependencies]
+language_core.workspace = true
+rust-embed.workspace = true
+anyhow.workspace = true
+toml.workspace = true
+util.workspace = true
+
+tree-sitter = { workspace = true, optional = true }
+tree-sitter-bash = { workspace = true, optional = true }
+tree-sitter-c = { workspace = true, optional = true }
+tree-sitter-cpp = { workspace = true, optional = true }
+tree-sitter-css = { workspace = true, optional = true }
+tree-sitter-diff = { workspace = true, optional = true }
+tree-sitter-gitcommit = { workspace = true, optional = true }
+tree-sitter-go = { workspace = true, optional = true }
+tree-sitter-go-mod = { workspace = true, optional = true }
+tree-sitter-gowork = { workspace = true, optional = true }
+tree-sitter-jsdoc = { workspace = true, optional = true }
+tree-sitter-json = { workspace = true, optional = true }
+tree-sitter-md = { workspace = true, optional = true }
+tree-sitter-python = { workspace = true, optional = true }
+tree-sitter-regex = { workspace = true, optional = true }
+tree-sitter-rust = { workspace = true, optional = true }
+tree-sitter-typescript = { workspace = true, optional = true }
+tree-sitter-yaml = { workspace = true, optional = true }
+
+[features]
+load-grammars = [
+ "tree-sitter",
+ "tree-sitter-bash",
+ "tree-sitter-c",
+ "tree-sitter-cpp",
+ "tree-sitter-css",
+ "tree-sitter-diff",
+ "tree-sitter-gitcommit",
+ "tree-sitter-go",
+ "tree-sitter-go-mod",
+ "tree-sitter-gowork",
+ "tree-sitter-jsdoc",
+ "tree-sitter-json",
+ "tree-sitter-md",
+ "tree-sitter-python",
+ "tree-sitter-regex",
+ "tree-sitter-rust",
+ "tree-sitter-typescript",
+ "tree-sitter-yaml",
+]
+test-support = ["load-grammars"]
diff --git a/crates/grammars/LICENSE-GPL b/crates/grammars/LICENSE-GPL
new file mode 120000
index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4
--- /dev/null
+++ b/crates/grammars/LICENSE-GPL
@@ -0,0 +1 @@
+../../LICENSE-GPL
\ No newline at end of file
diff --git a/crates/languages/src/bash/brackets.scm b/crates/grammars/src/bash/brackets.scm
similarity index 100%
rename from crates/languages/src/bash/brackets.scm
rename to crates/grammars/src/bash/brackets.scm
diff --git a/crates/languages/src/bash/config.toml b/crates/grammars/src/bash/config.toml
similarity index 100%
rename from crates/languages/src/bash/config.toml
rename to crates/grammars/src/bash/config.toml
diff --git a/crates/languages/src/bash/highlights.scm b/crates/grammars/src/bash/highlights.scm
similarity index 100%
rename from crates/languages/src/bash/highlights.scm
rename to crates/grammars/src/bash/highlights.scm
diff --git a/crates/languages/src/bash/indents.scm b/crates/grammars/src/bash/indents.scm
similarity index 100%
rename from crates/languages/src/bash/indents.scm
rename to crates/grammars/src/bash/indents.scm
diff --git a/crates/languages/src/bash/injections.scm b/crates/grammars/src/bash/injections.scm
similarity index 100%
rename from crates/languages/src/bash/injections.scm
rename to crates/grammars/src/bash/injections.scm
diff --git a/crates/languages/src/bash/overrides.scm b/crates/grammars/src/bash/overrides.scm
similarity index 100%
rename from crates/languages/src/bash/overrides.scm
rename to crates/grammars/src/bash/overrides.scm
diff --git a/crates/languages/src/bash/redactions.scm b/crates/grammars/src/bash/redactions.scm
similarity index 100%
rename from crates/languages/src/bash/redactions.scm
rename to crates/grammars/src/bash/redactions.scm
diff --git a/crates/languages/src/bash/runnables.scm b/crates/grammars/src/bash/runnables.scm
similarity index 100%
rename from crates/languages/src/bash/runnables.scm
rename to crates/grammars/src/bash/runnables.scm
diff --git a/crates/languages/src/bash/textobjects.scm b/crates/grammars/src/bash/textobjects.scm
similarity index 100%
rename from crates/languages/src/bash/textobjects.scm
rename to crates/grammars/src/bash/textobjects.scm
diff --git a/crates/languages/src/c/brackets.scm b/crates/grammars/src/c/brackets.scm
similarity index 100%
rename from crates/languages/src/c/brackets.scm
rename to crates/grammars/src/c/brackets.scm
diff --git a/crates/languages/src/c/config.toml b/crates/grammars/src/c/config.toml
similarity index 100%
rename from crates/languages/src/c/config.toml
rename to crates/grammars/src/c/config.toml
diff --git a/crates/languages/src/c/highlights.scm b/crates/grammars/src/c/highlights.scm
similarity index 100%
rename from crates/languages/src/c/highlights.scm
rename to crates/grammars/src/c/highlights.scm
diff --git a/crates/languages/src/c/imports.scm b/crates/grammars/src/c/imports.scm
similarity index 100%
rename from crates/languages/src/c/imports.scm
rename to crates/grammars/src/c/imports.scm
diff --git a/crates/languages/src/c/indents.scm b/crates/grammars/src/c/indents.scm
similarity index 100%
rename from crates/languages/src/c/indents.scm
rename to crates/grammars/src/c/indents.scm
diff --git a/crates/languages/src/c/injections.scm b/crates/grammars/src/c/injections.scm
similarity index 100%
rename from crates/languages/src/c/injections.scm
rename to crates/grammars/src/c/injections.scm
diff --git a/crates/languages/src/c/outline.scm b/crates/grammars/src/c/outline.scm
similarity index 100%
rename from crates/languages/src/c/outline.scm
rename to crates/grammars/src/c/outline.scm
diff --git a/crates/languages/src/c/overrides.scm b/crates/grammars/src/c/overrides.scm
similarity index 100%
rename from crates/languages/src/c/overrides.scm
rename to crates/grammars/src/c/overrides.scm
diff --git a/crates/languages/src/c/runnables.scm b/crates/grammars/src/c/runnables.scm
similarity index 100%
rename from crates/languages/src/c/runnables.scm
rename to crates/grammars/src/c/runnables.scm
diff --git a/crates/languages/src/c/textobjects.scm b/crates/grammars/src/c/textobjects.scm
similarity index 100%
rename from crates/languages/src/c/textobjects.scm
rename to crates/grammars/src/c/textobjects.scm
diff --git a/crates/languages/src/cpp/brackets.scm b/crates/grammars/src/cpp/brackets.scm
similarity index 100%
rename from crates/languages/src/cpp/brackets.scm
rename to crates/grammars/src/cpp/brackets.scm
diff --git a/crates/languages/src/cpp/config.toml b/crates/grammars/src/cpp/config.toml
similarity index 100%
rename from crates/languages/src/cpp/config.toml
rename to crates/grammars/src/cpp/config.toml
diff --git a/crates/languages/src/cpp/highlights.scm b/crates/grammars/src/cpp/highlights.scm
similarity index 100%
rename from crates/languages/src/cpp/highlights.scm
rename to crates/grammars/src/cpp/highlights.scm
diff --git a/crates/languages/src/cpp/imports.scm b/crates/grammars/src/cpp/imports.scm
similarity index 100%
rename from crates/languages/src/cpp/imports.scm
rename to crates/grammars/src/cpp/imports.scm
diff --git a/crates/languages/src/cpp/indents.scm b/crates/grammars/src/cpp/indents.scm
similarity index 100%
rename from crates/languages/src/cpp/indents.scm
rename to crates/grammars/src/cpp/indents.scm
diff --git a/crates/languages/src/cpp/injections.scm b/crates/grammars/src/cpp/injections.scm
similarity index 100%
rename from crates/languages/src/cpp/injections.scm
rename to crates/grammars/src/cpp/injections.scm
diff --git a/crates/languages/src/cpp/outline.scm b/crates/grammars/src/cpp/outline.scm
similarity index 100%
rename from crates/languages/src/cpp/outline.scm
rename to crates/grammars/src/cpp/outline.scm
diff --git a/crates/languages/src/cpp/overrides.scm b/crates/grammars/src/cpp/overrides.scm
similarity index 100%
rename from crates/languages/src/cpp/overrides.scm
rename to crates/grammars/src/cpp/overrides.scm
diff --git a/crates/languages/src/cpp/semantic_token_rules.json b/crates/grammars/src/cpp/semantic_token_rules.json
similarity index 100%
rename from crates/languages/src/cpp/semantic_token_rules.json
rename to crates/grammars/src/cpp/semantic_token_rules.json
diff --git a/crates/languages/src/cpp/textobjects.scm b/crates/grammars/src/cpp/textobjects.scm
similarity index 100%
rename from crates/languages/src/cpp/textobjects.scm
rename to crates/grammars/src/cpp/textobjects.scm
diff --git a/crates/languages/src/css/brackets.scm b/crates/grammars/src/css/brackets.scm
similarity index 100%
rename from crates/languages/src/css/brackets.scm
rename to crates/grammars/src/css/brackets.scm
diff --git a/crates/languages/src/css/config.toml b/crates/grammars/src/css/config.toml
similarity index 100%
rename from crates/languages/src/css/config.toml
rename to crates/grammars/src/css/config.toml
diff --git a/crates/languages/src/css/highlights.scm b/crates/grammars/src/css/highlights.scm
similarity index 100%
rename from crates/languages/src/css/highlights.scm
rename to crates/grammars/src/css/highlights.scm
diff --git a/crates/languages/src/css/indents.scm b/crates/grammars/src/css/indents.scm
similarity index 100%
rename from crates/languages/src/css/indents.scm
rename to crates/grammars/src/css/indents.scm
diff --git a/crates/languages/src/css/injections.scm b/crates/grammars/src/css/injections.scm
similarity index 100%
rename from crates/languages/src/css/injections.scm
rename to crates/grammars/src/css/injections.scm
diff --git a/crates/languages/src/css/outline.scm b/crates/grammars/src/css/outline.scm
similarity index 100%
rename from crates/languages/src/css/outline.scm
rename to crates/grammars/src/css/outline.scm
diff --git a/crates/languages/src/css/overrides.scm b/crates/grammars/src/css/overrides.scm
similarity index 100%
rename from crates/languages/src/css/overrides.scm
rename to crates/grammars/src/css/overrides.scm
diff --git a/crates/languages/src/css/textobjects.scm b/crates/grammars/src/css/textobjects.scm
similarity index 100%
rename from crates/languages/src/css/textobjects.scm
rename to crates/grammars/src/css/textobjects.scm
diff --git a/crates/languages/src/diff/config.toml b/crates/grammars/src/diff/config.toml
similarity index 100%
rename from crates/languages/src/diff/config.toml
rename to crates/grammars/src/diff/config.toml
diff --git a/crates/languages/src/diff/highlights.scm b/crates/grammars/src/diff/highlights.scm
similarity index 100%
rename from crates/languages/src/diff/highlights.scm
rename to crates/grammars/src/diff/highlights.scm
diff --git a/crates/languages/src/diff/injections.scm b/crates/grammars/src/diff/injections.scm
similarity index 100%
rename from crates/languages/src/diff/injections.scm
rename to crates/grammars/src/diff/injections.scm
diff --git a/crates/languages/src/gitcommit/config.toml b/crates/grammars/src/gitcommit/config.toml
similarity index 100%
rename from crates/languages/src/gitcommit/config.toml
rename to crates/grammars/src/gitcommit/config.toml
diff --git a/crates/languages/src/gitcommit/highlights.scm b/crates/grammars/src/gitcommit/highlights.scm
similarity index 100%
rename from crates/languages/src/gitcommit/highlights.scm
rename to crates/grammars/src/gitcommit/highlights.scm
diff --git a/crates/languages/src/gitcommit/injections.scm b/crates/grammars/src/gitcommit/injections.scm
similarity index 100%
rename from crates/languages/src/gitcommit/injections.scm
rename to crates/grammars/src/gitcommit/injections.scm
diff --git a/crates/languages/src/go/brackets.scm b/crates/grammars/src/go/brackets.scm
similarity index 100%
rename from crates/languages/src/go/brackets.scm
rename to crates/grammars/src/go/brackets.scm
diff --git a/crates/languages/src/go/config.toml b/crates/grammars/src/go/config.toml
similarity index 100%
rename from crates/languages/src/go/config.toml
rename to crates/grammars/src/go/config.toml
diff --git a/crates/languages/src/go/debugger.scm b/crates/grammars/src/go/debugger.scm
similarity index 100%
rename from crates/languages/src/go/debugger.scm
rename to crates/grammars/src/go/debugger.scm
diff --git a/crates/languages/src/go/highlights.scm b/crates/grammars/src/go/highlights.scm
similarity index 100%
rename from crates/languages/src/go/highlights.scm
rename to crates/grammars/src/go/highlights.scm
diff --git a/crates/languages/src/go/imports.scm b/crates/grammars/src/go/imports.scm
similarity index 100%
rename from crates/languages/src/go/imports.scm
rename to crates/grammars/src/go/imports.scm
diff --git a/crates/languages/src/go/indents.scm b/crates/grammars/src/go/indents.scm
similarity index 100%
rename from crates/languages/src/go/indents.scm
rename to crates/grammars/src/go/indents.scm
diff --git a/crates/languages/src/go/injections.scm b/crates/grammars/src/go/injections.scm
similarity index 100%
rename from crates/languages/src/go/injections.scm
rename to crates/grammars/src/go/injections.scm
diff --git a/crates/languages/src/go/outline.scm b/crates/grammars/src/go/outline.scm
similarity index 100%
rename from crates/languages/src/go/outline.scm
rename to crates/grammars/src/go/outline.scm
diff --git a/crates/languages/src/go/overrides.scm b/crates/grammars/src/go/overrides.scm
similarity index 100%
rename from crates/languages/src/go/overrides.scm
rename to crates/grammars/src/go/overrides.scm
diff --git a/crates/languages/src/go/runnables.scm b/crates/grammars/src/go/runnables.scm
similarity index 100%
rename from crates/languages/src/go/runnables.scm
rename to crates/grammars/src/go/runnables.scm
diff --git a/crates/languages/src/go/semantic_token_rules.json b/crates/grammars/src/go/semantic_token_rules.json
similarity index 100%
rename from crates/languages/src/go/semantic_token_rules.json
rename to crates/grammars/src/go/semantic_token_rules.json
diff --git a/crates/languages/src/go/textobjects.scm b/crates/grammars/src/go/textobjects.scm
similarity index 100%
rename from crates/languages/src/go/textobjects.scm
rename to crates/grammars/src/go/textobjects.scm
diff --git a/crates/languages/src/gomod/config.toml b/crates/grammars/src/gomod/config.toml
similarity index 100%
rename from crates/languages/src/gomod/config.toml
rename to crates/grammars/src/gomod/config.toml
diff --git a/crates/languages/src/gomod/highlights.scm b/crates/grammars/src/gomod/highlights.scm
similarity index 100%
rename from crates/languages/src/gomod/highlights.scm
rename to crates/grammars/src/gomod/highlights.scm
diff --git a/crates/languages/src/gomod/injections.scm b/crates/grammars/src/gomod/injections.scm
similarity index 100%
rename from crates/languages/src/gomod/injections.scm
rename to crates/grammars/src/gomod/injections.scm
diff --git a/crates/languages/src/gomod/structure.scm b/crates/grammars/src/gomod/structure.scm
similarity index 100%
rename from crates/languages/src/gomod/structure.scm
rename to crates/grammars/src/gomod/structure.scm
diff --git a/crates/languages/src/gowork/config.toml b/crates/grammars/src/gowork/config.toml
similarity index 100%
rename from crates/languages/src/gowork/config.toml
rename to crates/grammars/src/gowork/config.toml
diff --git a/crates/languages/src/gowork/highlights.scm b/crates/grammars/src/gowork/highlights.scm
similarity index 100%
rename from crates/languages/src/gowork/highlights.scm
rename to crates/grammars/src/gowork/highlights.scm
diff --git a/crates/languages/src/gowork/injections.scm b/crates/grammars/src/gowork/injections.scm
similarity index 100%
rename from crates/languages/src/gowork/injections.scm
rename to crates/grammars/src/gowork/injections.scm
diff --git a/crates/grammars/src/grammars.rs b/crates/grammars/src/grammars.rs
new file mode 100644
index 0000000000000000000000000000000000000000..00d6e6281c45b10a5dcfbd188b5848c63cc0cd75
--- /dev/null
+++ b/crates/grammars/src/grammars.rs
@@ -0,0 +1,108 @@
+use anyhow::Context as _;
+use language_core::{LanguageConfig, LanguageQueries, QUERY_FILENAME_PREFIXES};
+use rust_embed::RustEmbed;
+use util::asset_str;
+
+#[derive(RustEmbed)]
+#[folder = "src/"]
+#[exclude = "*.rs"]
+struct GrammarDir;
+
+/// Register all built-in native tree-sitter grammars with the provided registration function.
+///
+/// Each grammar is registered as a `(&str, tree_sitter_language::LanguageFn)` pair.
+/// This must be called before loading language configs/queries.
+#[cfg(feature = "load-grammars")]
+pub fn native_grammars() -> Vec<(&'static str, tree_sitter::Language)> {
+ vec![
+ ("bash", tree_sitter_bash::LANGUAGE.into()),
+ ("c", tree_sitter_c::LANGUAGE.into()),
+ ("cpp", tree_sitter_cpp::LANGUAGE.into()),
+ ("css", tree_sitter_css::LANGUAGE.into()),
+ ("diff", tree_sitter_diff::LANGUAGE.into()),
+ ("go", tree_sitter_go::LANGUAGE.into()),
+ ("gomod", tree_sitter_go_mod::LANGUAGE.into()),
+ ("gowork", tree_sitter_gowork::LANGUAGE.into()),
+ ("jsdoc", tree_sitter_jsdoc::LANGUAGE.into()),
+ ("json", tree_sitter_json::LANGUAGE.into()),
+ ("jsonc", tree_sitter_json::LANGUAGE.into()),
+ ("markdown", tree_sitter_md::LANGUAGE.into()),
+ ("markdown-inline", tree_sitter_md::INLINE_LANGUAGE.into()),
+ ("python", tree_sitter_python::LANGUAGE.into()),
+ ("regex", tree_sitter_regex::LANGUAGE.into()),
+ ("rust", tree_sitter_rust::LANGUAGE.into()),
+ ("tsx", tree_sitter_typescript::LANGUAGE_TSX.into()),
+ (
+ "typescript",
+ tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
+ ),
+ ("yaml", tree_sitter_yaml::LANGUAGE.into()),
+ ("gitcommit", tree_sitter_gitcommit::LANGUAGE.into()),
+ ]
+}
+
+/// Load and parse the `config.toml` for a given language name.
+pub fn load_config(name: &str) -> LanguageConfig {
+ let config_toml = String::from_utf8(
+ GrammarDir::get(&format!("{}/config.toml", name))
+ .unwrap_or_else(|| panic!("missing config for language {:?}", name))
+ .data
+ .to_vec(),
+ )
+ .unwrap();
+
+ let config: LanguageConfig = ::toml::from_str(&config_toml)
+ .with_context(|| format!("failed to load config.toml for language {name:?}"))
+ .unwrap();
+
+ config
+}
+
+/// Load and parse the `config.toml` for a given language name, stripping fields
+/// that require grammar support when grammars are not loaded.
+pub fn load_config_for_feature(name: &str, grammars_loaded: bool) -> LanguageConfig {
+ let config = load_config(name);
+
+ if grammars_loaded {
+ config
+ } else {
+ LanguageConfig {
+ name: config.name,
+ matcher: config.matcher,
+ jsx_tag_auto_close: config.jsx_tag_auto_close,
+ ..Default::default()
+ }
+ }
+}
+
+/// Get a raw embedded file by path (relative to `src/`).
+///
+/// Returns the file data as bytes, or `None` if the file does not exist.
+pub fn get_file(path: &str) -> Option {
+ GrammarDir::get(path)
+}
+
+/// Load all `.scm` query files for a given language name into a `LanguageQueries`.
+///
+/// Multiple `.scm` files with the same prefix (e.g. `highlights.scm` and
+/// `highlights_extra.scm`) are concatenated together with their contents appended.
+pub fn load_queries(name: &str) -> LanguageQueries {
+ let mut result = LanguageQueries::default();
+ for path in GrammarDir::iter() {
+ if let Some(remainder) = path.strip_prefix(name).and_then(|p| p.strip_prefix('/')) {
+ if !remainder.ends_with(".scm") {
+ continue;
+ }
+ for (prefix, query) in QUERY_FILENAME_PREFIXES {
+ if remainder.starts_with(prefix) {
+ let contents = asset_str::(path.as_ref());
+ match query(&mut result) {
+ None => *query(&mut result) = Some(contents),
+ Some(existing) => existing.to_mut().push_str(contents.as_ref()),
+ }
+ }
+ }
+ }
+ }
+ result
+}
diff --git a/crates/languages/src/javascript/brackets.scm b/crates/grammars/src/javascript/brackets.scm
similarity index 100%
rename from crates/languages/src/javascript/brackets.scm
rename to crates/grammars/src/javascript/brackets.scm
diff --git a/crates/languages/src/javascript/config.toml b/crates/grammars/src/javascript/config.toml
similarity index 100%
rename from crates/languages/src/javascript/config.toml
rename to crates/grammars/src/javascript/config.toml
diff --git a/crates/languages/src/javascript/debugger.scm b/crates/grammars/src/javascript/debugger.scm
similarity index 100%
rename from crates/languages/src/javascript/debugger.scm
rename to crates/grammars/src/javascript/debugger.scm
diff --git a/crates/languages/src/javascript/highlights.scm b/crates/grammars/src/javascript/highlights.scm
similarity index 100%
rename from crates/languages/src/javascript/highlights.scm
rename to crates/grammars/src/javascript/highlights.scm
diff --git a/crates/languages/src/javascript/imports.scm b/crates/grammars/src/javascript/imports.scm
similarity index 100%
rename from crates/languages/src/javascript/imports.scm
rename to crates/grammars/src/javascript/imports.scm
diff --git a/crates/languages/src/javascript/indents.scm b/crates/grammars/src/javascript/indents.scm
similarity index 100%
rename from crates/languages/src/javascript/indents.scm
rename to crates/grammars/src/javascript/indents.scm
diff --git a/crates/languages/src/javascript/injections.scm b/crates/grammars/src/javascript/injections.scm
similarity index 100%
rename from crates/languages/src/javascript/injections.scm
rename to crates/grammars/src/javascript/injections.scm
diff --git a/crates/languages/src/javascript/outline.scm b/crates/grammars/src/javascript/outline.scm
similarity index 100%
rename from crates/languages/src/javascript/outline.scm
rename to crates/grammars/src/javascript/outline.scm
diff --git a/crates/languages/src/javascript/overrides.scm b/crates/grammars/src/javascript/overrides.scm
similarity index 100%
rename from crates/languages/src/javascript/overrides.scm
rename to crates/grammars/src/javascript/overrides.scm
diff --git a/crates/languages/src/javascript/runnables.scm b/crates/grammars/src/javascript/runnables.scm
similarity index 100%
rename from crates/languages/src/javascript/runnables.scm
rename to crates/grammars/src/javascript/runnables.scm
diff --git a/crates/languages/src/javascript/textobjects.scm b/crates/grammars/src/javascript/textobjects.scm
similarity index 100%
rename from crates/languages/src/javascript/textobjects.scm
rename to crates/grammars/src/javascript/textobjects.scm
diff --git a/crates/languages/src/jsdoc/brackets.scm b/crates/grammars/src/jsdoc/brackets.scm
similarity index 100%
rename from crates/languages/src/jsdoc/brackets.scm
rename to crates/grammars/src/jsdoc/brackets.scm
diff --git a/crates/languages/src/jsdoc/config.toml b/crates/grammars/src/jsdoc/config.toml
similarity index 100%
rename from crates/languages/src/jsdoc/config.toml
rename to crates/grammars/src/jsdoc/config.toml
diff --git a/crates/languages/src/jsdoc/highlights.scm b/crates/grammars/src/jsdoc/highlights.scm
similarity index 100%
rename from crates/languages/src/jsdoc/highlights.scm
rename to crates/grammars/src/jsdoc/highlights.scm
diff --git a/crates/languages/src/json/brackets.scm b/crates/grammars/src/json/brackets.scm
similarity index 100%
rename from crates/languages/src/json/brackets.scm
rename to crates/grammars/src/json/brackets.scm
diff --git a/crates/languages/src/json/config.toml b/crates/grammars/src/json/config.toml
similarity index 100%
rename from crates/languages/src/json/config.toml
rename to crates/grammars/src/json/config.toml
diff --git a/crates/languages/src/json/highlights.scm b/crates/grammars/src/json/highlights.scm
similarity index 100%
rename from crates/languages/src/json/highlights.scm
rename to crates/grammars/src/json/highlights.scm
diff --git a/crates/languages/src/json/indents.scm b/crates/grammars/src/json/indents.scm
similarity index 100%
rename from crates/languages/src/json/indents.scm
rename to crates/grammars/src/json/indents.scm
diff --git a/crates/languages/src/json/outline.scm b/crates/grammars/src/json/outline.scm
similarity index 100%
rename from crates/languages/src/json/outline.scm
rename to crates/grammars/src/json/outline.scm
diff --git a/crates/languages/src/json/overrides.scm b/crates/grammars/src/json/overrides.scm
similarity index 100%
rename from crates/languages/src/json/overrides.scm
rename to crates/grammars/src/json/overrides.scm
diff --git a/crates/languages/src/json/redactions.scm b/crates/grammars/src/json/redactions.scm
similarity index 100%
rename from crates/languages/src/json/redactions.scm
rename to crates/grammars/src/json/redactions.scm
diff --git a/crates/languages/src/json/runnables.scm b/crates/grammars/src/json/runnables.scm
similarity index 100%
rename from crates/languages/src/json/runnables.scm
rename to crates/grammars/src/json/runnables.scm
diff --git a/crates/languages/src/json/textobjects.scm b/crates/grammars/src/json/textobjects.scm
similarity index 100%
rename from crates/languages/src/json/textobjects.scm
rename to crates/grammars/src/json/textobjects.scm
diff --git a/crates/languages/src/jsonc/brackets.scm b/crates/grammars/src/jsonc/brackets.scm
similarity index 100%
rename from crates/languages/src/jsonc/brackets.scm
rename to crates/grammars/src/jsonc/brackets.scm
diff --git a/crates/languages/src/jsonc/config.toml b/crates/grammars/src/jsonc/config.toml
similarity index 100%
rename from crates/languages/src/jsonc/config.toml
rename to crates/grammars/src/jsonc/config.toml
diff --git a/crates/languages/src/jsonc/highlights.scm b/crates/grammars/src/jsonc/highlights.scm
similarity index 100%
rename from crates/languages/src/jsonc/highlights.scm
rename to crates/grammars/src/jsonc/highlights.scm
diff --git a/crates/languages/src/jsonc/indents.scm b/crates/grammars/src/jsonc/indents.scm
similarity index 100%
rename from crates/languages/src/jsonc/indents.scm
rename to crates/grammars/src/jsonc/indents.scm
diff --git a/crates/languages/src/jsonc/injections.scm b/crates/grammars/src/jsonc/injections.scm
similarity index 100%
rename from crates/languages/src/jsonc/injections.scm
rename to crates/grammars/src/jsonc/injections.scm
diff --git a/crates/languages/src/jsonc/outline.scm b/crates/grammars/src/jsonc/outline.scm
similarity index 100%
rename from crates/languages/src/jsonc/outline.scm
rename to crates/grammars/src/jsonc/outline.scm
diff --git a/crates/languages/src/jsonc/overrides.scm b/crates/grammars/src/jsonc/overrides.scm
similarity index 100%
rename from crates/languages/src/jsonc/overrides.scm
rename to crates/grammars/src/jsonc/overrides.scm
diff --git a/crates/languages/src/jsonc/redactions.scm b/crates/grammars/src/jsonc/redactions.scm
similarity index 100%
rename from crates/languages/src/jsonc/redactions.scm
rename to crates/grammars/src/jsonc/redactions.scm
diff --git a/crates/languages/src/jsonc/textobjects.scm b/crates/grammars/src/jsonc/textobjects.scm
similarity index 100%
rename from crates/languages/src/jsonc/textobjects.scm
rename to crates/grammars/src/jsonc/textobjects.scm
diff --git a/crates/languages/src/markdown-inline/config.toml b/crates/grammars/src/markdown-inline/config.toml
similarity index 100%
rename from crates/languages/src/markdown-inline/config.toml
rename to crates/grammars/src/markdown-inline/config.toml
diff --git a/crates/languages/src/markdown-inline/highlights.scm b/crates/grammars/src/markdown-inline/highlights.scm
similarity index 100%
rename from crates/languages/src/markdown-inline/highlights.scm
rename to crates/grammars/src/markdown-inline/highlights.scm
diff --git a/crates/languages/src/markdown-inline/injections.scm b/crates/grammars/src/markdown-inline/injections.scm
similarity index 100%
rename from crates/languages/src/markdown-inline/injections.scm
rename to crates/grammars/src/markdown-inline/injections.scm
diff --git a/crates/languages/src/markdown/brackets.scm b/crates/grammars/src/markdown/brackets.scm
similarity index 100%
rename from crates/languages/src/markdown/brackets.scm
rename to crates/grammars/src/markdown/brackets.scm
diff --git a/crates/languages/src/markdown/config.toml b/crates/grammars/src/markdown/config.toml
similarity index 100%
rename from crates/languages/src/markdown/config.toml
rename to crates/grammars/src/markdown/config.toml
diff --git a/crates/languages/src/markdown/highlights.scm b/crates/grammars/src/markdown/highlights.scm
similarity index 100%
rename from crates/languages/src/markdown/highlights.scm
rename to crates/grammars/src/markdown/highlights.scm
diff --git a/crates/languages/src/markdown/indents.scm b/crates/grammars/src/markdown/indents.scm
similarity index 100%
rename from crates/languages/src/markdown/indents.scm
rename to crates/grammars/src/markdown/indents.scm
diff --git a/crates/languages/src/markdown/injections.scm b/crates/grammars/src/markdown/injections.scm
similarity index 100%
rename from crates/languages/src/markdown/injections.scm
rename to crates/grammars/src/markdown/injections.scm
diff --git a/crates/languages/src/markdown/outline.scm b/crates/grammars/src/markdown/outline.scm
similarity index 100%
rename from crates/languages/src/markdown/outline.scm
rename to crates/grammars/src/markdown/outline.scm
diff --git a/crates/languages/src/markdown/textobjects.scm b/crates/grammars/src/markdown/textobjects.scm
similarity index 100%
rename from crates/languages/src/markdown/textobjects.scm
rename to crates/grammars/src/markdown/textobjects.scm
diff --git a/crates/languages/src/python/brackets.scm b/crates/grammars/src/python/brackets.scm
similarity index 100%
rename from crates/languages/src/python/brackets.scm
rename to crates/grammars/src/python/brackets.scm
diff --git a/crates/languages/src/python/config.toml b/crates/grammars/src/python/config.toml
similarity index 100%
rename from crates/languages/src/python/config.toml
rename to crates/grammars/src/python/config.toml
diff --git a/crates/languages/src/python/debugger.scm b/crates/grammars/src/python/debugger.scm
similarity index 100%
rename from crates/languages/src/python/debugger.scm
rename to crates/grammars/src/python/debugger.scm
diff --git a/crates/languages/src/python/highlights.scm b/crates/grammars/src/python/highlights.scm
similarity index 100%
rename from crates/languages/src/python/highlights.scm
rename to crates/grammars/src/python/highlights.scm
diff --git a/crates/languages/src/python/imports.scm b/crates/grammars/src/python/imports.scm
similarity index 100%
rename from crates/languages/src/python/imports.scm
rename to crates/grammars/src/python/imports.scm
diff --git a/crates/languages/src/python/indents.scm b/crates/grammars/src/python/indents.scm
similarity index 100%
rename from crates/languages/src/python/indents.scm
rename to crates/grammars/src/python/indents.scm
diff --git a/crates/languages/src/python/injections.scm b/crates/grammars/src/python/injections.scm
similarity index 100%
rename from crates/languages/src/python/injections.scm
rename to crates/grammars/src/python/injections.scm
diff --git a/crates/languages/src/python/outline.scm b/crates/grammars/src/python/outline.scm
similarity index 100%
rename from crates/languages/src/python/outline.scm
rename to crates/grammars/src/python/outline.scm
diff --git a/crates/languages/src/python/overrides.scm b/crates/grammars/src/python/overrides.scm
similarity index 100%
rename from crates/languages/src/python/overrides.scm
rename to crates/grammars/src/python/overrides.scm
diff --git a/crates/languages/src/python/runnables.scm b/crates/grammars/src/python/runnables.scm
similarity index 100%
rename from crates/languages/src/python/runnables.scm
rename to crates/grammars/src/python/runnables.scm
diff --git a/crates/languages/src/python/semantic_token_rules.json b/crates/grammars/src/python/semantic_token_rules.json
similarity index 100%
rename from crates/languages/src/python/semantic_token_rules.json
rename to crates/grammars/src/python/semantic_token_rules.json
diff --git a/crates/languages/src/python/textobjects.scm b/crates/grammars/src/python/textobjects.scm
similarity index 100%
rename from crates/languages/src/python/textobjects.scm
rename to crates/grammars/src/python/textobjects.scm
diff --git a/crates/languages/src/regex/brackets.scm b/crates/grammars/src/regex/brackets.scm
similarity index 100%
rename from crates/languages/src/regex/brackets.scm
rename to crates/grammars/src/regex/brackets.scm
diff --git a/crates/languages/src/regex/config.toml b/crates/grammars/src/regex/config.toml
similarity index 100%
rename from crates/languages/src/regex/config.toml
rename to crates/grammars/src/regex/config.toml
diff --git a/crates/languages/src/regex/highlights.scm b/crates/grammars/src/regex/highlights.scm
similarity index 100%
rename from crates/languages/src/regex/highlights.scm
rename to crates/grammars/src/regex/highlights.scm
diff --git a/crates/languages/src/rust/brackets.scm b/crates/grammars/src/rust/brackets.scm
similarity index 100%
rename from crates/languages/src/rust/brackets.scm
rename to crates/grammars/src/rust/brackets.scm
diff --git a/crates/languages/src/rust/config.toml b/crates/grammars/src/rust/config.toml
similarity index 100%
rename from crates/languages/src/rust/config.toml
rename to crates/grammars/src/rust/config.toml
diff --git a/crates/languages/src/rust/debugger.scm b/crates/grammars/src/rust/debugger.scm
similarity index 100%
rename from crates/languages/src/rust/debugger.scm
rename to crates/grammars/src/rust/debugger.scm
diff --git a/crates/languages/src/rust/highlights.scm b/crates/grammars/src/rust/highlights.scm
similarity index 100%
rename from crates/languages/src/rust/highlights.scm
rename to crates/grammars/src/rust/highlights.scm
diff --git a/crates/languages/src/rust/imports.scm b/crates/grammars/src/rust/imports.scm
similarity index 100%
rename from crates/languages/src/rust/imports.scm
rename to crates/grammars/src/rust/imports.scm
diff --git a/crates/languages/src/rust/indents.scm b/crates/grammars/src/rust/indents.scm
similarity index 100%
rename from crates/languages/src/rust/indents.scm
rename to crates/grammars/src/rust/indents.scm
diff --git a/crates/languages/src/rust/injections.scm b/crates/grammars/src/rust/injections.scm
similarity index 100%
rename from crates/languages/src/rust/injections.scm
rename to crates/grammars/src/rust/injections.scm
diff --git a/crates/languages/src/rust/outline.scm b/crates/grammars/src/rust/outline.scm
similarity index 100%
rename from crates/languages/src/rust/outline.scm
rename to crates/grammars/src/rust/outline.scm
diff --git a/crates/languages/src/rust/overrides.scm b/crates/grammars/src/rust/overrides.scm
similarity index 100%
rename from crates/languages/src/rust/overrides.scm
rename to crates/grammars/src/rust/overrides.scm
diff --git a/crates/languages/src/rust/runnables.scm b/crates/grammars/src/rust/runnables.scm
similarity index 100%
rename from crates/languages/src/rust/runnables.scm
rename to crates/grammars/src/rust/runnables.scm
diff --git a/crates/languages/src/rust/semantic_token_rules.json b/crates/grammars/src/rust/semantic_token_rules.json
similarity index 100%
rename from crates/languages/src/rust/semantic_token_rules.json
rename to crates/grammars/src/rust/semantic_token_rules.json
diff --git a/crates/languages/src/rust/textobjects.scm b/crates/grammars/src/rust/textobjects.scm
similarity index 100%
rename from crates/languages/src/rust/textobjects.scm
rename to crates/grammars/src/rust/textobjects.scm
diff --git a/crates/languages/src/tsx/brackets.scm b/crates/grammars/src/tsx/brackets.scm
similarity index 100%
rename from crates/languages/src/tsx/brackets.scm
rename to crates/grammars/src/tsx/brackets.scm
diff --git a/crates/languages/src/tsx/config.toml b/crates/grammars/src/tsx/config.toml
similarity index 100%
rename from crates/languages/src/tsx/config.toml
rename to crates/grammars/src/tsx/config.toml
diff --git a/crates/languages/src/tsx/debugger.scm b/crates/grammars/src/tsx/debugger.scm
similarity index 100%
rename from crates/languages/src/tsx/debugger.scm
rename to crates/grammars/src/tsx/debugger.scm
diff --git a/crates/languages/src/tsx/highlights.scm b/crates/grammars/src/tsx/highlights.scm
similarity index 100%
rename from crates/languages/src/tsx/highlights.scm
rename to crates/grammars/src/tsx/highlights.scm
diff --git a/crates/languages/src/tsx/imports.scm b/crates/grammars/src/tsx/imports.scm
similarity index 100%
rename from crates/languages/src/tsx/imports.scm
rename to crates/grammars/src/tsx/imports.scm
diff --git a/crates/languages/src/tsx/indents.scm b/crates/grammars/src/tsx/indents.scm
similarity index 100%
rename from crates/languages/src/tsx/indents.scm
rename to crates/grammars/src/tsx/indents.scm
diff --git a/crates/languages/src/tsx/injections.scm b/crates/grammars/src/tsx/injections.scm
similarity index 100%
rename from crates/languages/src/tsx/injections.scm
rename to crates/grammars/src/tsx/injections.scm
diff --git a/crates/languages/src/tsx/outline.scm b/crates/grammars/src/tsx/outline.scm
similarity index 100%
rename from crates/languages/src/tsx/outline.scm
rename to crates/grammars/src/tsx/outline.scm
diff --git a/crates/languages/src/tsx/overrides.scm b/crates/grammars/src/tsx/overrides.scm
similarity index 100%
rename from crates/languages/src/tsx/overrides.scm
rename to crates/grammars/src/tsx/overrides.scm
diff --git a/crates/languages/src/tsx/runnables.scm b/crates/grammars/src/tsx/runnables.scm
similarity index 100%
rename from crates/languages/src/tsx/runnables.scm
rename to crates/grammars/src/tsx/runnables.scm
diff --git a/crates/languages/src/tsx/textobjects.scm b/crates/grammars/src/tsx/textobjects.scm
similarity index 100%
rename from crates/languages/src/tsx/textobjects.scm
rename to crates/grammars/src/tsx/textobjects.scm
diff --git a/crates/languages/src/typescript/brackets.scm b/crates/grammars/src/typescript/brackets.scm
similarity index 100%
rename from crates/languages/src/typescript/brackets.scm
rename to crates/grammars/src/typescript/brackets.scm
diff --git a/crates/languages/src/typescript/config.toml b/crates/grammars/src/typescript/config.toml
similarity index 100%
rename from crates/languages/src/typescript/config.toml
rename to crates/grammars/src/typescript/config.toml
diff --git a/crates/languages/src/typescript/debugger.scm b/crates/grammars/src/typescript/debugger.scm
similarity index 100%
rename from crates/languages/src/typescript/debugger.scm
rename to crates/grammars/src/typescript/debugger.scm
diff --git a/crates/languages/src/typescript/highlights.scm b/crates/grammars/src/typescript/highlights.scm
similarity index 100%
rename from crates/languages/src/typescript/highlights.scm
rename to crates/grammars/src/typescript/highlights.scm
diff --git a/crates/languages/src/typescript/imports.scm b/crates/grammars/src/typescript/imports.scm
similarity index 100%
rename from crates/languages/src/typescript/imports.scm
rename to crates/grammars/src/typescript/imports.scm
diff --git a/crates/languages/src/typescript/indents.scm b/crates/grammars/src/typescript/indents.scm
similarity index 100%
rename from crates/languages/src/typescript/indents.scm
rename to crates/grammars/src/typescript/indents.scm
diff --git a/crates/languages/src/typescript/injections.scm b/crates/grammars/src/typescript/injections.scm
similarity index 100%
rename from crates/languages/src/typescript/injections.scm
rename to crates/grammars/src/typescript/injections.scm
diff --git a/crates/languages/src/typescript/outline.scm b/crates/grammars/src/typescript/outline.scm
similarity index 100%
rename from crates/languages/src/typescript/outline.scm
rename to crates/grammars/src/typescript/outline.scm
diff --git a/crates/languages/src/typescript/overrides.scm b/crates/grammars/src/typescript/overrides.scm
similarity index 100%
rename from crates/languages/src/typescript/overrides.scm
rename to crates/grammars/src/typescript/overrides.scm
diff --git a/crates/languages/src/typescript/runnables.scm b/crates/grammars/src/typescript/runnables.scm
similarity index 100%
rename from crates/languages/src/typescript/runnables.scm
rename to crates/grammars/src/typescript/runnables.scm
diff --git a/crates/languages/src/typescript/textobjects.scm b/crates/grammars/src/typescript/textobjects.scm
similarity index 100%
rename from crates/languages/src/typescript/textobjects.scm
rename to crates/grammars/src/typescript/textobjects.scm
diff --git a/crates/languages/src/yaml/brackets.scm b/crates/grammars/src/yaml/brackets.scm
similarity index 100%
rename from crates/languages/src/yaml/brackets.scm
rename to crates/grammars/src/yaml/brackets.scm
diff --git a/crates/languages/src/yaml/config.toml b/crates/grammars/src/yaml/config.toml
similarity index 100%
rename from crates/languages/src/yaml/config.toml
rename to crates/grammars/src/yaml/config.toml
diff --git a/crates/languages/src/yaml/highlights.scm b/crates/grammars/src/yaml/highlights.scm
similarity index 100%
rename from crates/languages/src/yaml/highlights.scm
rename to crates/grammars/src/yaml/highlights.scm
diff --git a/crates/languages/src/yaml/injections.scm b/crates/grammars/src/yaml/injections.scm
similarity index 100%
rename from crates/languages/src/yaml/injections.scm
rename to crates/grammars/src/yaml/injections.scm
diff --git a/crates/languages/src/yaml/outline.scm b/crates/grammars/src/yaml/outline.scm
similarity index 100%
rename from crates/languages/src/yaml/outline.scm
rename to crates/grammars/src/yaml/outline.scm
diff --git a/crates/languages/src/yaml/overrides.scm b/crates/grammars/src/yaml/overrides.scm
similarity index 100%
rename from crates/languages/src/yaml/overrides.scm
rename to crates/grammars/src/yaml/overrides.scm
diff --git a/crates/languages/src/yaml/redactions.scm b/crates/grammars/src/yaml/redactions.scm
similarity index 100%
rename from crates/languages/src/yaml/redactions.scm
rename to crates/grammars/src/yaml/redactions.scm
diff --git a/crates/languages/src/yaml/textobjects.scm b/crates/grammars/src/yaml/textobjects.scm
similarity index 100%
rename from crates/languages/src/yaml/textobjects.scm
rename to crates/grammars/src/yaml/textobjects.scm
diff --git a/crates/languages/src/zed-keybind-context/brackets.scm b/crates/grammars/src/zed-keybind-context/brackets.scm
similarity index 100%
rename from crates/languages/src/zed-keybind-context/brackets.scm
rename to crates/grammars/src/zed-keybind-context/brackets.scm
diff --git a/crates/languages/src/zed-keybind-context/config.toml b/crates/grammars/src/zed-keybind-context/config.toml
similarity index 100%
rename from crates/languages/src/zed-keybind-context/config.toml
rename to crates/grammars/src/zed-keybind-context/config.toml
diff --git a/crates/languages/src/zed-keybind-context/highlights.scm b/crates/grammars/src/zed-keybind-context/highlights.scm
similarity index 100%
rename from crates/languages/src/zed-keybind-context/highlights.scm
rename to crates/grammars/src/zed-keybind-context/highlights.scm
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index 625b51f9c1b9f5cbf04f4474b72d08557542352f..89932125c1bfbc05202038c1abac2a6380e19e93 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -233,11 +233,6 @@ pub enum IconName {
Star,
StarFilled,
Stop,
- SweepAi,
- SweepAiDisabled,
- SweepAiDown,
- SweepAiError,
- SweepAiUp,
Tab,
Terminal,
TerminalAlt,
@@ -249,6 +244,8 @@ pub enum IconName {
ThreadFromSummary,
ThreadsSidebarLeftClosed,
ThreadsSidebarLeftOpen,
+ ThreadsSidebarRightClosed,
+ ThreadsSidebarRightOpen,
ThumbsDown,
ThumbsUp,
TodoComplete,
diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs
index a6aece93d519fbb85cee61b75cad5ffbff77cbc6..9f10967e72a6dbde8c97b42c465945386709d3ed 100644
--- a/crates/keymap_editor/src/keymap_editor.rs
+++ b/crates/keymap_editor/src/keymap_editor.rs
@@ -24,6 +24,7 @@ use gpui::{
actions, anchored, deferred, div,
};
use language::{Language, LanguageConfig, ToOffset as _};
+
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{CompletionDisplayOptions, Project};
use settings::{
@@ -2405,9 +2406,10 @@ impl RenderOnce for SyntaxHighlightedText {
}
let mut run_style = text_style.clone();
- if let Some(highlight_style) = highlight_id.style(syntax_theme) {
+ if let Some(highlight_style) = syntax_theme.get(highlight_id).cloned() {
run_style = run_style.highlight(highlight_style);
}
+
// add the highlighted range
runs.push(run_style.to_run(highlight_range.len()));
offset = highlight_range.end;
diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml
index 37c19172f7c48743e1436ba41e30d0c7ebf99d1d..cec6421c059335c62db9f8db4485eb939d46db01 100644
--- a/crates/language/Cargo.toml
+++ b/crates/language/Cargo.toml
@@ -40,6 +40,7 @@ globset.workspace = true
gpui.workspace = true
http_client.workspace = true
imara-diff.workspace = true
+language_core.workspace = true
itertools.workspace = true
log.workspace = true
lsp.workspace = true
@@ -48,7 +49,6 @@ postage.workspace = true
rand = { workspace = true, optional = true }
regex.workspace = true
rpc.workspace = true
-schemars.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
@@ -101,6 +101,11 @@ toml.workspace = true
unindent.workspace = true
util = { workspace = true, features = ["test-support"] }
zlog.workspace = true
+criterion.workspace = true
+
+[[bench]]
+name = "highlight_map"
+harness = false
[package.metadata.cargo-machete]
ignored = ["tracing"]
diff --git a/crates/language/benches/highlight_map.rs b/crates/language/benches/highlight_map.rs
new file mode 100644
index 0000000000000000000000000000000000000000..678bd08a8db40b588c6f2716a14b04048fb46d23
--- /dev/null
+++ b/crates/language/benches/highlight_map.rs
@@ -0,0 +1,144 @@
+use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
+use gpui::rgba;
+use language::build_highlight_map;
+use theme::SyntaxTheme;
+
+fn syntax_theme(highlight_names: &[&str]) -> SyntaxTheme {
+ SyntaxTheme::new(highlight_names.iter().enumerate().map(|(i, name)| {
+ let r = ((i * 37) % 256) as u8;
+ let g = ((i * 53) % 256) as u8;
+ let b = ((i * 71) % 256) as u8;
+ let color = rgba(u32::from_be_bytes([r, g, b, 0xff]));
+ (name.to_string(), color.into())
+ }))
+}
+
+static SMALL_THEME_KEYS: &[&str] = &[
+ "comment", "function", "keyword", "string", "type", "variable",
+];
+
+static LARGE_THEME_KEYS: &[&str] = &[
+ "attribute",
+ "boolean",
+ "comment",
+ "comment.doc",
+ "constant",
+ "constant.builtin",
+ "constructor",
+ "embedded",
+ "emphasis",
+ "emphasis.strong",
+ "function",
+ "function.builtin",
+ "function.method",
+ "function.method.builtin",
+ "function.special.definition",
+ "keyword",
+ "keyword.control",
+ "keyword.control.conditional",
+ "keyword.control.import",
+ "keyword.control.repeat",
+ "keyword.control.return",
+ "keyword.modifier",
+ "keyword.operator",
+ "label",
+ "link_text",
+ "link_uri",
+ "number",
+ "operator",
+ "property",
+ "punctuation",
+ "punctuation.bracket",
+ "punctuation.delimiter",
+ "punctuation.list_marker",
+ "punctuation.special",
+ "string",
+ "string.escape",
+ "string.regex",
+ "string.special",
+ "string.special.symbol",
+ "tag",
+ "text.literal",
+ "title",
+ "type",
+ "type.builtin",
+ "type.super",
+ "variable",
+ "variable.builtin",
+ "variable.member",
+ "variable.parameter",
+ "variable.special",
+];
+
+static SMALL_CAPTURE_NAMES: &[&str] = &[
+ "function",
+ "keyword",
+ "string.escape",
+ "type.builtin",
+ "variable.builtin",
+];
+
+static LARGE_CAPTURE_NAMES: &[&str] = &[
+ "attribute",
+ "boolean",
+ "comment",
+ "comment.doc",
+ "constant",
+ "constant.builtin",
+ "constructor",
+ "function",
+ "function.builtin",
+ "function.method",
+ "keyword",
+ "keyword.control",
+ "keyword.control.conditional",
+ "keyword.control.import",
+ "keyword.modifier",
+ "keyword.operator",
+ "label",
+ "number",
+ "operator",
+ "property",
+ "punctuation.bracket",
+ "punctuation.delimiter",
+ "punctuation.special",
+ "string",
+ "string.escape",
+ "string.regex",
+ "string.special",
+ "tag",
+ "type",
+ "type.builtin",
+ "variable",
+ "variable.builtin",
+ "variable.member",
+ "variable.parameter",
+];
+
+fn bench_build_highlight_map(c: &mut Criterion) {
+ let mut group = c.benchmark_group("build_highlight_map");
+
+ for (capture_label, capture_names) in [
+ ("small_captures", SMALL_CAPTURE_NAMES as &[&str]),
+ ("large_captures", LARGE_CAPTURE_NAMES as &[&str]),
+ ] {
+ for (theme_label, theme_keys) in [
+ ("small_theme", SMALL_THEME_KEYS as &[&str]),
+ ("large_theme", LARGE_THEME_KEYS as &[&str]),
+ ] {
+ let theme = syntax_theme(theme_keys);
+ group.bench_with_input(
+ BenchmarkId::new(capture_label, theme_label),
+ &(capture_names, &theme),
+ |b, (capture_names, theme)| {
+ b.iter(|| build_highlight_map(black_box(capture_names), black_box(theme)));
+ },
+ );
+ }
+ }
+
+ group.finish();
+}
+
+criterion_group!(benches, bench_build_highlight_map);
+criterion_main!(benches);
diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs
index 41aaf5b162d9fc231d5a5f37d20b9b79cf23564b..013025de87ad3957f9ac8d8c58f638baeac1448c 100644
--- a/crates/language/src/buffer.rs
+++ b/crates/language/src/buffer.rs
@@ -16,11 +16,10 @@ use crate::{
unified_diff_with_offsets,
};
pub use crate::{
- Grammar, Language, LanguageRegistry,
- diagnostic_set::DiagnosticSet,
- highlight_map::{HighlightId, HighlightMap},
+ Grammar, HighlightId, HighlightMap, Language, LanguageRegistry, diagnostic_set::DiagnosticSet,
proto,
};
+
use anyhow::{Context as _, Result};
use clock::Lamport;
pub use clock::ReplicaId;
@@ -33,10 +32,8 @@ use gpui::{
Task, TextStyle,
};
-use lsp::{LanguageServerId, NumberOrString};
+use lsp::LanguageServerId;
use parking_lot::Mutex;
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
use settings::WorktreeId;
use smallvec::SmallVec;
use smol::future::yield_now;
@@ -252,57 +249,6 @@ struct SelectionSet {
lamport_timestamp: clock::Lamport,
}
-/// A diagnostic associated with a certain range of a buffer.
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub struct Diagnostic {
- /// The name of the service that produced this diagnostic.
- pub source: Option,
- /// The ID provided by the dynamic registration that produced this diagnostic.
- pub registration_id: Option,
- /// A machine-readable code that identifies this diagnostic.
- pub code: Option,
- pub code_description: Option,
- /// Whether this diagnostic is a hint, warning, or error.
- pub severity: DiagnosticSeverity,
- /// The human-readable message associated with this diagnostic.
- pub message: String,
- /// The human-readable message (in markdown format)
- pub markdown: Option,
- /// An id that identifies the group to which this diagnostic belongs.
- ///
- /// When a language server produces a diagnostic with
- /// one or more associated diagnostics, those diagnostics are all
- /// assigned a single group ID.
- pub group_id: usize,
- /// Whether this diagnostic is the primary diagnostic for its group.
- ///
- /// In a given group, the primary diagnostic is the top-level diagnostic
- /// returned by the language server. The non-primary diagnostics are the
- /// associated diagnostics.
- pub is_primary: bool,
- /// Whether this diagnostic is considered to originate from an analysis of
- /// files on disk, as opposed to any unsaved buffer contents. This is a
- /// property of a given diagnostic source, and is configured for a given
- /// language server via the [`LspAdapter::disk_based_diagnostic_sources`](crate::LspAdapter::disk_based_diagnostic_sources) method
- /// for the language server.
- pub is_disk_based: bool,
- /// Whether this diagnostic marks unnecessary code.
- pub is_unnecessary: bool,
- /// Quick separation of diagnostics groups based by their source.
- pub source_kind: DiagnosticSourceKind,
- /// Data from language server that produced this diagnostic. Passed back to the LS when we request code actions for this diagnostic.
- pub data: Option,
- /// Whether to underline the corresponding text range in the editor.
- pub underline: bool,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
-pub enum DiagnosticSourceKind {
- Pulled,
- Pushed,
- Other,
-}
-
/// An operation used to synchronize this buffer with its other replicas.
#[derive(Clone, Debug, PartialEq)]
pub enum Operation {
@@ -749,7 +695,7 @@ impl HighlightedTextBuilder {
if let Some(highlight_style) = chunk
.syntax_highlight_id
- .and_then(|id| id.style(syntax_theme))
+ .and_then(|id| syntax_theme.get(id).cloned())
{
let highlight_style = override_style.map_or(highlight_style, |override_style| {
highlight_style.highlight(override_style)
@@ -4551,7 +4497,8 @@ impl BufferSnapshot {
let style = chunk
.syntax_highlight_id
.zip(theme)
- .and_then(|(highlight, theme)| highlight.style(theme));
+ .and_then(|(highlight, theme)| theme.get(highlight).cloned());
+
if let Some(style) = style {
let start = text.len();
let end = start + chunk.text.len();
@@ -5836,27 +5783,6 @@ impl operation_queue::Operation for Operation {
}
}
-impl Default for Diagnostic {
- fn default() -> Self {
- Self {
- source: Default::default(),
- source_kind: DiagnosticSourceKind::Other,
- code: None,
- code_description: None,
- severity: DiagnosticSeverity::ERROR,
- message: Default::default(),
- markdown: None,
- group_id: 0,
- is_primary: false,
- is_disk_based: false,
- is_unnecessary: false,
- underline: true,
- data: None,
- registration_id: None,
- }
- }
-}
-
impl IndentSize {
/// Returns an [`IndentSize`] representing the given spaces.
pub fn spaces(len: u32) -> Self {
diff --git a/crates/language/src/diagnostic.rs b/crates/language/src/diagnostic.rs
new file mode 100644
index 0000000000000000000000000000000000000000..951feec0da18582b56b361797efc0b346e7b2a04
--- /dev/null
+++ b/crates/language/src/diagnostic.rs
@@ -0,0 +1 @@
+pub use language_core::diagnostic::{Diagnostic, DiagnosticSourceKind};
diff --git a/crates/language/src/highlight_map.rs b/crates/language/src/highlight_map.rs
deleted file mode 100644
index ed9eb5d11d7bc4b156dc9bd660fb10a485129c3d..0000000000000000000000000000000000000000
--- a/crates/language/src/highlight_map.rs
+++ /dev/null
@@ -1,114 +0,0 @@
-use gpui::HighlightStyle;
-use std::sync::Arc;
-use theme::SyntaxTheme;
-
-#[derive(Clone, Debug)]
-pub struct HighlightMap(Arc<[HighlightId]>);
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub struct HighlightId(pub u32);
-
-const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);
-
-impl HighlightMap {
- pub(crate) fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self {
- // For each capture name in the highlight query, find the longest
- // key in the theme's syntax styles that matches all of the
- // dot-separated components of the capture name.
- HighlightMap(
- capture_names
- .iter()
- .map(|capture_name| {
- theme
- .highlights
- .iter()
- .enumerate()
- .filter_map(|(i, (key, _))| {
- let mut len = 0;
- let capture_parts = capture_name.split('.');
- for key_part in key.split('.') {
- if capture_parts.clone().any(|part| part == key_part) {
- len += 1;
- } else {
- return None;
- }
- }
- Some((i, len))
- })
- .max_by_key(|(_, len)| *len)
- .map_or(DEFAULT_SYNTAX_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32))
- })
- .collect(),
- )
- }
-
- pub fn get(&self, capture_id: u32) -> HighlightId {
- self.0
- .get(capture_id as usize)
- .copied()
- .unwrap_or(DEFAULT_SYNTAX_HIGHLIGHT_ID)
- }
-}
-
-impl HighlightId {
- pub const TABSTOP_INSERT_ID: HighlightId = HighlightId(u32::MAX - 1);
- pub const TABSTOP_REPLACE_ID: HighlightId = HighlightId(u32::MAX - 2);
-
- pub(crate) fn is_default(&self) -> bool {
- *self == DEFAULT_SYNTAX_HIGHLIGHT_ID
- }
-
- pub fn style(&self, theme: &SyntaxTheme) -> Option {
- theme.highlights.get(self.0 as usize).map(|entry| entry.1)
- }
-
- pub fn name<'a>(&self, theme: &'a SyntaxTheme) -> Option<&'a str> {
- theme.highlights.get(self.0 as usize).map(|e| e.0.as_str())
- }
-}
-
-impl Default for HighlightMap {
- fn default() -> Self {
- Self(Arc::new([]))
- }
-}
-
-impl Default for HighlightId {
- fn default() -> Self {
- DEFAULT_SYNTAX_HIGHLIGHT_ID
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use gpui::rgba;
-
- #[test]
- fn test_highlight_map() {
- let theme = SyntaxTheme {
- highlights: [
- ("function", rgba(0x100000ff)),
- ("function.method", rgba(0x200000ff)),
- ("function.async", rgba(0x300000ff)),
- ("variable.builtin.self.rust", rgba(0x400000ff)),
- ("variable.builtin", rgba(0x500000ff)),
- ("variable", rgba(0x600000ff)),
- ]
- .iter()
- .map(|(name, color)| (name.to_string(), (*color).into()))
- .collect(),
- };
-
- let capture_names = &[
- "function.special",
- "function.async.rust",
- "variable.builtin.self",
- ];
-
- let map = HighlightMap::new(capture_names, &theme);
- assert_eq!(map.get(0).name(&theme), Some("function"));
- assert_eq!(map.get(1).name(&theme), Some("function.async"));
- assert_eq!(map.get(2).name(&theme), Some("variable.builtin"));
- }
-}
diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs
index d9c1256290a0b47a198b700c2a5710c0b1df1795..469759a24c74b8d1349fbc3a66d5037d8ef8587d 100644
--- a/crates/language/src/language.rs
+++ b/crates/language/src/language.rs
@@ -7,9 +7,10 @@
//!
//! Notably we do *not* assign a single language to a single file; in real world a single file can consist of multiple programming languages - HTML is a good example of that - and `language` crate tends to reflect that status quo in its API.
mod buffer;
+mod diagnostic;
mod diagnostic_set;
-mod highlight_map;
mod language_registry;
+
pub mod language_settings;
mod manifest;
pub mod modeline;
@@ -23,17 +24,30 @@ mod toolchain;
#[cfg(test)]
pub mod buffer_tests;
-use crate::language_settings::SoftWrap;
pub use crate::language_settings::{AutoIndentMode, EditPredictionsMode, IndentGuideSettings};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
-use collections::{HashMap, HashSet, IndexSet};
+use collections::{HashMap, HashSet};
use futures::Future;
use futures::future::LocalBoxFuture;
use futures::lock::OwnedMutexGuard;
-use gpui::{App, AsyncApp, Entity, SharedString};
-pub use highlight_map::HighlightMap;
+use gpui::{App, AsyncApp, Entity};
use http_client::HttpClient;
+
+pub use language_core::highlight_map::{HighlightId, HighlightMap};
+
+pub use language_core::{
+ BlockCommentConfig, BracketPair, BracketPairConfig, BracketPairContent, BracketsConfig,
+ BracketsPatternConfig, CodeLabel, CodeLabelBuilder, DebugVariablesConfig, DebuggerTextObject,
+ DecreaseIndentConfig, Grammar, GrammarId, HighlightsConfig, ImportsConfig, IndentConfig,
+ InjectionConfig, InjectionPatternConfig, JsxTagAutoCloseConfig, LanguageConfig,
+ LanguageConfigOverride, LanguageId, LanguageMatcher, OrderedListConfig, OutlineConfig,
+ Override, OverrideConfig, OverrideEntry, PromptResponseContext, RedactionConfig,
+ RunnableCapture, RunnableConfig, SoftWrap, Symbol, TaskListConfig, TextObject,
+ TextObjectConfig, ToLspPosition, WrapCharactersConfig,
+ auto_indent_using_last_non_empty_line_default, deserialize_regex, deserialize_regex_vec,
+ regex_json_schema, regex_vec_json_schema, serialize_regex,
+};
pub use language_registry::{
LanguageName, LanguageServerStatusUpdate, LoadedLanguage, ServerHealth,
};
@@ -44,13 +58,10 @@ pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQue
pub use modeline::{ModelineSettings, parse_modeline};
use parking_lot::Mutex;
use regex::Regex;
-use schemars::{JsonSchema, SchemaGenerator, json_schema};
use semver::Version;
-use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use serde_json::Value;
use settings::WorktreeId;
use smol::future::FutureExt as _;
-use std::num::NonZeroU32;
use std::{
ffi::OsStr,
fmt::Debug,
@@ -59,10 +70,7 @@ use std::{
ops::{DerefMut, Range},
path::{Path, PathBuf},
str,
- sync::{
- Arc, LazyLock,
- atomic::{AtomicUsize, Ordering::SeqCst},
- },
+ sync::{Arc, LazyLock},
};
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
use task::RunnableTag;
@@ -77,12 +85,12 @@ pub use toolchain::{
LanguageToolchainStore, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister,
ToolchainMetadata, ToolchainScope,
};
-use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime};
+use tree_sitter::{self, QueryCursor, WasmStore, wasmtime};
use util::rel_path::RelPath;
-use util::serde::default_true;
pub use buffer::Operation;
pub use buffer::*;
+pub use diagnostic::{Diagnostic, DiagnosticSourceKind};
pub use diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup};
pub use language_registry::{
AvailableLanguage, BinaryStatus, LanguageNotFound, LanguageQueries, LanguageRegistry,
@@ -96,6 +104,16 @@ pub use syntax_map::{
pub use text::{AnchorRangeExt, LineEnding};
pub use tree_sitter::{Node, Parser, Tree, TreeCursor};
+pub(crate) fn to_settings_soft_wrap(value: language_core::SoftWrap) -> settings::SoftWrap {
+ match value {
+ language_core::SoftWrap::None => settings::SoftWrap::None,
+ language_core::SoftWrap::PreferLine => settings::SoftWrap::PreferLine,
+ language_core::SoftWrap::EditorWidth => settings::SoftWrap::EditorWidth,
+ language_core::SoftWrap::PreferredLineLength => settings::SoftWrap::PreferredLineLength,
+ language_core::SoftWrap::Bounded => settings::SoftWrap::Bounded,
+ }
+}
+
static QUERY_CURSORS: Mutex> = Mutex::new(vec![]);
static PARSERS: Mutex> = Mutex::new(vec![]);
@@ -125,8 +143,6 @@ where
func(cursor.deref_mut())
}
-static NEXT_LANGUAGE_ID: AtomicUsize = AtomicUsize::new(0);
-static NEXT_GRAMMAR_ID: AtomicUsize = AtomicUsize::new(0);
static WASM_ENGINE: LazyLock = LazyLock::new(|| {
wasmtime::Engine::new(&wasmtime::Config::new()).expect("Failed to create Wasmtime engine")
});
@@ -188,26 +204,12 @@ pub static PLAIN_TEXT: LazyLock> = LazyLock::new(|| {
))
});
-/// Types that represent a position in a buffer, and can be converted into
-/// an LSP position, to send to a language server.
-pub trait ToLspPosition {
- /// Converts the value into an LSP position.
- fn to_lsp_position(self) -> lsp::Position;
-}
-
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Location {
pub buffer: Entity,
pub range: Range,
}
-#[derive(Debug, Clone)]
-pub struct Symbol {
- pub name: String,
- pub kind: lsp::SymbolKind,
- pub container_name: Option,
-}
-
type ServerBinaryCache = futures::lock::Mutex |