Compare commits

..

2 Commits

Author SHA1 Message Date
Jakub Konka
3a301afbc6 Enable all debug info for easier profiling 2025-11-10 22:38:40 +01:00
Jakub Konka
78add792c7 project_diff: Load buffers in the background 2025-11-10 22:01:38 +01:00
280 changed files with 3176 additions and 10454 deletions

View File

@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
blank_issues_enabled: false
contact_links:
- name: Feature Request

View File

@@ -56,14 +56,14 @@ jobs:
- id: set-package-name
name: after_release::publish_winget::set_package_name
run: |
if ("${{ github.event.release.prerelease }}" -eq "true") {
$PACKAGE_NAME = "ZedIndustries.Zed.Preview"
} else {
$PACKAGE_NAME = "ZedIndustries.Zed"
}
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT
shell: pwsh
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- name: after_release::publish_winget::winget_releaser
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
with:
@@ -86,19 +86,3 @@ jobs:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
notify_on_failure:
needs:
- rebuild_releases_page
- post_to_discord
- publish_winget
- create_sentry_release
if: failure()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: release::notify_on_failure::notify_slack
run: |-
curl -X POST -H 'Content-type: application/json'\
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
shell: bash -euxo pipefail {0}
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }}

View File

@@ -1,7 +1,6 @@
# Generated from xtask::workflows::cherry_pick
# Rebuild with `cargo xtask workflows`.
name: cherry_pick
run-name: 'cherry_pick to ${{ inputs.channel }} #${{ inputs.pr_number }}'
on:
workflow_dispatch:
inputs:
@@ -17,10 +16,6 @@ on:
description: channel
required: true
type: string
pr_number:
description: pr_number
required: true
type: string
jobs:
run_cherry_pick:
runs-on: namespace-profile-2x4-ubuntu-2404

View File

@@ -15,13 +15,13 @@ jobs:
stale-issue-message: >
Hi there! 👋
We're working to clean up our issue tracker by closing older bugs that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and it will be kept open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, it will close automatically in 14 days.
We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and we will keep it open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, we'll close it in 7 days.
Thanks for your help!
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
days-before-stale: 60
days-before-close: 14
only-issue-types: "Bug,Crash"
days-before-stale: 120
days-before-close: 7
any-of-issue-labels: "bug,panic / crash"
operations-per-run: 1000
ascending: true
enable-statistics: true

View File

@@ -35,9 +35,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::install_hyperfine
run: cargo install hyperfine
shell: bash -euxo pipefail {0}

View File

@@ -57,19 +57,16 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
@@ -205,9 +202,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
@@ -248,9 +242,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
@@ -484,20 +475,6 @@ jobs:
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
notify_on_failure:
needs:
- upload_release_assets
- auto_release_preview
if: failure()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: release::notify_on_failure::notify_slack
run: |-
curl -X POST -H 'Content-type: application/json'\
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
shell: bash -euxo pipefail {0}
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -93,9 +93,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
@@ -143,9 +140,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
@@ -493,21 +487,3 @@ jobs:
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
timeout-minutes: 60
notify_on_failure:
needs:
- bundle_linux_aarch64
- bundle_linux_x86_64
- bundle_mac_aarch64
- bundle_mac_x86_64
- bundle_windows_aarch64
- bundle_windows_x86_64
if: failure()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: release::notify_on_failure::notify_slack
run: |-
curl -X POST -H 'Content-type: application/json'\
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
shell: bash -euxo pipefail {0}
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }}

View File

@@ -6,21 +6,24 @@ env:
CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1'
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_EVAL_TELEMETRY: '1'
MODEL_NAME: ${{ inputs.model_name }}
on:
workflow_dispatch:
inputs:
model_name:
description: model_name
required: true
type: string
pull_request:
types:
- synchronize
- reopened
- labeled
branches:
- '**'
schedule:
- cron: 0 0 * * *
workflow_dispatch: {}
jobs:
agent_evals:
if: |
github.repository_owner == 'zed-industries' &&
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
@@ -37,9 +40,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
@@ -49,19 +49,14 @@ jobs:
run: cargo build --package=eval
shell: bash -euxo pipefail {0}
- name: run_agent_evals::agent_evals::run_eval
run: cargo run --package=eval -- --repetitions=8 --concurrency=1 --model "${MODEL_NAME}"
run: cargo run --package=eval -- --repetitions=8 --concurrency=1
shell: bash -euxo pipefail {0}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
timeout-minutes: 600
timeout-minutes: 60
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -34,9 +34,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
@@ -77,9 +74,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}

View File

@@ -1,69 +0,0 @@
# Generated from xtask::workflows::run_cron_unit_evals
# Rebuild with `cargo xtask workflows`.
name: run_cron_unit_evals
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1'
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
on:
schedule:
- cron: 47 1 * * 2
workflow_dispatch: {}
jobs:
cron_unit_evals:
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
shell: bash -euxo pipefail {0}
- name: ./script/run-unit-evals
run: ./script/run-unit-evals
shell: bash -euxo pipefail {0}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
- name: run_agent_evals::cron_unit_evals::send_failure_to_slack
if: ${{ failure() }}
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
with:
method: chat.postMessage
token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}
payload: |
channel: C04UDRNNJFQ
text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -143,19 +143,16 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
@@ -235,9 +232,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
@@ -269,19 +263,16 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: cargo build -p collab
run: cargo build -p collab
shell: bash -euxo pipefail {0}
@@ -357,9 +348,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: run_tests::check_docs::install_mdbook
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08
with:

View File

@@ -1,26 +1,17 @@
# Generated from xtask::workflows::run_unit_evals
# Generated from xtask::workflows::run_agent_evals
# Rebuild with `cargo xtask workflows`.
name: run_unit_evals
name: run_agent_evals
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1'
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_EVAL_TELEMETRY: '1'
MODEL_NAME: ${{ inputs.model_name }}
on:
workflow_dispatch:
inputs:
model_name:
description: model_name
required: true
type: string
commit_sha:
description: commit_sha
required: true
type: string
schedule:
- cron: 47 1 * * 2
workflow_dispatch: {}
jobs:
run_unit_evals:
unit_evals:
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
@@ -42,9 +33,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
@@ -56,10 +44,15 @@ jobs:
shell: bash -euxo pipefail {0}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
UNIT_EVAL_COMMIT: ${{ inputs.commit_sha }}
- name: run_agent_evals::unit_evals::send_failure_to_slack
if: ${{ failure() }}
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
with:
method: chat.postMessage
token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}
payload: |
channel: C04UDRNNJFQ
text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
- name: steps::cleanup_cargo_config
if: always()
run: |

43
Cargo.lock generated
View File

@@ -96,7 +96,6 @@ dependencies = [
"auto_update",
"editor",
"extension_host",
"fs",
"futures 0.3.31",
"gpui",
"language",
@@ -1331,14 +1330,10 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"clock",
"ctor",
"db",
"futures 0.3.31",
"gpui",
"http_client",
"log",
"parking_lot",
"paths",
"release_channel",
"serde",
@@ -1349,7 +1344,6 @@ dependencies = [
"util",
"which 6.0.3",
"workspace",
"zlog",
]
[[package]]
@@ -6248,7 +6242,7 @@ dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"spin 0.9.8",
"spin",
]
[[package]]
@@ -6359,9 +6353,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "fork"
version = "0.4.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30268f1eefccc9d72f43692e8b89e659aeb52e84016c3b32b6e7e9f1c8f38f94"
checksum = "05dc8b302e04a1c27f4fe694439ef0f29779ca4edc205b7b58f00db04e29656d"
dependencies = [
"libc",
]
@@ -7287,7 +7281,6 @@ dependencies = [
"calloop",
"calloop-wayland-source",
"cbindgen",
"circular-buffer",
"cocoa 0.26.0",
"cocoa-foundation 0.2.0",
"collections",
@@ -7343,7 +7336,6 @@ dependencies = [
"slotmap",
"smallvec",
"smol",
"spin 0.10.0",
"stacksafe",
"strum 0.27.2",
"sum_tree",
@@ -7807,7 +7799,6 @@ dependencies = [
"parking_lot",
"serde",
"serde_json",
"serde_urlencoded",
"sha2",
"tempfile",
"url",
@@ -9074,7 +9065,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin 0.9.8",
"spin",
]
[[package]]
@@ -10016,18 +10007,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniprofiler_ui"
version = "0.1.0"
dependencies = [
"gpui",
"serde_json",
"smol",
"util",
"workspace",
"zed_actions",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -13092,7 +13071,6 @@ dependencies = [
"settings",
"smallvec",
"telemetry",
"tempfile",
"theme",
"ui",
"util",
@@ -15868,15 +15846,6 @@ dependencies = [
"lock_api",
]
[[package]]
name = "spin"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
dependencies = [
"lock_api",
]
[[package]]
name = "spirv"
version = "0.3.0+sdk-1.3.268.0"
@@ -21170,7 +21139,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.213.8"
version = "0.213.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -21188,7 +21157,6 @@ dependencies = [
"breadcrumbs",
"call",
"channel",
"chrono",
"clap",
"cli",
"client",
@@ -21246,7 +21214,6 @@ dependencies = [
"menu",
"migrator",
"mimalloc",
"miniprofiler_ui",
"nc",
"nix 0.29.0",
"node_runtime",

View File

@@ -110,7 +110,6 @@ members = [
"crates/menu",
"crates/migrator",
"crates/mistral",
"crates/miniprofiler_ui",
"crates/multi_buffer",
"crates/nc",
"crates/net",
@@ -342,7 +341,6 @@ menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" }
multi_buffer = { path = "crates/multi_buffer" }
miniprofiler_ui = { path = "crates/miniprofiler_ui" }
nc = { path = "crates/nc" }
net = { path = "crates/net" }
node_runtime = { path = "crates/node_runtime" }
@@ -506,7 +504,7 @@ emojis = "0.6.1"
env_logger = "0.11"
exec = "0.3.1"
fancy-regex = "0.14.0"
fork = "0.4.0"
fork = "0.2.0"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
@@ -842,7 +840,7 @@ ui_input = { codegen-units = 1 }
zed_actions = { codegen-units = 1 }
[profile.release]
debug = "limited"
debug = "full"
lto = "thin"
codegen-units = 1

View File

@@ -1,7 +1,7 @@
# Zed
[![Zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev)
[![CI](https://github.com/zed-industries/zed/actions/workflows/run_tests.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/run_tests.yml)
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00156 10.3996C9.32705 10.3996 10.4016 9.32509 10.4016 7.99961C10.4016 6.67413 9.32705 5.59961 8.00156 5.59961C6.67608 5.59961 5.60156 6.67413 5.60156 7.99961C5.60156 9.32509 6.67608 10.3996 8.00156 10.3996Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.4 5.6V8.6C10.4 9.07739 10.5896 9.53523 10.9272 9.8728C11.2648 10.2104 11.7226 10.4 12.2 10.4C12.6774 10.4 13.1352 10.2104 13.4728 9.8728C13.8104 9.53523 14 9.07739 14 8.6V8C14 6.64839 13.5436 5.33636 12.7048 4.27651C11.8661 3.21665 10.694 2.47105 9.37852 2.16051C8.06306 1.84997 6.68129 1.99269 5.45707 2.56554C4.23285 3.13838 3.23791 4.1078 2.63344 5.31672C2.02898 6.52565 1.85041 7.90325 2.12667 9.22633C2.40292 10.5494 3.11782 11.7405 4.15552 12.6065C5.19323 13.4726 6.49295 13.9629 7.84411 13.998C9.19527 14.0331 10.5187 13.611 11.6 12.8" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -421,6 +421,12 @@
"ctrl-[": "editor::Cancel"
}
},
{
"context": "vim_mode == helix_select && !menu",
"bindings": {
"escape": "vim::SwitchToHelixNormalMode"
}
},
{
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
"bindings": {

View File

@@ -605,10 +605,6 @@
// to both the horizontal and vertical delta values while scrolling. Fast scrolling
// happens when a user holds the alt or option key while scrolling.
"fast_scroll_sensitivity": 4.0,
"sticky_scroll": {
// Whether to stick scopes to the top of the editor.
"enabled": false
},
"relative_line_numbers": "disabled",
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
"search_wrap": true,
@@ -616,13 +612,9 @@
"search": {
// Whether to show the project search button in the status bar.
"button": true,
// Whether to only match on whole words.
"whole_word": false,
// Whether to match case sensitively.
"case_sensitive": false,
// Whether to include gitignored files in search results.
"include_ignored": false,
// Whether to interpret the search query as a regular expression.
"regex": false,
// Whether to center the cursor on each search match when navigating.
"center_on_match": false
@@ -748,15 +740,8 @@
"hide_root": false,
// Whether to hide the hidden entries in the project panel.
"hide_hidden": false,
// Settings for automatically opening files.
"auto_open": {
// Whether to automatically open newly created files in the editor.
"on_create": true,
// Whether to automatically open files after pasting or duplicating them.
"on_paste": true,
// Whether to automatically open files dropped from external sources.
"on_drop": true
}
// Whether to automatically open files when pasting them in the project panel.
"open_file_on_paste": true
},
"outline_panel": {
// Whether to show the outline panel button in the status bar

View File

@@ -1866,14 +1866,10 @@ impl AcpThread {
.checkpoint
.as_ref()
.map(|c| c.git_checkpoint.clone());
// Cancel any in-progress generation before restoring
let cancel_task = self.cancel(cx);
let rewind = self.rewind(id.clone(), cx);
let git_store = self.project.read(cx).git_store().clone();
cx.spawn(async move |_, cx| {
cancel_task.await;
rewind.await?;
if let Some(checkpoint) = checkpoint {
git_store
@@ -1898,25 +1894,9 @@ impl AcpThread {
cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
this.update(cx, |this, cx| {
if let Some((ix, _)) = this.user_message_mut(&id) {
// Collect all terminals from entries that will be removed
let terminals_to_remove: Vec<acp::TerminalId> = this.entries[ix..]
.iter()
.flat_map(|entry| entry.terminals())
.filter_map(|terminal| terminal.read(cx).id().clone().into())
.collect();
let range = ix..this.entries.len();
this.entries.truncate(ix);
cx.emit(AcpThreadEvent::EntriesRemoved(range));
// Kill and remove the terminals
for terminal_id in terminals_to_remove {
if let Some(terminal) = this.terminals.remove(&terminal_id) {
terminal.update(cx, |terminal, cx| {
terminal.kill(cx);
});
}
}
}
this.action_log().update(cx, |action_log, cx| {
action_log.reject_all_edits(Some(telemetry), cx)
@@ -3823,314 +3803,4 @@ mod tests {
}
});
}
/// Tests that restoring a checkpoint properly cleans up terminals that were
/// created after that checkpoint, and cancels any in-progress generation.
///
/// Reproduces issue #35142: When a checkpoint is restored, any terminal processes
/// that were started after that checkpoint should be terminated, and any in-progress
/// AI generation should be canceled.
#[gpui::test]
async fn test_restore_checkpoint_kills_terminal(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let connection = Rc::new(FakeAgentConnection::new());
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
.await
.unwrap();
// Send first user message to create a checkpoint
cx.update(|cx| {
thread.update(cx, |thread, cx| {
thread.send(vec!["first message".into()], cx)
})
})
.await
.unwrap();
// Send second message (creates another checkpoint) - we'll restore to this one
cx.update(|cx| {
thread.update(cx, |thread, cx| {
thread.send(vec!["second message".into()], cx)
})
})
.await
.unwrap();
// Create 2 terminals BEFORE the checkpoint that have completed running
let terminal_id_1 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal_1 = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
)
.unwrap();
builder.subscribe(cx)
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id_1.clone(),
label: "echo 'first'".to_string(),
cwd: Some(PathBuf::from("/test")),
output_byte_limit: None,
terminal: mock_terminal_1.clone(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id_1.clone(),
data: b"first\n".to_vec(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id_1.clone(),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
},
cx,
);
});
let terminal_id_2 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal_2 = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
)
.unwrap();
builder.subscribe(cx)
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id_2.clone(),
label: "echo 'second'".to_string(),
cwd: Some(PathBuf::from("/test")),
output_byte_limit: None,
terminal: mock_terminal_2.clone(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id_2.clone(),
data: b"second\n".to_vec(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id_2.clone(),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
},
cx,
);
});
// Get the second message ID to restore to
let second_message_id = thread.read_with(cx, |thread, _| {
// At this point we have:
// - Index 0: First user message (with checkpoint)
// - Index 1: Second user message (with checkpoint)
// No assistant responses because FakeAgentConnection just returns EndTurn
let AgentThreadEntry::UserMessage(message) = &thread.entries[1] else {
panic!("expected user message at index 1");
};
message.id.clone().unwrap()
});
// Create a terminal AFTER the checkpoint we'll restore to.
// This simulates the AI agent starting a long-running terminal command.
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
)
.unwrap();
builder.subscribe(cx)
});
// Register the terminal as created
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id.clone(),
label: "sleep 1000".to_string(),
cwd: Some(PathBuf::from("/test")),
output_byte_limit: None,
terminal: mock_terminal.clone(),
},
cx,
);
});
// Simulate the terminal producing output (still running)
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id.clone(),
data: b"terminal is running...\n".to_vec(),
},
cx,
);
});
// Create a tool call entry that references this terminal
// This represents the agent requesting a terminal command
thread.update(cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("terminal-tool-1".into()),
title: "Running command".into(),
kind: acp::ToolKind::Execute,
status: acp::ToolCallStatus::InProgress,
content: vec![acp::ToolCallContent::Terminal {
terminal_id: terminal_id.clone(),
}],
locations: vec![],
raw_input: Some(
serde_json::json!({"command": "sleep 1000", "cd": "/test"}),
),
raw_output: None,
meta: None,
}),
cx,
)
.unwrap();
});
// Verify terminal exists and is in the thread
let terminal_exists_before =
thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id));
assert!(
terminal_exists_before,
"Terminal should exist before checkpoint restore"
);
// Verify the terminal's underlying task is still running (not completed)
let terminal_running_before = thread.read_with(cx, |thread, _cx| {
let terminal_entity = thread.terminals.get(&terminal_id).unwrap();
terminal_entity.read_with(cx, |term, _cx| {
term.output().is_none() // output is None means it's still running
})
});
assert!(
terminal_running_before,
"Terminal should be running before checkpoint restore"
);
// Verify we have the expected entries before restore
let entry_count_before = thread.read_with(cx, |thread, _| thread.entries.len());
assert!(
entry_count_before > 1,
"Should have multiple entries before restore"
);
// Restore the checkpoint to the second message.
// This should:
// 1. Cancel any in-progress generation (via the cancel() call)
// 2. Remove the terminal that was created after that point
thread
.update(cx, |thread, cx| {
thread.restore_checkpoint(second_message_id, cx)
})
.await
.unwrap();
// Verify that no send_task is in progress after restore
// (cancel() clears the send_task)
let has_send_task_after = thread.read_with(cx, |thread, _| thread.send_task.is_some());
assert!(
!has_send_task_after,
"Should not have a send_task after restore (cancel should have cleared it)"
);
// Verify the entries were truncated (restoring to index 1 truncates at 1, keeping only index 0)
let entry_count = thread.read_with(cx, |thread, _| thread.entries.len());
assert_eq!(
entry_count, 1,
"Should have 1 entry after restore (only the first user message)"
);
// Verify the 2 completed terminals from before the checkpoint still exist
let terminal_1_exists = thread.read_with(cx, |thread, _| {
thread.terminals.contains_key(&terminal_id_1)
});
assert!(
terminal_1_exists,
"Terminal 1 (from before checkpoint) should still exist"
);
let terminal_2_exists = thread.read_with(cx, |thread, _| {
thread.terminals.contains_key(&terminal_id_2)
});
assert!(
terminal_2_exists,
"Terminal 2 (from before checkpoint) should still exist"
);
// Verify they're still in completed state
let terminal_1_completed = thread.read_with(cx, |thread, _cx| {
let terminal_entity = thread.terminals.get(&terminal_id_1).unwrap();
terminal_entity.read_with(cx, |term, _cx| term.output().is_some())
});
assert!(terminal_1_completed, "Terminal 1 should still be completed");
let terminal_2_completed = thread.read_with(cx, |thread, _cx| {
let terminal_entity = thread.terminals.get(&terminal_id_2).unwrap();
terminal_entity.read_with(cx, |term, _cx| term.output().is_some())
});
assert!(terminal_2_completed, "Terminal 2 should still be completed");
// Verify the running terminal (created after checkpoint) was removed
let terminal_3_exists =
thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id));
assert!(
!terminal_3_exists,
"Terminal 3 (created after checkpoint) should have been removed"
);
// Verify total count is 2 (the two from before the checkpoint)
let terminal_count = thread.read_with(cx, |thread, _| thread.terminals.len());
assert_eq!(
terminal_count, 2,
"Should have exactly 2 terminals (the completed ones from before checkpoint)"
);
}
}

View File

@@ -17,7 +17,6 @@ anyhow.workspace = true
auto_update.workspace = true
editor.workspace = true
extension_host.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true

View File

@@ -51,7 +51,6 @@ pub struct ActivityIndicator {
project: Entity<Project>,
auto_updater: Option<Entity<AutoUpdater>>,
context_menu_handle: PopoverMenuHandle<ContextMenu>,
fs_jobs: Vec<fs::JobInfo>,
}
#[derive(Debug)]
@@ -100,27 +99,6 @@ impl ActivityIndicator {
})
.detach();
let fs = project.read(cx).fs().clone();
let mut job_events = fs.subscribe_to_jobs();
cx.spawn(async move |this, cx| {
while let Some(job_event) = job_events.next().await {
this.update(cx, |this: &mut ActivityIndicator, cx| {
match job_event {
fs::JobEvent::Started { info } => {
this.fs_jobs.retain(|j| j.id != info.id);
this.fs_jobs.push(info);
}
fs::JobEvent::Completed { id } => {
this.fs_jobs.retain(|j| j.id != id);
}
}
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach();
cx.subscribe(
&project.read(cx).lsp_store(),
|activity_indicator, _, event, cx| {
@@ -223,8 +201,7 @@ impl ActivityIndicator {
statuses: Vec::new(),
project: project.clone(),
auto_updater,
context_menu_handle: PopoverMenuHandle::default(),
fs_jobs: Vec::new(),
context_menu_handle: Default::default(),
}
});
@@ -455,23 +432,6 @@ impl ActivityIndicator {
});
}
// Show any long-running fs command
for fs_job in &self.fs_jobs {
if Instant::now().duration_since(fs_job.start) >= GIT_OPERATION_DELAY {
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_rotate_animation(2)
.into_any_element(),
),
message: fs_job.message.clone().into(),
on_click: None,
tooltip_message: None,
});
}
}
// Show any language server installation info.
let mut downloading = SmallVec::<[_; 3]>::new();
let mut checking_for_update = SmallVec::<[_; 3]>::new();

View File

@@ -133,7 +133,9 @@ impl LanguageModels {
for model in provider.provided_models(cx) {
let model_info = Self::map_language_model_to_info(&model, &provider);
let model_id = model_info.id.clone();
provider_models.push(model_info);
if !recommended_models.contains(&(model.provider_id(), model.id())) {
provider_models.push(model_info);
}
models.insert(model_id, model);
}
if !provider_models.is_empty() {

View File

@@ -150,7 +150,6 @@ impl DbThread {
.unwrap_or_default(),
input: tool_use.input,
is_input_complete: true,
thought_signature: None,
},
));
}
@@ -182,7 +181,6 @@ impl DbThread {
crate::Message::Agent(AgentMessage {
content,
tool_results,
reasoning_details: None,
})
}
language_model::Role::System => {

View File

@@ -703,7 +703,6 @@ impl EditAgent {
role: Role::User,
content: vec![MessageContent::Text(prompt)],
cache: false,
reasoning_details: None,
});
// Include tools in the request so that we can take advantage of

View File

@@ -1081,7 +1081,6 @@ fn message(
role,
content: contents.into_iter().collect(),
cache: false,
reasoning_details: None,
}
}
@@ -1109,7 +1108,6 @@ fn tool_use(
raw_input: serde_json::to_string_pretty(&input).unwrap(),
input: serde_json::to_value(input).unwrap(),
is_input_complete: true,
thought_signature: None,
})
}
@@ -1269,7 +1267,6 @@ impl EvalAssertion {
role: Role::User,
content: vec![prompt.into()],
cache: false,
reasoning_details: None,
}],
thinking_allowed: true,
..Default::default()
@@ -1596,7 +1593,6 @@ impl EditAgentTest {
role: Role::System,
content: vec![MessageContent::Text(system_prompt)],
cache: true,
reasoning_details: None,
}]
.into_iter()
.chain(eval.conversation)

View File

@@ -215,8 +215,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
vec![LanguageModelRequestMessage {
role: Role::User,
content: vec!["Message 1".into()],
cache: true,
reasoning_details: None,
cache: true
}]
);
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
@@ -240,20 +239,17 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Message 1".into()],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec!["Response to Message 1".into()],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Message 2".into()],
cache: true,
reasoning_details: None,
cache: true
}
]
);
@@ -278,7 +274,6 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
};
fake_model
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
@@ -299,44 +294,37 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Message 1".into()],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec!["Response to Message 1".into()],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Message 2".into()],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec!["Response to Message 2".into()],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Use the echo tool".into()],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::ToolResult(tool_result)],
cache: true,
reasoning_details: None,
cache: true
}
]
);
@@ -473,7 +461,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -483,7 +470,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -534,7 +520,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -569,7 +554,6 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -608,7 +592,6 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -638,7 +621,6 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
is_input_complete: true,
thought_signature: None,
};
fake_model
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
@@ -659,20 +641,17 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["abc".into()],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use.clone())],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::ToolResult(tool_result.clone())],
cache: true,
reasoning_details: None,
cache: true
},
]
);
@@ -698,26 +677,22 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["abc".into()],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::ToolResult(tool_result)],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Continue where you left off".into()],
cache: true,
reasoning_details: None,
cache: true
}
]
);
@@ -756,7 +731,6 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
is_input_complete: true,
thought_signature: None,
};
let tool_result = LanguageModelToolResult {
tool_use_id: "tool_id_1".into(),
@@ -791,26 +765,22 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["abc".into()],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::ToolResult(tool_result)],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
content: vec!["ghi".into()],
cache: true,
reasoning_details: None,
cache: true
}
]
);
@@ -963,7 +933,7 @@ async fn test_profiles(cx: &mut TestAppContext) {
// Test that test-1 profile (default) has echo and delay tools
thread
.update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test-1".into()), cx);
thread.set_profile(AgentProfileId("test-1".into()));
thread.send(UserMessageId::new(), ["test"], cx)
})
.unwrap();
@@ -983,7 +953,7 @@ async fn test_profiles(cx: &mut TestAppContext) {
// Switch to test-2 profile, and verify that it has only the infinite tool.
thread
.update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test-2".into()), cx);
thread.set_profile(AgentProfileId("test-2".into()));
thread.send(UserMessageId::new(), ["test2"], cx)
})
.unwrap();
@@ -1032,8 +1002,8 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
)
.await;
cx.run_until_parked();
thread.update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test".into()), cx)
thread.update(cx, |thread, _| {
thread.set_profile(AgentProfileId("test".into()))
});
let mut mcp_tool_calls = setup_context_server(
@@ -1067,7 +1037,6 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -1111,7 +1080,6 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "mcp"}).to_string(),
input: json!({"text": "mcp"}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -1121,7 +1089,6 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "native"}).to_string(),
input: json!({"text": "native"}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -1202,8 +1169,8 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
.await;
cx.run_until_parked();
thread.update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test".into()), cx);
thread.update(cx, |thread, _| {
thread.set_profile(AgentProfileId("test".into()));
thread.add_tool(EchoTool);
thread.add_tool(DelayTool);
thread.add_tool(WordListTool);
@@ -1821,7 +1788,6 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
};
let echo_tool_use = LanguageModelToolUse {
id: "tool_id_2".into(),
@@ -1829,7 +1795,6 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
};
fake_model.send_last_completion_stream_text_chunk("Hi!");
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -1853,8 +1818,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Hey!".into()],
cache: true,
reasoning_details: None,
cache: true
},
LanguageModelRequestMessage {
role: Role::Assistant,
@@ -1862,8 +1826,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
MessageContent::Text("Hi!".into()),
MessageContent::ToolUse(echo_tool_use.clone())
],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
@@ -1874,8 +1837,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
content: "test".into(),
output: Some("test".into())
})],
cache: false,
reasoning_details: None,
cache: false
},
],
);
@@ -2038,7 +2000,6 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_input: input.to_string(),
input,
is_input_complete: false,
thought_signature: None,
},
));
@@ -2051,7 +2012,6 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_input: input.to_string(),
input,
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -2254,7 +2214,6 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
};
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
tool_use_1.clone(),
@@ -2273,14 +2232,12 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Call the echo tool!".into()],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
cache: false,
reasoning_details: None,
cache: false
},
LanguageModelRequestMessage {
role: Role::User,
@@ -2293,8 +2250,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
output: Some("test".into())
}
)],
cache: true,
reasoning_details: None,
cache: true
},
]
);
@@ -2308,8 +2264,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
thread.last_message(),
Some(Message::Agent(AgentMessage {
content: vec![AgentMessageContent::Text("Done".into())],
tool_results: IndexMap::default(),
reasoning_details: None,
tool_results: IndexMap::default()
}))
);
})

View File

@@ -30,17 +30,16 @@ use gpui::{
};
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt,
LanguageModelId, LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry,
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
LanguageModelToolUse, LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage,
ZED_CLOUD_PROVIDER_ID,
LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
};
use project::Project;
use prompt_store::ProjectContext;
use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use settings::{LanguageModelSelection, Settings, update_settings_file};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
use std::{
collections::BTreeMap,
@@ -113,7 +112,6 @@ impl Message {
role: Role::User,
content: vec!["Continue where you left off".into()],
cache: false,
reasoning_details: None,
}],
}
}
@@ -178,7 +176,6 @@ impl UserMessage {
role: Role::User,
content: Vec::with_capacity(self.content.len()),
cache: false,
reasoning_details: None,
};
const OPEN_CONTEXT: &str = "<context>\n\
@@ -446,7 +443,6 @@ impl AgentMessage {
role: Role::Assistant,
content: Vec::with_capacity(self.content.len()),
cache: false,
reasoning_details: self.reasoning_details.clone(),
};
for chunk in &self.content {
match chunk {
@@ -482,7 +478,6 @@ impl AgentMessage {
role: Role::User,
content: Vec::new(),
cache: false,
reasoning_details: None,
};
for tool_result in self.tool_results.values() {
@@ -512,7 +507,6 @@ impl AgentMessage {
pub struct AgentMessage {
pub content: Vec<AgentMessageContent>,
pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
pub reasoning_details: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -804,8 +798,7 @@ impl Thread {
let profile_id = db_thread
.profile
.unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone());
let mut model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
let model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
db_thread
.model
.and_then(|model| {
@@ -818,16 +811,6 @@ impl Thread {
.or_else(|| registry.default_model())
.map(|model| model.model)
});
if model.is_none() {
model = Self::resolve_profile_model(&profile_id, cx);
}
if model.is_none() {
model = LanguageModelRegistry::global(cx).update(cx, |registry, _cx| {
registry.default_model().map(|model| model.model)
});
}
let (prompt_capabilities_tx, prompt_capabilities_rx) =
watch::channel(Self::prompt_capabilities(model.as_deref()));
@@ -1024,17 +1007,8 @@ impl Thread {
&self.profile_id
}
pub fn set_profile(&mut self, profile_id: AgentProfileId, cx: &mut Context<Self>) {
if self.profile_id == profile_id {
return;
}
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
self.profile_id = profile_id;
// Swap to the profile's preferred model when available.
if let Some(model) = Self::resolve_profile_model(&self.profile_id, cx) {
self.set_model(model, cx);
}
}
pub fn cancel(&mut self, cx: &mut Context<Self>) {
@@ -1091,35 +1065,6 @@ impl Thread {
})
}
/// Look up the active profile and resolve its preferred model if one is configured.
fn resolve_profile_model(
profile_id: &AgentProfileId,
cx: &mut Context<Self>,
) -> Option<Arc<dyn LanguageModel>> {
let selection = AgentSettings::get_global(cx)
.profiles
.get(profile_id)?
.default_model
.clone()?;
Self::resolve_model_from_selection(&selection, cx)
}
/// Translate a stored model selection into the configured model from the registry.
fn resolve_model_from_selection(
selection: &LanguageModelSelection,
cx: &mut Context<Self>,
) -> Option<Arc<dyn LanguageModel>> {
let selected = SelectedModel {
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
model: LanguageModelId::from(selection.model.clone()),
};
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry
.select_model(&selected, cx)
.map(|configured| configured.model)
})
}
pub fn resume(
&mut self,
cx: &mut Context<Self>,
@@ -1398,18 +1343,6 @@ impl Thread {
self.handle_thinking_event(text, signature, event_stream, cx)
}
RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx),
ReasoningDetails(details) => {
let last_message = self.pending_message();
// Store the last non-empty reasoning_details (overwrites earlier ones)
// This ensures we keep the encrypted reasoning with signatures, not the early text reasoning
if let serde_json::Value::Array(ref arr) = details {
if !arr.is_empty() {
last_message.reasoning_details = Some(details);
}
} else {
last_message.reasoning_details = Some(details);
}
}
ToolUse(tool_use) => {
return Ok(self.handle_tool_use_event(tool_use, event_stream, cx));
}
@@ -1689,7 +1622,6 @@ impl Thread {
role: Role::User,
content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
cache: false,
reasoning_details: None,
});
let task = cx
@@ -1756,7 +1688,6 @@ impl Thread {
role: Role::User,
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
cache: false,
reasoning_details: None,
});
self.pending_title_generation = Some(cx.spawn(async move |this, cx| {
let mut title = String::new();
@@ -2006,7 +1937,6 @@ impl Thread {
role: Role::System,
content: vec![system_prompt.into()],
cache: false,
reasoning_details: None,
}];
for message in &self.messages {
messages.extend(message.to_request());

View File

@@ -136,7 +136,7 @@ impl AcpConnection {
while let Ok(n) = stderr.read_line(&mut line).await
&& n > 0
{
log::warn!("agent stderr: {}", line.trim());
log::warn!("agent stderr: {}", &line);
line.clear();
}
Ok(())

View File

@@ -50,14 +50,13 @@ impl crate::AgentServer for CustomAgentServer {
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file(fs, cx, move |settings, _| {
if let Some(settings) = settings
settings
.agent_servers
.get_or_insert_default()
.custom
.get_mut(&name)
{
settings.default_mode = mode_id.map(|m| m.to_string())
}
.unwrap()
.default_mode = mode_id.map(|m| m.to_string())
});
}

View File

@@ -6,8 +6,8 @@ use convert_case::{Case, Casing as _};
use fs::Fs;
use gpui::{App, SharedString};
use settings::{
AgentProfileContent, ContextServerPresetContent, LanguageModelSelection, Settings as _,
SettingsContent, update_settings_file,
AgentProfileContent, ContextServerPresetContent, Settings as _, SettingsContent,
update_settings_file,
};
use util::ResultExt as _;
@@ -53,30 +53,19 @@ impl AgentProfile {
let base_profile =
base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned());
// Copy toggles from the base profile so the new profile starts with familiar defaults.
let tools = base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default();
let enable_all_context_servers = base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default();
let context_servers = base_profile
.as_ref()
.map(|profile| profile.context_servers.clone())
.unwrap_or_default();
// Preserve the base profile's model preference when cloning into a new profile.
let default_model = base_profile
.as_ref()
.and_then(|profile| profile.default_model.clone());
let profile_settings = AgentProfileSettings {
name: name.into(),
tools,
enable_all_context_servers,
context_servers,
default_model,
tools: base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default(),
enable_all_context_servers: base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default(),
context_servers: base_profile
.map(|profile| profile.context_servers)
.unwrap_or_default(),
};
update_settings_file(fs, cx, {
@@ -107,8 +96,6 @@ pub struct AgentProfileSettings {
pub tools: IndexMap<Arc<str>, bool>,
pub enable_all_context_servers: bool,
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
/// Default language model to apply when this profile becomes active.
pub default_model: Option<LanguageModelSelection>,
}
impl AgentProfileSettings {
@@ -157,7 +144,6 @@ impl AgentProfileSettings {
)
})
.collect(),
default_model: self.default_model.clone(),
},
);
@@ -167,23 +153,15 @@ impl AgentProfileSettings {
impl From<AgentProfileContent> for AgentProfileSettings {
fn from(content: AgentProfileContent) -> Self {
let AgentProfileContent {
name,
tools,
enable_all_context_servers,
context_servers,
default_model,
} = content;
Self {
name: name.into(),
tools,
enable_all_context_servers: enable_all_context_servers.unwrap_or_default(),
context_servers: context_servers
name: content.name.into(),
tools: content.tools,
enable_all_context_servers: content.enable_all_context_servers.unwrap_or_default(),
context_servers: content
.context_servers
.into_iter()
.map(|(server_id, preset)| (server_id, preset.into()))
.collect(),
default_model,
}
}
}

View File

@@ -109,8 +109,6 @@ impl ContextPickerCompletionProvider {
icon_path: Some(mode.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
@@ -148,8 +146,6 @@ impl ContextPickerCompletionProvider {
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
icon_path: Some(icon_for_completion),
confirm: Some(confirm_completion_callback(
thread_entry.title().clone(),
@@ -181,8 +177,6 @@ impl ContextPickerCompletionProvider {
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
icon_path: Some(icon_path),
confirm: Some(confirm_completion_callback(
rule.title,
@@ -239,8 +233,6 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(completion_icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
file_name,
@@ -292,8 +284,6 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
symbol.name.into(),
@@ -326,8 +316,6 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
url_to_fetch.to_string().into(),
@@ -396,8 +384,6 @@ impl ContextPickerCompletionProvider {
icon_path: Some(action.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
@@ -708,18 +694,14 @@ fn build_symbol_label(symbol_name: &str, file_name: &str, line: u32, cx: &App) -
}
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
let path = cx
.theme()
.syntax()
.highlight_id("variable")
.map(HighlightId);
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabelBuilder::default();
label.push_str(file_name, None);
label.push_str(" ", None);
if let Some(directory) = directory {
label.push_str(directory, path);
label.push_str(directory, comment_id);
}
label.build()
@@ -788,8 +770,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
)),
source: project::CompletionSource::Custom,
icon_path: None,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(Arc::new({
let editor = editor.clone();

View File

@@ -15,7 +15,6 @@ use editor::{
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
MultiBuffer, ToOffset,
actions::Paste,
code_context_menus::CodeContextMenu,
display_map::{Crease, CreaseId, FoldId},
scroll::Autoscroll,
};
@@ -273,15 +272,6 @@ impl MessageEditor {
self.editor.read(cx).is_empty(cx)
}
pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
self.editor
.read(cx)
.context_menu()
.borrow()
.as_ref()
.is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
}
pub fn mentions(&self) -> HashSet<MentionUri> {
self.mention_set
.mentions
@@ -846,45 +836,6 @@ impl MessageEditor {
cx.emit(MessageEditorEvent::Send)
}
pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let editor = self.editor.clone();
cx.spawn_in(window, async move |_, cx| {
editor
.update_in(cx, |editor, window, cx| {
let menu_is_open =
editor.context_menu().borrow().as_ref().is_some_and(|menu| {
matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
});
let has_at_sign = {
let snapshot = editor.display_snapshot(cx);
let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
let offset = cursor.to_offset(&snapshot);
if offset > 0 {
snapshot
.buffer_snapshot()
.reversed_chars_at(offset)
.next()
.map(|sign| sign == '@')
.unwrap_or(false)
} else {
false
}
};
if menu_is_open && has_at_sign {
return;
}
editor.insert("@", window, cx);
editor.show_completions(&editor::actions::ShowCompletions, window, cx);
})
.log_err();
})
.detach();
}
fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
self.send(cx);
}
@@ -1244,17 +1195,6 @@ impl MessageEditor {
self.editor.read(cx).text(cx)
}
pub fn set_placeholder_text(
&mut self,
placeholder: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.set_placeholder_text(placeholder, window, cx);
});
}
#[cfg(test)]
pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {

View File

@@ -457,23 +457,25 @@ impl Render for AcpThreadHistory {
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread))
.child(
h_flex()
.h(px(41.)) // Match the toolbar perfectly
.w_full()
.py_1()
.px_2()
.gap_2()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
Icon::new(IconName::MagnifyingGlass)
.color(Color::Muted)
.size(IconSize::Small),
)
.child(self.search_editor.clone()),
)
.when(!self.history_store.read(cx).is_empty(cx), |parent| {
parent.child(
h_flex()
.h(px(41.)) // Match the toolbar perfectly
.w_full()
.py_1()
.px_2()
.gap_2()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
Icon::new(IconName::MagnifyingGlass)
.color(Color::Muted)
.size(IconSize::Small),
)
.child(self.search_editor.clone()),
)
})
.child({
let view = v_flex()
.id("list-container")
@@ -482,15 +484,19 @@ impl Render for AcpThreadHistory {
.flex_grow();
if self.history_store.read(cx).is_empty(cx) {
view.justify_center().items_center().child(
Label::new("You don't have any past threads yet.")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else if self.search_produced_no_matches() {
view.justify_center()
.items_center()
.child(Label::new("No threads match your search.").size(LabelSize::Small))
.child(
h_flex().w_full().justify_center().child(
Label::new("You don't have any past threads yet.")
.size(LabelSize::Small),
),
)
} else if self.search_produced_no_matches() {
view.justify_center().child(
h_flex().w_full().justify_center().child(
Label::new("No threads match your search.").size(LabelSize::Small),
),
)
} else {
view.child(
uniform_list(
@@ -667,7 +673,7 @@ impl EntryTimeFormat {
timezone,
time_format::TimestampFormat::EnhancedAbsolute,
),
EntryTimeFormat::TimeOnly => time_format::format_time(timestamp.to_offset(timezone)),
EntryTimeFormat::TimeOnly => time_format::format_time(timestamp),
}
}
}

View File

@@ -125,9 +125,8 @@ impl ProfileProvider for Entity<agent::Thread> {
}
fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
self.update(cx, |thread, cx| {
// Apply the profile and let the thread swap to its default model.
thread.set_profile(profile_id, cx);
self.update(cx, |thread, _cx| {
thread.set_profile(profile_id);
});
}
@@ -337,7 +336,19 @@ impl AcpThreadView {
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let available_commands = Rc::new(RefCell::new(vec![]));
let placeholder = placeholder_text(agent.name().as_ref(), false);
let placeholder = if agent.name() == "Zed Agent" {
format!("Message the {} — @ to include context", agent.name())
} else if agent.name() == "Claude Code"
|| agent.name() == "Codex"
|| !available_commands.borrow().is_empty()
{
format!(
"Message {} — @ to include context, / for commands",
agent.name()
)
} else {
format!("Message {} — @ to include context", agent.name())
};
let message_editor = cx.new(|cx| {
let mut editor = MessageEditor::new(
@@ -1444,14 +1455,7 @@ impl AcpThreadView {
});
}
let has_commands = !available_commands.is_empty();
self.available_commands.replace(available_commands);
let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands);
self.message_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(&new_placeholder, window, cx);
});
}
AcpThreadEvent::ModeUpdated(_mode) => {
// The connection keeps track of the mode
@@ -4188,8 +4192,6 @@ impl AcpThreadView {
.justify_between()
.child(
h_flex()
.gap_0p5()
.child(self.render_add_context_button(cx))
.child(self.render_follow_toggle(cx))
.children(self.render_burn_mode_toggle(cx)),
)
@@ -4504,29 +4506,6 @@ impl AcpThreadView {
}))
}
fn render_add_context_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
let message_editor = self.message_editor.clone();
let menu_visible = message_editor.read(cx).is_completions_menu_visible(cx);
IconButton::new("add-context", IconName::AtSign)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.when(!menu_visible, |this| {
this.tooltip(move |_window, cx| {
Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
})
})
.on_click(cx.listener(move |_this, _, window, cx| {
let message_editor_clone = message_editor.clone();
window.defer(cx, move |window, cx| {
message_editor_clone.update(cx, |message_editor, cx| {
message_editor.trigger_completion_menu(window, cx);
});
});
}))
}
fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
let workspace = self.workspace.clone();
MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
@@ -5728,19 +5707,6 @@ fn loading_contents_spinner(size: IconSize) -> AnyElement {
.into_any_element()
}
fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
if agent_name == "Zed Agent" {
format!("Message the {} — @ to include context", agent_name)
} else if has_commands {
format!(
"Message {} — @ to include context, / for commands",
agent_name
)
} else {
format!("Message {} — @ to include context", agent_name)
}
}
impl Focusable for AcpThreadView {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match self.thread_state {

View File

@@ -8,7 +8,6 @@ use std::{ops::Range, sync::Arc};
use agent::ContextServerRegistry;
use anyhow::Result;
use client::zed_urls;
use cloud_llm_client::{Plan, PlanV1, PlanV2};
use collections::HashMap;
use context_server::ContextServerId;
@@ -27,20 +26,18 @@ use language_model::{
use language_models::AllLanguageModelSettings;
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
agent_server_store::{
AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME,
},
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
};
use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure,
Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize,
PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor,
ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, PopoverMenu, Switch,
SwitchColor, Tooltip, WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
use zed_actions::{ExtensionCategoryFilter, OpenBrowser};
use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
@@ -418,7 +415,6 @@ impl AgentConfiguration {
cx: &mut Context<Self>,
) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let popover_menu = PopoverMenu::new("add-provider-popover")
.trigger(
Button::new("add-provider", "Add Provider")
@@ -429,6 +425,7 @@ impl AgentConfiguration {
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.anchor(gpui::Corner::TopRight)
.menu({
let workspace = self.workspace.clone();
move |window, cx| {
@@ -450,11 +447,6 @@ impl AgentConfiguration {
})
}))
}
})
.anchor(gpui::Corner::TopRight)
.offset(gpui::Point {
x: px(0.0),
y: px(2.0),
});
v_flex()
@@ -549,6 +541,7 @@ impl AgentConfiguration {
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.anchor(gpui::Corner::TopRight)
.menu({
move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
@@ -571,11 +564,6 @@ impl AgentConfiguration {
})
}))
}
})
.anchor(gpui::Corner::TopRight)
.offset(gpui::Point {
x: px(0.0),
y: px(2.0),
});
v_flex()
@@ -650,13 +638,15 @@ impl AgentConfiguration {
let is_running = matches!(server_status, ContextServerStatus::Running);
let item_id = SharedString::from(context_server_id.0.clone());
// Servers without a configuration can only be provided by extensions.
let provided_by_extension = server_configuration.is_none_or(|config| {
matches!(
config.as_ref(),
ContextServerConfiguration::Extension { .. }
)
});
let is_from_extension = server_configuration
.as_ref()
.map(|config| {
matches!(
config.as_ref(),
ContextServerConfiguration::Extension { .. }
)
})
.unwrap_or(false);
let error = if let ContextServerStatus::Error(error) = server_status.clone() {
Some(error)
@@ -670,7 +660,7 @@ impl AgentConfiguration {
.tools_for_server(&context_server_id)
.count();
let (source_icon, source_tooltip) = if provided_by_extension {
let (source_icon, source_tooltip) = if is_from_extension {
(
IconName::ZedSrcExtension,
"This MCP server was installed from an extension.",
@@ -720,6 +710,7 @@ impl AgentConfiguration {
let fs = self.fs.clone();
let context_server_id = context_server_id.clone();
let language_registry = self.language_registry.clone();
let context_server_store = self.context_server_store.clone();
let workspace = self.workspace.clone();
let context_server_registry = self.context_server_registry.clone();
@@ -761,10 +752,23 @@ impl AgentConfiguration {
.entry("Uninstall", None, {
let fs = fs.clone();
let context_server_id = context_server_id.clone();
let context_server_store = context_server_store.clone();
let workspace = workspace.clone();
move |_, cx| {
let is_provided_by_extension = context_server_store
.read(cx)
.configuration_for_server(&context_server_id)
.as_ref()
.map(|config| {
matches!(
config.as_ref(),
ContextServerConfiguration::Extension { .. }
)
})
.unwrap_or(false);
let uninstall_extension_task = match (
provided_by_extension,
is_provided_by_extension,
resolve_extension_for_context_server(&context_server_id, cx),
) {
(true, Some((id, manifest))) => {
@@ -955,7 +959,7 @@ impl AgentConfiguration {
.cloned()
.collect::<Vec<_>>();
let user_defined_agents: Vec<_> = user_defined_agents
let user_defined_agents = user_defined_agents
.into_iter()
.map(|name| {
let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
@@ -963,93 +967,27 @@ impl AgentConfiguration {
} else {
AgentIcon::Name(IconName::Ai)
};
(name, icon)
self.render_agent_server(icon, name, true)
.into_any_element()
})
.collect();
.collect::<Vec<_>>();
let add_agent_popover = PopoverMenu::new("add-agent-server-popover")
.trigger(
Button::new("add-agent", "Add Agent")
.style(ButtonStyle::Outlined)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.menu({
move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.entry("Install from Extensions", None, {
|window, cx| {
window.dispatch_action(
zed_actions::Extensions {
category_filter: Some(
ExtensionCategoryFilter::AgentServers,
),
id: None,
}
.boxed_clone(),
cx,
)
}
let add_agens_button = Button::new("add-agent", "Add Agent")
.style(ButtonStyle::Outlined)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(move |_, window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(workspace, cx).await
})
.entry("Add Custom Agent", None, {
move |window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(
workspace, cx,
)
.await
})
.detach_and_log_err(cx);
}
}
})
.separator()
.header("Learn More")
.item(
ContextMenuEntry::new("Agent Servers Docs")
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_position(IconPosition::End)
.handler({
move |window, cx| {
window.dispatch_action(
Box::new(OpenBrowser {
url: zed_urls::agent_server_docs(cx),
}),
cx,
);
}
}),
)
.item(
ContextMenuEntry::new("ACP Docs")
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_position(IconPosition::End)
.handler({
move |window, cx| {
window.dispatch_action(
Box::new(OpenBrowser {
url: "https://agentclientprotocol.com/".into(),
}),
cx,
);
}
}),
)
}))
.detach_and_log_err(cx);
}
})
.anchor(gpui::Corner::TopRight)
.offset(gpui::Point {
x: px(0.0),
y: px(2.0),
});
v_flex()
@@ -1060,7 +998,7 @@ impl AgentConfiguration {
.child(self.render_section_title(
"External Agents",
"All agents connected through the Agent Client Protocol.",
add_agent_popover.into_any_element(),
add_agens_button.into_any_element(),
))
.child(
v_flex()
@@ -1071,29 +1009,26 @@ impl AgentConfiguration {
AgentIcon::Name(IconName::AiClaude),
"Claude Code",
false,
cx,
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
AgentIcon::Name(IconName::AiOpenAi),
"Codex CLI",
false,
cx,
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
AgentIcon::Name(IconName::AiGemini),
"Gemini CLI",
false,
cx,
))
.map(|mut parent| {
for (name, icon) in user_defined_agents {
for agent in user_defined_agents {
parent = parent
.child(
Divider::horizontal().color(DividerColor::BorderFaded),
)
.child(self.render_agent_server(icon, name, true, cx));
.child(agent);
}
parent
}),
@@ -1106,7 +1041,6 @@ impl AgentConfiguration {
icon: AgentIcon,
name: impl Into<SharedString>,
external: bool,
cx: &mut Context<Self>,
) -> impl IntoElement {
let name = name.into();
let icon = match icon {
@@ -1121,53 +1055,28 @@ impl AgentConfiguration {
let tooltip_id = SharedString::new(format!("agent-source-{}", name));
let tooltip_message = format!("The {} agent was installed from an extension.", name);
let agent_server_name = ExternalAgentServerName(name.clone());
let uninstall_btn_id = SharedString::from(format!("uninstall-{}", name));
let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Uninstall Agent Extension"))
.on_click(cx.listener(move |this, _, _window, cx| {
let agent_name = agent_server_name.clone();
if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| {
store.get_extension_id_for_agent(&agent_name)
}) {
ExtensionStore::global(cx)
.update(cx, |store, cx| store.uninstall_extension(ext_id, cx))
.detach_and_log_err(cx);
}
}));
h_flex()
.gap_1()
.justify_between()
.gap_1p5()
.child(icon)
.child(Label::new(name))
.when(external, |this| {
this.child(
div()
.id(tooltip_id)
.flex_none()
.tooltip(Tooltip::text(tooltip_message))
.child(
Icon::new(IconName::ZedSrcExtension)
.size(IconSize::Small)
.color(Color::Muted),
),
)
})
.child(
h_flex()
.gap_1p5()
.child(icon)
.child(Label::new(name))
.when(external, |this| {
this.child(
div()
.id(tooltip_id)
.flex_none()
.tooltip(Tooltip::text(tooltip_message))
.child(
Icon::new(IconName::ZedSrcExtension)
.size(IconSize::Small)
.color(Color::Muted),
),
)
})
.child(
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::Small),
),
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::Small),
)
.when(external, |this| this.child(uninstall_button))
}
}

View File

@@ -7,10 +7,8 @@ use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profil
use editor::Editor;
use fs::Fs;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
use language_model::{LanguageModel, LanguageModelRegistry};
use settings::{
LanguageModelProviderSetting, LanguageModelSelection, Settings as _, update_settings_file,
};
use language_model::LanguageModel;
use settings::Settings as _;
use ui::{
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
};
@@ -18,7 +16,6 @@ use workspace::{ModalView, Workspace};
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::language_model_selector::{LanguageModelSelector, language_model_selector};
use crate::{AgentPanel, ManageProfiles};
enum Mode {
@@ -35,11 +32,6 @@ enum Mode {
tool_picker: Entity<ToolPicker>,
_subscription: Subscription,
},
ConfigureDefaultModel {
profile_id: AgentProfileId,
model_picker: Entity<LanguageModelSelector>,
_subscription: Subscription,
},
}
impl Mode {
@@ -91,7 +83,6 @@ pub struct ChooseProfileMode {
pub struct ViewProfileMode {
profile_id: AgentProfileId,
fork_profile: NavigableEntry,
configure_default_model: NavigableEntry,
configure_tools: NavigableEntry,
configure_mcps: NavigableEntry,
cancel_item: NavigableEntry,
@@ -189,7 +180,6 @@ impl ManageProfilesModal {
self.mode = Mode::ViewProfile(ViewProfileMode {
profile_id,
fork_profile: NavigableEntry::focusable(cx),
configure_default_model: NavigableEntry::focusable(cx),
configure_tools: NavigableEntry::focusable(cx),
configure_mcps: NavigableEntry::focusable(cx),
cancel_item: NavigableEntry::focusable(cx),
@@ -197,83 +187,6 @@ impl ManageProfilesModal {
self.focus_handle(cx).focus(window);
}
fn configure_default_model(
&mut self,
profile_id: AgentProfileId,
window: &mut Window,
cx: &mut Context<Self>,
) {
let fs = self.fs.clone();
let profile_id_for_closure = profile_id.clone();
let model_picker = cx.new(|cx| {
let fs = fs.clone();
let profile_id = profile_id_for_closure.clone();
language_model_selector(
{
let profile_id = profile_id.clone();
move |cx| {
let settings = AgentSettings::get_global(cx);
settings
.profiles
.get(&profile_id)
.and_then(|profile| profile.default_model.as_ref())
.and_then(|selection| {
let registry = LanguageModelRegistry::read_global(cx);
let provider_id = language_model::LanguageModelProviderId(
gpui::SharedString::from(selection.provider.0.clone()),
);
let provider = registry.provider(&provider_id)?;
let model = provider
.provided_models(cx)
.iter()
.find(|m| m.id().0 == selection.model.as_str())?
.clone();
Some(language_model::ConfiguredModel { provider, model })
})
}
},
move |model, cx| {
let provider = model.provider_id().0.to_string();
let model_id = model.id().0.to_string();
let profile_id = profile_id.clone();
update_settings_file(fs.clone(), cx, move |settings, _cx| {
let agent_settings = settings.agent.get_or_insert_default();
if let Some(profiles) = agent_settings.profiles.as_mut() {
if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
profile.default_model = Some(LanguageModelSelection {
provider: LanguageModelProviderSetting(provider.clone()),
model: model_id.clone(),
});
}
}
});
},
false, // Do not use popover styles for the model picker
window,
cx,
)
.modal(false)
});
let dismiss_subscription = cx.subscribe_in(&model_picker, window, {
let profile_id = profile_id.clone();
move |this, _picker, _: &DismissEvent, window, cx| {
this.view_profile(profile_id.clone(), window, cx);
}
});
self.mode = Mode::ConfigureDefaultModel {
profile_id,
model_picker,
_subscription: dismiss_subscription,
};
self.focus_handle(cx).focus(window);
}
fn configure_mcp_tools(
&mut self,
profile_id: AgentProfileId,
@@ -364,7 +277,6 @@ impl ManageProfilesModal {
Mode::ViewProfile(_) => {}
Mode::ConfigureTools { .. } => {}
Mode::ConfigureMcps { .. } => {}
Mode::ConfigureDefaultModel { .. } => {}
}
}
@@ -387,9 +299,6 @@ impl ManageProfilesModal {
Mode::ConfigureMcps { profile_id, .. } => {
self.view_profile(profile_id.clone(), window, cx)
}
Mode::ConfigureDefaultModel { profile_id, .. } => {
self.view_profile(profile_id.clone(), window, cx)
}
}
}
}
@@ -404,7 +313,6 @@ impl Focusable for ManageProfilesModal {
Mode::ViewProfile(_) => self.focus_handle.clone(),
Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
Mode::ConfigureMcps { tool_picker, .. } => tool_picker.focus_handle(cx),
Mode::ConfigureDefaultModel { model_picker, .. } => model_picker.focus_handle(cx),
}
}
}
@@ -636,47 +544,6 @@ impl ManageProfilesModal {
}),
),
)
.child(
div()
.id("configure-default-model")
.track_focus(&mode.configure_default_model.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_default_model(
profile_id.clone(),
window,
cx,
);
})
})
.child(
ListItem::new("model-item")
.toggle_state(
mode.configure_default_model
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(
Icon::new(IconName::ZedAssistant)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("Configure Default Model"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_default_model(
profile_id.clone(),
window,
cx,
);
})
}),
),
)
.child(
div()
.id("configure-builtin-tools")
@@ -801,7 +668,6 @@ impl ManageProfilesModal {
.into_any_element(),
)
.entry(mode.fork_profile)
.entry(mode.configure_default_model)
.entry(mode.configure_tools)
.entry(mode.configure_mcps)
.entry(mode.cancel_item)
@@ -887,29 +753,6 @@ impl Render for ManageProfilesModal {
.child(go_back_item)
.into_any_element()
}
Mode::ConfigureDefaultModel {
profile_id,
model_picker,
..
} => {
let profile_name = settings
.profiles
.get(profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
v_flex()
.pb_1()
.child(ProfileModalHeader::new(
format!("{profile_name} — Configure Default Model"),
Some(IconName::Ai),
))
.child(ListSeparator)
.child(v_flex().w(rems(34.)).child(model_picker.clone()))
.child(ListSeparator)
.child(go_back_item)
.into_any_element()
}
Mode::ConfigureMcps {
profile_id,
tool_picker,

View File

@@ -314,7 +314,6 @@ impl PickerDelegate for ToolPickerDelegate {
)
})
.collect(),
default_model: default_profile.default_model.clone(),
});
if let Some(server_id) = server_id {

View File

@@ -47,7 +47,6 @@ impl AgentModelSelector {
}
}
},
true, // Use popover styles for picker
window,
cx,
)

View File

@@ -452,7 +452,6 @@ impl CodegenAlternative {
role: Role::User,
content: Vec::new(),
cache: false,
reasoning_details: None,
};
if let Some(context_task) = context_task {

View File

@@ -278,8 +278,6 @@ impl ContextPickerCompletionProvider {
icon_path: Some(mode.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
@@ -388,8 +386,6 @@ impl ContextPickerCompletionProvider {
icon_path: Some(action.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
@@ -421,8 +417,6 @@ impl ContextPickerCompletionProvider {
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(thread_entry.title().to_string(), None),
match_start: None,
snippet_deduplication_key: None,
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
@@ -490,8 +484,6 @@ impl ContextPickerCompletionProvider {
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(rules.title.to_string(), None),
match_start: None,
snippet_deduplication_key: None,
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
@@ -532,8 +524,6 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(IconName::ToolWeb.path().into()),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
IconName::ToolWeb.path().into(),
@@ -622,8 +612,6 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(completion_icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
crease_icon_path,
@@ -701,8 +689,6 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(IconName::Code.path().into()),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
IconName::Code.path().into(),

View File

@@ -1,6 +1,6 @@
use std::{cmp::Reverse, sync::Arc};
use collections::IndexMap;
use collections::{HashSet, IndexMap};
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
use language_model::{
@@ -19,26 +19,14 @@ pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
pub fn language_model_selector(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
popover_styles: bool,
window: &mut Window,
cx: &mut Context<LanguageModelSelector>,
) -> LanguageModelSelector {
let delegate = LanguageModelPickerDelegate::new(
get_active_model,
on_model_changed,
popover_styles,
window,
cx,
);
if popover_styles {
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
.max_height(Some(rems(20.).into()))
} else {
Picker::list(delegate, window, cx).show_scrollbar(true)
}
let delegate = LanguageModelPickerDelegate::new(get_active_model, on_model_changed, window, cx);
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
.max_height(Some(rems(20.).into()))
}
fn all_models(cx: &App) -> GroupedModels {
@@ -57,7 +45,7 @@ fn all_models(cx: &App) -> GroupedModels {
})
.collect();
let all = providers
let other = providers
.iter()
.flat_map(|provider| {
provider
@@ -70,7 +58,7 @@ fn all_models(cx: &App) -> GroupedModels {
})
.collect();
GroupedModels::new(all, recommended)
GroupedModels::new(other, recommended)
}
#[derive(Clone)]
@@ -87,14 +75,12 @@ pub struct LanguageModelPickerDelegate {
selected_index: usize,
_authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>,
popover_styles: bool,
}
impl LanguageModelPickerDelegate {
fn new(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
popover_styles: bool,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Self {
@@ -127,7 +113,6 @@ impl LanguageModelPickerDelegate {
}
},
)],
popover_styles,
}
}
@@ -210,24 +195,33 @@ impl LanguageModelPickerDelegate {
struct GroupedModels {
recommended: Vec<ModelInfo>,
all: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>,
other: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>,
}
impl GroupedModels {
pub fn new(all: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
let mut all_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
for model in all {
pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
let recommended_ids = recommended
.iter()
.map(|info| (info.model.provider_id(), info.model.id()))
.collect::<HashSet<_>>();
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
for model in other {
if recommended_ids.contains(&(model.model.provider_id(), model.model.id())) {
continue;
}
let provider = model.model.provider_id();
if let Some(models) = all_by_provider.get_mut(&provider) {
if let Some(models) = other_by_provider.get_mut(&provider) {
models.push(model);
} else {
all_by_provider.insert(provider, vec![model]);
other_by_provider.insert(provider, vec![model]);
}
}
Self {
recommended,
all: all_by_provider,
other: other_by_provider,
}
}
@@ -243,7 +237,7 @@ impl GroupedModels {
);
}
for models in self.all.values() {
for models in self.other.values() {
if models.is_empty() {
continue;
}
@@ -258,6 +252,20 @@ impl GroupedModels {
}
entries
}
fn model_infos(&self) -> Vec<ModelInfo> {
let other = self
.other
.values()
.flat_map(|model| model.iter())
.cloned()
.collect::<Vec<_>>();
self.recommended
.iter()
.chain(&other)
.cloned()
.collect::<Vec<_>>()
}
}
enum LanguageModelPickerEntry {
@@ -402,9 +410,8 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.collect::<Vec<_>>();
let available_models = all_models
.all
.values()
.flat_map(|models| models.iter())
.model_infos()
.iter()
.filter(|m| configured_provider_ids.contains(&m.model.provider_id()))
.cloned()
.collect::<Vec<_>>();
@@ -523,10 +530,6 @@ impl PickerDelegate for LanguageModelPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<gpui::AnyElement> {
if !self.popover_styles {
return None;
}
Some(
h_flex()
.w_full()
@@ -742,52 +745,46 @@ mod tests {
}
#[gpui::test]
fn test_recommended_models_also_appear_in_other(_cx: &mut TestAppContext) {
fn test_exclude_recommended_models(_cx: &mut TestAppContext) {
let recommended_models = create_models(vec![("zed", "claude")]);
let all_models = create_models(vec![
("zed", "claude"), // Should also appear in "other"
("zed", "claude"), // Should be filtered out from "other"
("zed", "gemini"),
("copilot", "o3"),
]);
let grouped_models = GroupedModels::new(all_models, recommended_models);
let actual_all_models = grouped_models
.all
let actual_other_models = grouped_models
.other
.values()
.flatten()
.cloned()
.collect::<Vec<_>>();
// Recommended models should also appear in "all"
assert_models_eq(
actual_all_models,
vec!["zed/claude", "zed/gemini", "copilot/o3"],
);
// Recommended models should not appear in "other"
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/o3"]);
}
#[gpui::test]
fn test_models_from_different_providers(_cx: &mut TestAppContext) {
fn test_dont_exclude_models_from_other_providers(_cx: &mut TestAppContext) {
let recommended_models = create_models(vec![("zed", "claude")]);
let all_models = create_models(vec![
("zed", "claude"), // Should also appear in "other"
("zed", "claude"), // Should be filtered out from "other"
("zed", "gemini"),
("copilot", "claude"), // Different provider, should appear in "other"
("copilot", "claude"), // Should not be filtered out from "other"
]);
let grouped_models = GroupedModels::new(all_models, recommended_models);
let actual_all_models = grouped_models
.all
let actual_other_models = grouped_models
.other
.values()
.flatten()
.cloned()
.collect::<Vec<_>>();
// All models should appear in "all" regardless of recommended status
assert_models_eq(
actual_all_models,
vec!["zed/claude", "zed/gemini", "copilot/claude"],
);
// Recommended models should not appear in "other"
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/claude"]);
}
}

View File

@@ -15,8 +15,8 @@ use std::{
sync::{Arc, atomic::AtomicBool},
};
use ui::{
DocumentationAside, DocumentationEdge, DocumentationSide, HighlightedLabel, KeyBinding,
LabelSize, ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
DocumentationAside, DocumentationEdge, DocumentationSide, HighlightedLabel, LabelSize,
ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
/// Trait for types that can provide and manage agent profiles
@@ -81,7 +81,6 @@ impl ProfileSelector {
self.provider.clone(),
self.profiles.clone(),
cx.background_executor().clone(),
self.focus_handle.clone(),
cx,
);
@@ -208,7 +207,6 @@ pub(crate) struct ProfilePickerDelegate {
selected_index: usize,
query: String,
cancel: Option<Arc<AtomicBool>>,
focus_handle: FocusHandle,
}
impl ProfilePickerDelegate {
@@ -217,7 +215,6 @@ impl ProfilePickerDelegate {
provider: Arc<dyn ProfileProvider>,
profiles: AvailableProfiles,
background: BackgroundExecutor,
focus_handle: FocusHandle,
cx: &mut Context<ProfileSelector>,
) -> Self {
let candidates = Self::candidates_from(profiles);
@@ -234,7 +231,6 @@ impl ProfilePickerDelegate {
selected_index: 0,
query: String::new(),
cancel: None,
focus_handle,
};
this.selected_index = this
@@ -598,26 +594,20 @@ impl PickerDelegate for ProfilePickerDelegate {
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<gpui::AnyElement> {
let focus_handle = self.focus_handle.clone();
Some(
h_flex()
.w_full()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.p_1p5()
.p_1()
.gap_4()
.justify_between()
.child(
Button::new("configure", "Configure")
.full_width()
.style(ButtonStyle::Outlined)
.key_binding(
KeyBinding::for_action_in(
&ManageProfiles::default(),
&focus_handle,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
)
.icon(IconName::Settings)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
}),
@@ -669,25 +659,20 @@ mod tests {
is_builtin: true,
}];
cx.update(|cx| {
let focus_handle = cx.focus_handle();
let delegate = ProfilePickerDelegate {
fs: FakeFs::new(cx.executor()),
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
background: cx.executor(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: Vec::new(),
selected_index: 0,
query: String::new(),
cancel: None,
};
let delegate = ProfilePickerDelegate {
fs: FakeFs::new(cx.background_executor().clone()),
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
background: cx.background_executor().clone(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: Vec::new(),
selected_index: 0,
query: String::new(),
cancel: None,
focus_handle,
};
let matches = Vec::new(); // No matches
let _entries = delegate.entries_from_matches(matches);
});
let matches = Vec::new(); // No matches
let _entries = delegate.entries_from_matches(matches);
}
#[gpui::test]
@@ -705,35 +690,30 @@ mod tests {
},
];
cx.update(|cx| {
let focus_handle = cx.focus_handle();
let delegate = ProfilePickerDelegate {
fs: FakeFs::new(cx.executor()),
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
background: cx.executor(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: vec![
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 0,
positions: Vec::new(),
}),
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 1,
positions: Vec::new(),
}),
],
selected_index: 0,
query: String::new(),
cancel: None,
};
let delegate = ProfilePickerDelegate {
fs: FakeFs::new(cx.background_executor().clone()),
provider: Arc::new(TestProfileProvider::new(AgentProfileId("write".into()))),
background: cx.background_executor().clone(),
candidates,
string_candidates: Arc::new(Vec::new()),
filtered_entries: vec![
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 0,
positions: Vec::new(),
}),
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 1,
positions: Vec::new(),
}),
],
selected_index: 0,
query: String::new(),
cancel: None,
focus_handle,
};
// Active profile should be found at index 0
let active_index = delegate.index_of_profile(&AgentProfileId("write".into()));
assert_eq!(active_index, Some(0));
});
// Active profile should be found at index 0
let active_index = delegate.index_of_profile(&AgentProfileId("write".into()));
assert_eq!(active_index, Some(0));
}
struct TestProfileProvider {

View File

@@ -127,8 +127,6 @@ impl SlashCommandCompletionProvider {
new_text,
label: command.label(cx),
icon_path: None,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm,
source: CompletionSource::Custom,
@@ -234,8 +232,6 @@ impl SlashCommandCompletionProvider {
icon_path: None,
new_text,
documentation: None,
match_start: None,
snippet_deduplication_key: None,
confirm,
insert_text_mode: None,
source: CompletionSource::Custom,

View File

@@ -277,7 +277,6 @@ impl TerminalInlineAssistant {
role: Role::User,
content: vec![],
cache: false,
reasoning_details: None,
};
context_load_task

View File

@@ -314,7 +314,6 @@ impl TextThreadEditor {
)
});
},
true, // Use popover styles for picker
window,
cx,
)
@@ -478,7 +477,7 @@ impl TextThreadEditor {
editor.insert(&format!("/{name}"), window, cx);
if command.accepts_arguments() {
editor.insert(" ", window, cx);
editor.show_completions(&ShowCompletions, window, cx);
editor.show_completions(&ShowCompletions::default(), window, cx);
}
});
});
@@ -2592,11 +2591,12 @@ impl SearchableItem for TextThreadEditor {
&mut self,
index: usize,
matches: &[Self::Match],
collapse: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.activate_match(index, matches, window, cx);
editor.activate_match(index, matches, collapse, window, cx);
});
}

View File

@@ -67,13 +67,6 @@ pub enum Model {
alias = "claude-opus-4-1-thinking-latest"
)]
ClaudeOpus4_1Thinking,
#[serde(rename = "claude-opus-4-5", alias = "claude-opus-4-5-latest")]
ClaudeOpus4_5,
#[serde(
rename = "claude-opus-4-5-thinking",
alias = "claude-opus-4-5-thinking-latest"
)]
ClaudeOpus4_5Thinking,
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
ClaudeSonnet4,
#[serde(
@@ -138,14 +131,6 @@ impl Model {
}
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-opus-4-5-thinking") {
return Ok(Self::ClaudeOpus4_5Thinking);
}
if id.starts_with("claude-opus-4-5") {
return Ok(Self::ClaudeOpus4_5);
}
if id.starts_with("claude-opus-4-1-thinking") {
return Ok(Self::ClaudeOpus4_1Thinking);
}
@@ -223,8 +208,6 @@ impl Model {
Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
Self::ClaudeOpus4_5 => "claude-opus-4-5-latest",
Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-thinking-latest",
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest",
@@ -247,7 +230,6 @@ impl Model {
match self {
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
Self::ClaudeOpus4_5 | Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-20251101",
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-20250929",
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
@@ -267,8 +249,6 @@ impl Model {
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
Self::ClaudeOpus4_5 => "Claude Opus 4.5",
Self::ClaudeOpus4_5Thinking => "Claude Opus 4.5 Thinking",
Self::ClaudeSonnet4 => "Claude Sonnet 4",
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
@@ -294,8 +274,6 @@ impl Model {
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -325,8 +303,6 @@ impl Model {
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -350,8 +326,6 @@ impl Model {
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -374,8 +348,6 @@ impl Model {
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -400,7 +372,6 @@ impl Model {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_5
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4_5
| Self::Claude3_5Sonnet
@@ -412,7 +383,6 @@ impl Model {
| Self::Claude3Haiku => AnthropicModelMode::Default,
Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5Thinking
| Self::ClaudeHaiku4_5Thinking

View File

@@ -254,7 +254,6 @@ impl PasswordProxy {
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path).await?;
// todo(shell): There might be no powershell on the system
#[cfg(target_os = "windows")]
let askpass_helper = format!(
"powershell.exe -ExecutionPolicy Bypass -File {}",

View File

@@ -1416,7 +1416,6 @@ impl TextThread {
role: Role::User,
content: vec!["Respond only with OK, nothing else.".into()],
cache: false,
reasoning_details: None,
});
req
};
@@ -2084,11 +2083,6 @@ impl TextThread {
}
}
LanguageModelCompletionEvent::StartMessage { .. } => {}
LanguageModelCompletionEvent::ReasoningDetails(_) => {
// ReasoningDetails are metadata (signatures, encrypted data, format info)
// used for request/response validation, not UI content.
// The displayable thinking text is already handled by the Thinking event.
}
LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason;
}
@@ -2312,7 +2306,6 @@ impl TextThread {
role: message.role,
content: Vec::new(),
cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor),
reasoning_details: None,
};
while let Some(content) = contents.peek() {
@@ -2684,7 +2677,6 @@ impl TextThread {
role: Role::User,
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
cache: false,
reasoning_details: None,
});
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can

View File

@@ -33,9 +33,4 @@ workspace.workspace = true
which.workspace = true
[dev-dependencies]
ctor.workspace = true
clock= { workspace = true, "features" = ["test-support"] }
futures.workspace = true
gpui = { workspace = true, "features" = ["test-support"] }
parking_lot.workspace = true
zlog.workspace = true

View File

@@ -1,11 +1,12 @@
use anyhow::{Context as _, Result};
use client::Client;
use client::{Client, TelemetrySettings};
use db::RELEASE_CHANNEL;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, SemanticVersion,
Task, Window, actions,
};
use http_client::{HttpClient, HttpClientWithUrl};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use paths::remote_servers_dir;
use release_channel::{AppCommitSha, ReleaseChannel};
use serde::{Deserialize, Serialize};
@@ -40,23 +41,22 @@ actions!(
]
);
#[derive(Serialize)]
struct UpdateRequestBody {
installation_id: Option<Arc<str>>,
release_channel: Option<&'static str>,
telemetry: bool,
is_staff: Option<bool>,
destination: &'static str,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VersionCheckType {
Sha(AppCommitSha),
Semantic(SemanticVersion),
}
#[derive(Serialize, Debug)]
pub struct AssetQuery<'a> {
asset: &'a str,
os: &'a str,
arch: &'a str,
metrics_id: Option<&'a str>,
system_id: Option<&'a str>,
is_staff: Option<bool>,
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub enum AutoUpdateStatus {
Idle,
Checking,
@@ -66,31 +66,6 @@ pub enum AutoUpdateStatus {
Errored { error: Arc<anyhow::Error> },
}
impl PartialEq for AutoUpdateStatus {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(AutoUpdateStatus::Idle, AutoUpdateStatus::Idle) => true,
(AutoUpdateStatus::Checking, AutoUpdateStatus::Checking) => true,
(
AutoUpdateStatus::Downloading { version: v1 },
AutoUpdateStatus::Downloading { version: v2 },
) => v1 == v2,
(
AutoUpdateStatus::Installing { version: v1 },
AutoUpdateStatus::Installing { version: v2 },
) => v1 == v2,
(
AutoUpdateStatus::Updated { version: v1 },
AutoUpdateStatus::Updated { version: v2 },
) => v1 == v2,
(AutoUpdateStatus::Errored { error: e1 }, AutoUpdateStatus::Errored { error: e2 }) => {
e1.to_string() == e2.to_string()
}
_ => false,
}
}
}
impl AutoUpdateStatus {
pub fn is_updated(&self) -> bool {
matches!(self, Self::Updated { .. })
@@ -100,13 +75,13 @@ impl AutoUpdateStatus {
pub struct AutoUpdater {
status: AutoUpdateStatus,
current_version: SemanticVersion,
client: Arc<Client>,
http_client: Arc<HttpClientWithUrl>,
pending_poll: Option<Task<Option<()>>>,
quit_subscription: Option<gpui::Subscription>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ReleaseAsset {
#[derive(Deserialize, Clone, Debug)]
pub struct JsonRelease {
pub version: String,
pub url: String,
}
@@ -162,7 +137,7 @@ struct GlobalAutoUpdate(Option<Entity<AutoUpdater>>);
impl Global for GlobalAutoUpdate {}
pub fn init(client: Arc<Client>, cx: &mut App) {
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
workspace.register_action(|_, action, window, cx| check(action, window, cx));
@@ -174,7 +149,7 @@ pub fn init(client: Arc<Client>, cx: &mut App) {
let version = release_channel::AppVersion::global(cx);
let auto_updater = cx.new(|cx| {
let updater = AutoUpdater::new(version, client, cx);
let updater = AutoUpdater::new(version, http_client, cx);
let poll_for_updates = ReleaseChannel::try_global(cx)
.map(|channel| channel.poll_for_updates())
@@ -258,7 +233,7 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> {
let current_version = auto_updater.current_version;
let release_channel = release_channel.dev_name();
let path = format!("/releases/{release_channel}/{current_version}");
let url = &auto_updater.client.http_client().build_url(&path);
let url = &auto_updater.http_client.build_url(&path);
cx.open_url(url);
}
ReleaseChannel::Nightly => {
@@ -321,7 +296,11 @@ impl AutoUpdater {
cx.default_global::<GlobalAutoUpdate>().0.clone()
}
fn new(current_version: SemanticVersion, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
fn new(
current_version: SemanticVersion,
http_client: Arc<HttpClientWithUrl>,
cx: &mut Context<Self>,
) -> Self {
// On windows, executable files cannot be overwritten while they are
// running, so we must wait to overwrite the application until quitting
// or restarting. When quitting the app, we spawn the auto update helper
@@ -342,7 +321,7 @@ impl AutoUpdater {
Self {
status: AutoUpdateStatus::Idle,
current_version,
client,
http_client,
pending_poll: None,
quit_subscription,
}
@@ -350,7 +329,8 @@ impl AutoUpdater {
pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
if cfg!(target_os = "windows") {
#[cfg(target_os = "windows")]
{
use util::ResultExt;
cleanup_windows()
@@ -374,7 +354,7 @@ impl AutoUpdater {
cx.notify();
self.pending_poll = Some(cx.spawn(async move |this, cx| {
let result = Self::update(this.upgrade()?, cx).await;
let result = Self::update(this.upgrade()?, cx.clone()).await;
this.update(cx, |this, cx| {
this.pending_poll = None;
if let Err(error) = result {
@@ -420,10 +400,10 @@ impl AutoUpdater {
// you can override this function. You should also update get_remote_server_release_url to return
// Ok(None).
pub async fn download_remote_server_release(
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
os: &str,
arch: &str,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
cx: &mut AsyncApp,
) -> Result<PathBuf> {
@@ -435,13 +415,13 @@ impl AutoUpdater {
})??;
set_status("Fetching remote server release", cx);
let release = Self::get_release_asset(
let release = Self::get_release(
&this,
release_channel,
version,
"zed-remote-server",
os,
arch,
version,
Some(release_channel),
cx,
)
.await?;
@@ -452,7 +432,7 @@ impl AutoUpdater {
let version_path = platform_dir.join(format!("{}.gz", release.version));
smol::fs::create_dir_all(&platform_dir).await.ok();
let client = this.read_with(cx, |this, _| this.client.http_client())?;
let client = this.read_with(cx, |this, _| this.http_client.clone())?;
if smol::fs::metadata(&version_path).await.is_err() {
log::info!(
@@ -460,19 +440,19 @@ impl AutoUpdater {
release.version
);
set_status("Downloading remote server", cx);
download_remote_server_binary(&version_path, release, client).await?;
download_remote_server_binary(&version_path, release, client, cx).await?;
}
Ok(version_path)
}
pub async fn get_remote_server_release_url(
channel: ReleaseChannel,
version: Option<SemanticVersion>,
os: &str,
arch: &str,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncApp,
) -> Result<Option<String>> {
) -> Result<Option<(String, String)>> {
let this = cx.update(|cx| {
cx.default_global::<GlobalAutoUpdate>()
.0
@@ -480,99 +460,108 @@ impl AutoUpdater {
.context("auto-update not initialized")
})??;
let release =
Self::get_release_asset(&this, channel, version, "zed-remote-server", os, arch, cx)
.await?;
let release = Self::get_release(
&this,
"zed-remote-server",
os,
arch,
version,
Some(release_channel),
cx,
)
.await?;
Ok(Some(release.url))
let update_request_body = build_remote_server_update_request_body(cx)?;
let body = serde_json::to_string(&update_request_body)?;
Ok(Some((release.url, body)))
}
async fn get_release_asset(
async fn get_release(
this: &Entity<Self>,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
asset: &str,
os: &str,
arch: &str,
version: Option<SemanticVersion>,
release_channel: Option<ReleaseChannel>,
cx: &mut AsyncApp,
) -> Result<ReleaseAsset> {
let client = this.read_with(cx, |this, _| this.client.clone())?;
) -> Result<JsonRelease> {
let client = this.read_with(cx, |this, _| this.http_client.clone())?;
let (system_id, metrics_id, is_staff) = if client.telemetry().metrics_enabled() {
(
client.telemetry().system_id(),
client.telemetry().metrics_id(),
client.telemetry().is_staff(),
)
if let Some(version) = version {
let channel = release_channel.map(|c| c.dev_name()).unwrap_or("stable");
let url = format!("/api/releases/{channel}/{version}/{asset}-{os}-{arch}.gz?update=1",);
Ok(JsonRelease {
version: version.to_string(),
url: client.build_url(&url),
})
} else {
(None, None, None)
};
let mut url_string = client.build_url(&format!(
"/api/releases/latest?asset={}&os={}&arch={}",
asset, os, arch
));
if let Some(param) = release_channel.and_then(|c| c.release_query_param()) {
url_string += "&";
url_string += param;
}
let version = if let Some(version) = version {
version.to_string()
} else {
"latest".to_string()
};
let http_client = client.http_client();
let mut response = client.get(&url_string, Default::default(), true).await?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
let path = format!("/releases/{}/{}/asset", release_channel.dev_name(), version,);
let url = http_client.build_zed_cloud_url_with_query(
&path,
AssetQuery {
os,
arch,
asset,
metrics_id: metrics_id.as_deref(),
system_id: system_id.as_deref(),
is_staff: is_staff,
},
)?;
let mut response = http_client
.get(url.as_str(), Default::default(), true)
.await?;
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await?;
anyhow::ensure!(
response.status().is_success(),
"failed to fetch release: {:?}",
String::from_utf8_lossy(&body),
);
serde_json::from_slice(body.as_slice()).with_context(|| {
format!(
"error deserializing release {:?}",
anyhow::ensure!(
response.status().is_success(),
"failed to fetch release: {:?}",
String::from_utf8_lossy(&body),
)
})
);
serde_json::from_slice(body.as_slice()).with_context(|| {
format!(
"error deserializing release {:?}",
String::from_utf8_lossy(&body),
)
})
}
}
async fn update(this: Entity<Self>, cx: &mut AsyncApp) -> Result<()> {
async fn get_latest_release(
this: &Entity<Self>,
asset: &str,
os: &str,
arch: &str,
release_channel: Option<ReleaseChannel>,
cx: &mut AsyncApp,
) -> Result<JsonRelease> {
Self::get_release(this, asset, os, arch, None, release_channel, cx).await
}
async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
let (client, installed_version, previous_status, release_channel) =
this.read_with(cx, |this, cx| {
this.read_with(&cx, |this, cx| {
(
this.client.http_client(),
this.http_client.clone(),
this.current_version,
this.status.clone(),
ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable),
ReleaseChannel::try_global(cx),
)
})?;
Self::check_dependencies()?;
this.update(cx, |this, cx| {
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Checking;
log::info!("Auto Update: checking for updates");
cx.notify();
})?;
let fetched_release_data =
Self::get_release_asset(&this, release_channel, None, "zed", OS, ARCH, cx).await?;
Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
let fetched_version = fetched_release_data.clone().version;
let app_commit_sha = cx.update(|cx| AppCommitSha::try_global(cx).map(|sha| sha.full()));
let newer_version = Self::check_if_fetched_version_is_newer(
release_channel,
*RELEASE_CHANNEL,
app_commit_sha,
installed_version,
fetched_version,
@@ -580,7 +569,7 @@ impl AutoUpdater {
)?;
let Some(newer_version) = newer_version else {
return this.update(cx, |this, cx| {
return this.update(&mut cx, |this, cx| {
let status = match previous_status {
AutoUpdateStatus::Updated { .. } => previous_status,
_ => AutoUpdateStatus::Idle,
@@ -590,7 +579,7 @@ impl AutoUpdater {
});
};
this.update(cx, |this, cx| {
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Downloading {
version: newer_version.clone(),
};
@@ -599,21 +588,21 @@ impl AutoUpdater {
let installer_dir = InstallerDir::new().await?;
let target_path = Self::target_path(&installer_dir).await?;
download_release(&target_path, fetched_release_data, client).await?;
download_release(&target_path, fetched_release_data, client, &cx).await?;
this.update(cx, |this, cx| {
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Installing {
version: newer_version.clone(),
};
cx.notify();
})?;
let new_binary_path = Self::install_release(installer_dir, target_path, cx).await?;
let new_binary_path = Self::install_release(installer_dir, target_path, &cx).await?;
if let Some(new_binary_path) = new_binary_path {
cx.update(|cx| cx.set_restart_path(new_binary_path))?;
}
this.update(cx, |this, cx| {
this.update(&mut cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated {
@@ -692,12 +681,6 @@ impl AutoUpdater {
target_path: PathBuf,
cx: &AsyncApp,
) -> Result<Option<PathBuf>> {
#[cfg(test)]
if let Some(test_install) =
cx.try_read_global::<tests::InstallOverride, _>(|g, _| g.0.clone())
{
return test_install(target_path, cx);
}
match OS {
"macos" => install_release_macos(&installer_dir, target_path, cx).await,
"linux" => install_release_linux(&installer_dir, target_path, cx).await,
@@ -748,13 +731,16 @@ impl AutoUpdater {
async fn download_remote_server_binary(
target_path: &PathBuf,
release: ReleaseAsset,
release: JsonRelease,
client: Arc<HttpClientWithUrl>,
cx: &AsyncApp,
) -> Result<()> {
let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?;
let mut temp_file = File::create(&temp).await?;
let update_request_body = build_remote_server_update_request_body(cx)?;
let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
let mut response = client.get(&release.url, Default::default(), true).await?;
let mut response = client.get(&release.url, request_body, true).await?;
anyhow::ensure!(
response.status().is_success(),
"failed to download remote server release: {:?}",
@@ -766,19 +752,65 @@ async fn download_remote_server_binary(
Ok(())
}
fn build_remote_server_update_request_body(cx: &AsyncApp) -> Result<UpdateRequestBody> {
let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
let telemetry = Client::global(cx).telemetry().clone();
let is_staff = telemetry.is_staff();
let installation_id = telemetry.installation_id();
let release_channel =
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
(
installation_id,
release_channel,
telemetry_enabled,
is_staff,
)
})?;
Ok(UpdateRequestBody {
installation_id,
release_channel,
telemetry: telemetry_enabled,
is_staff,
destination: "remote",
})
}
async fn download_release(
target_path: &Path,
release: ReleaseAsset,
release: JsonRelease,
client: Arc<HttpClientWithUrl>,
cx: &AsyncApp,
) -> Result<()> {
let mut target_file = File::create(&target_path).await?;
let mut response = client.get(&release.url, Default::default(), true).await?;
anyhow::ensure!(
response.status().is_success(),
"failed to download update: {:?}",
response.status()
);
let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
let telemetry = Client::global(cx).telemetry().clone();
let is_staff = telemetry.is_staff();
let installation_id = telemetry.installation_id();
let release_channel =
ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
(
installation_id,
release_channel,
telemetry_enabled,
is_staff,
)
})?;
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
installation_id,
release_channel,
telemetry: telemetry_enabled,
is_staff,
destination: "local",
})?);
let mut response = client.get(&release.url, request_body, true).await?;
smol::io::copy(response.body_mut(), &mut target_file).await?;
log::info!("downloaded update. path:{:?}", target_path);
@@ -902,16 +934,28 @@ async fn install_release_macos(
Ok(None)
}
#[cfg(target_os = "windows")]
async fn cleanup_windows() -> Result<()> {
use util::ResultExt;
let parent = std::env::current_exe()?
.parent()
.context("No parent dir for Zed.exe")?
.to_owned();
// keep in sync with crates/auto_update_helper/src/updater.rs
_ = smol::fs::remove_dir(parent.join("updates")).await;
_ = smol::fs::remove_dir(parent.join("install")).await;
_ = smol::fs::remove_dir(parent.join("old")).await;
smol::fs::remove_dir(parent.join("updates"))
.await
.context("failed to remove updates dir")
.log_err();
smol::fs::remove_dir(parent.join("install"))
.await
.context("failed to remove install dir")
.log_err();
smol::fs::remove_dir(parent.join("old"))
.await
.context("failed to remove old version dir")
.log_err();
Ok(())
}
@@ -966,33 +1010,11 @@ pub async fn finalize_auto_update_on_quit() {
#[cfg(test)]
mod tests {
use client::Client;
use clock::FakeSystemClock;
use futures::channel::oneshot;
use gpui::TestAppContext;
use http_client::{FakeHttpClient, Response};
use settings::default_settings;
use std::{
rc::Rc,
sync::{
Arc,
atomic::{self, AtomicBool},
},
};
use tempfile::tempdir;
#[ctor::ctor]
fn init_logger() {
zlog::init_test();
}
use super::*;
pub(super) struct InstallOverride(
pub Rc<dyn Fn(PathBuf, &AsyncApp) -> Result<Option<PathBuf>>>,
);
impl Global for InstallOverride {}
#[gpui::test]
fn test_auto_update_defaults_to_true(cx: &mut TestAppContext) {
cx.update(|cx| {
@@ -1008,115 +1030,6 @@ mod tests {
});
}
#[gpui::test]
async fn test_auto_update_downloads(cx: &mut TestAppContext) {
cx.background_executor.allow_parking();
zlog::init_test();
let release_available = Arc::new(AtomicBool::new(false));
let (dmg_tx, dmg_rx) = oneshot::channel::<String>();
cx.update(|cx| {
settings::init(cx);
let current_version = SemanticVersion::new(0, 100, 0);
release_channel::init_test(current_version, ReleaseChannel::Stable, cx);
let clock = Arc::new(FakeSystemClock::new());
let release_available = Arc::clone(&release_available);
let dmg_rx = Arc::new(parking_lot::Mutex::new(Some(dmg_rx)));
let fake_client_http = FakeHttpClient::create(move |req| {
let release_available = release_available.load(atomic::Ordering::Relaxed);
let dmg_rx = dmg_rx.clone();
async move {
if req.uri().path() == "/releases/stable/latest/asset" {
if release_available {
return Ok(Response::builder().status(200).body(
r#"{"version":"0.100.1","url":"https://test.example/new-download"}"#.into()
).unwrap());
} else {
return Ok(Response::builder().status(200).body(
r#"{"version":"0.100.0","url":"https://test.example/old-download"}"#.into()
).unwrap());
}
} else if req.uri().path() == "/new-download" {
return Ok(Response::builder().status(200).body({
let dmg_rx = dmg_rx.lock().take().unwrap();
dmg_rx.await.unwrap().into()
}).unwrap());
}
Ok(Response::builder().status(404).body("".into()).unwrap())
}
});
let client = Client::new(clock, fake_client_http, cx);
crate::init(client, cx);
});
let auto_updater = cx.update(|cx| AutoUpdater::get(cx).expect("auto updater should exist"));
cx.background_executor.run_until_parked();
auto_updater.read_with(cx, |updater, _| {
assert_eq!(updater.status(), AutoUpdateStatus::Idle);
assert_eq!(updater.current_version(), SemanticVersion::new(0, 100, 0));
});
release_available.store(true, atomic::Ordering::SeqCst);
cx.background_executor.advance_clock(POLL_INTERVAL);
cx.background_executor.run_until_parked();
loop {
cx.background_executor.timer(Duration::from_millis(0)).await;
cx.run_until_parked();
let status = auto_updater.read_with(cx, |updater, _| updater.status());
if !matches!(status, AutoUpdateStatus::Idle) {
break;
}
}
let status = auto_updater.read_with(cx, |updater, _| updater.status());
assert_eq!(
status,
AutoUpdateStatus::Downloading {
version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
}
);
dmg_tx.send("<fake-zed-update>".to_owned()).unwrap();
let tmp_dir = Arc::new(tempdir().unwrap());
cx.update(|cx| {
let tmp_dir = tmp_dir.clone();
cx.set_global(InstallOverride(Rc::new(move |target_path, _cx| {
let tmp_dir = tmp_dir.clone();
let dest_path = tmp_dir.path().join("zed");
std::fs::copy(&target_path, &dest_path)?;
Ok(Some(dest_path))
})));
});
loop {
cx.background_executor.timer(Duration::from_millis(0)).await;
cx.run_until_parked();
let status = auto_updater.read_with(cx, |updater, _| updater.status());
if !matches!(status, AutoUpdateStatus::Downloading { .. }) {
break;
}
}
let status = auto_updater.read_with(cx, |updater, _| updater.status());
assert_eq!(
status,
AutoUpdateStatus::Updated {
version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
}
);
let will_restart = cx.expect_restart();
cx.update(|cx| cx.restart());
let path = will_restart.await.unwrap().unwrap();
assert_eq!(path, tmp_dir.path().join("zed"));
assert_eq!(std::fs::read_to_string(path).unwrap(), "<fake-zed-update>");
}
#[test]
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
let release_channel = ReleaseChannel::Stable;

View File

@@ -1,6 +1,6 @@
use std::{
cell::LazyCell,
path::Path,
sync::LazyLock,
time::{Duration, Instant},
};
@@ -13,8 +13,8 @@ use windows::Win32::{
use crate::windows_impl::WM_JOB_UPDATED;
pub(crate) struct Job {
pub apply: Box<dyn Fn(&Path) -> Result<()> + Send + Sync>,
pub rollback: Box<dyn Fn(&Path) -> Result<()> + Send + Sync>,
pub apply: Box<dyn Fn(&Path) -> Result<()>>,
pub rollback: Box<dyn Fn(&Path) -> Result<()>>,
}
impl Job {
@@ -154,8 +154,10 @@ impl Job {
}
}
// app is single threaded
#[cfg(not(test))]
pub(crate) static JOBS: LazyLock<[Job; 22]> = LazyLock::new(|| {
#[allow(clippy::declare_interior_mutable_const)]
pub(crate) const JOBS: LazyCell<[Job; 22]> = LazyCell::new(|| {
fn p(value: &str) -> &Path {
Path::new(value)
}
@@ -204,8 +206,10 @@ pub(crate) static JOBS: LazyLock<[Job; 22]> = LazyLock::new(|| {
]
});
// app is single threaded
#[cfg(test)]
pub(crate) static JOBS: LazyLock<[Job; 9]> = LazyLock::new(|| {
#[allow(clippy::declare_interior_mutable_const)]
pub(crate) const JOBS: LazyCell<[Job; 9]> = LazyCell::new(|| {
fn p(value: &str) -> &Path {
Path::new(value)
}

View File

@@ -51,13 +51,6 @@ pub enum Model {
alias = "claude-opus-4-1-thinking-latest"
)]
ClaudeOpus4_1Thinking,
#[serde(rename = "claude-opus-4-5", alias = "claude-opus-4-5-latest")]
ClaudeOpus4_5,
#[serde(
rename = "claude-opus-4-5-thinking",
alias = "claude-opus-4-5-thinking-latest"
)]
ClaudeOpus4_5Thinking,
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
Claude3_5SonnetV2,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
@@ -148,19 +141,7 @@ impl Model {
}
pub fn from_id(id: &str) -> anyhow::Result<Self> {
if id.starts_with("claude-opus-4-5-thinking") {
Ok(Self::ClaudeOpus4_5Thinking)
} else if id.starts_with("claude-opus-4-5") {
Ok(Self::ClaudeOpus4_5)
} else if id.starts_with("claude-opus-4-1-thinking") {
Ok(Self::ClaudeOpus4_1Thinking)
} else if id.starts_with("claude-opus-4-1") {
Ok(Self::ClaudeOpus4_1)
} else if id.starts_with("claude-opus-4-thinking") {
Ok(Self::ClaudeOpus4Thinking)
} else if id.starts_with("claude-opus-4") {
Ok(Self::ClaudeOpus4)
} else if id.starts_with("claude-3-5-sonnet-v2") {
if id.starts_with("claude-3-5-sonnet-v2") {
Ok(Self::Claude3_5SonnetV2)
} else if id.starts_with("claude-3-opus") {
Ok(Self::Claude3Opus)
@@ -197,8 +178,6 @@ impl Model {
Model::ClaudeOpus4_1 => "claude-opus-4-1",
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking",
Model::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking",
Model::ClaudeOpus4_5 => "claude-opus-4-5",
Model::ClaudeOpus4_5Thinking => "claude-opus-4-5-thinking",
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
Model::Claude3Opus => "claude-3-opus",
@@ -266,9 +245,6 @@ impl Model {
Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => {
"anthropic.claude-opus-4-1-20250805-v1:0"
}
Model::ClaudeOpus4_5 | Model::ClaudeOpus4_5Thinking => {
"anthropic.claude-opus-4-5-20251101-v1:0"
}
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
@@ -333,8 +309,6 @@ impl Model {
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
Self::ClaudeOpus4_5 => "Claude Opus 4.5",
Self::ClaudeOpus4_5Thinking => "Claude Opus 4.5 Thinking",
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3Opus => "Claude 3 Opus",
@@ -405,9 +379,7 @@ impl Model {
| Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking => 200_000,
| Self::ClaudeOpus4_1Thinking => 200_000,
Self::AmazonNovaPremier => 1_000_000,
Self::PalmyraWriterX5 => 1_000_000,
Self::PalmyraWriterX4 => 128_000,
@@ -421,11 +393,7 @@ impl Model {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::ClaudeHaiku4_5
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking => 64_000,
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking | Self::ClaudeHaiku4_5 => 64_000,
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
@@ -450,8 +418,6 @@ impl Model {
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -477,8 +443,6 @@ impl Model {
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -520,9 +484,7 @@ impl Model {
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking => true,
| Self::ClaudeOpus4_1Thinking => true,
// Custom models - check if they have cache configuration
Self::Custom {
@@ -544,9 +506,7 @@ impl Model {
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking => Some(BedrockModelCacheConfiguration {
| Self::ClaudeOpus4_1Thinking => Some(BedrockModelCacheConfiguration {
max_cache_anchors: 4,
min_total_token: 1024,
}),
@@ -575,11 +535,11 @@ impl Model {
budget_tokens: Some(4096),
}
}
Model::ClaudeOpus4Thinking
| Model::ClaudeOpus4_1Thinking
| Model::ClaudeOpus4_5Thinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
Model::ClaudeOpus4Thinking | Model::ClaudeOpus4_1Thinking => {
BedrockModelMode::Thinking {
budget_tokens: Some(4096),
}
}
_ => BedrockModelMode::Default,
}
}
@@ -633,8 +593,6 @@ impl Model {
| Model::ClaudeOpus4Thinking
| Model::ClaudeOpus4_1
| Model::ClaudeOpus4_1Thinking
| Model::ClaudeOpus4_5
| Model::ClaudeOpus4_5Thinking
| Model::Claude3Haiku
| Model::Claude3Opus
| Model::Claude3Sonnet

View File

@@ -1487,7 +1487,7 @@ impl Client {
let url = self
.http
.build_zed_cloud_url("/internal/users/impersonate")?;
.build_zed_cloud_url("/internal/users/impersonate", &[])?;
let request = Request::post(url.as_str())
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {api_token}"))

View File

@@ -435,7 +435,7 @@ impl Telemetry {
Some(project_types)
}
fn report_event(self: &Arc<Self>, mut event: Event) {
fn report_event(self: &Arc<Self>, event: Event) {
let mut state = self.state.lock();
// RUST_LOG=telemetry=trace to debug telemetry events
log::trace!(target: "telemetry", "{:?}", event);
@@ -444,12 +444,6 @@ impl Telemetry {
return;
}
match &mut event {
Event::Flexible(event) => event
.event_properties
.insert("event_source".into(), "zed".into()),
};
if state.flush_events_task.is_none() {
let this = self.clone();
state.flush_events_task = Some(self.executor.spawn(async move {

View File

@@ -51,11 +51,3 @@ pub fn external_agents_docs(cx: &App) -> String {
server_url = server_url(cx)
)
}
/// Returns the URL to Zed agent servers documentation.
pub fn agent_server_docs(cx: &App) -> String {
format!(
"{server_url}/docs/extensions/agent-servers",
server_url = server_url(cx)
)
}

View File

@@ -62,7 +62,7 @@ impl CloudApiClient {
let request = self.build_request(
Request::builder().method(Method::GET).uri(
self.http_client
.build_zed_cloud_url("/client/users/me")?
.build_zed_cloud_url("/client/users/me", &[])?
.as_ref(),
),
AsyncBody::default(),
@@ -89,7 +89,7 @@ impl CloudApiClient {
pub fn connect(&self, cx: &App) -> Result<Task<Result<Connection>>> {
let mut connect_url = self
.http_client
.build_zed_cloud_url("/client/users/connect")?;
.build_zed_cloud_url("/client/users/connect", &[])?;
connect_url
.set_scheme(match connect_url.scheme() {
"https" => "wss",
@@ -123,7 +123,7 @@ impl CloudApiClient {
.method(Method::POST)
.uri(
self.http_client
.build_zed_cloud_url("/client/llm_tokens")?
.build_zed_cloud_url("/client/llm_tokens", &[])?
.as_ref(),
)
.when_some(system_id, |builder, system_id| {
@@ -154,7 +154,7 @@ impl CloudApiClient {
let request = build_request(
Request::builder().method(Method::GET).uri(
self.http_client
.build_zed_cloud_url("/client/users/me")?
.build_zed_cloud_url("/client/users/me", &[])?
.as_ref(),
),
AsyncBody::default(),

View File

@@ -73,7 +73,6 @@ pub enum PromptFormat {
MarkedExcerpt,
LabeledSections,
NumLinesUniDiff,
OldTextNewText,
/// Prompt format intended for use via zeta_cli
OnlySnippets,
}
@@ -101,7 +100,6 @@ impl std::fmt::Display for PromptFormat {
PromptFormat::LabeledSections => write!(f, "Labeled Sections"),
PromptFormat::OnlySnippets => write!(f, "Only Snippets"),
PromptFormat::NumLinesUniDiff => write!(f, "Numbered Lines / Unified Diff"),
PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
}
}
}

View File

@@ -56,98 +56,50 @@ const LABELED_SECTIONS_INSTRUCTIONS: &str = indoc! {r#"
const NUMBERED_LINES_INSTRUCTIONS: &str = indoc! {r#"
# Instructions
You are an edit prediction agent in a code editor.
Your job is to predict the next edit that the user will make,
based on their last few edits and their current cursor location.
You are a code completion assistant helping a programmer finish their work. Your task is to:
## Output Format
1. Analyze the edit history to understand what the programmer is trying to achieve
2. Identify any incomplete refactoring or changes that need to be finished
3. Make the remaining edits that a human programmer would logically make next
4. Apply systematic changes consistently across the entire codebase - if you see a pattern starting, complete it everywhere.
You must briefly explain your understanding of the user's goal, in one
or two sentences, and then specify their next edit in the form of a
unified diff, like this:
Focus on:
- Understanding the intent behind the changes (e.g., improving error handling, refactoring APIs, fixing bugs)
- Completing any partially-applied changes across the codebase
- Ensuring consistency with the programming style and patterns already established
- Making edits that maintain or improve code quality
- If the programmer started refactoring one instance of a pattern, find and update ALL similar instances
- Don't write a lot of code if you're not sure what to do
Rules:
- Do not just mechanically apply patterns - reason about what changes make sense given the context and the programmer's apparent goals.
- Do not just fix syntax errors - look for the broader refactoring pattern and apply it systematically throughout the code.
- Write the edits in the unified diff format as shown in the example.
# Example output:
```
--- a/src/myapp/cli.py
+++ b/src/myapp/cli.py
@@ ... @@
import os
import time
import sys
+from constants import LOG_LEVEL_WARNING
@@ ... @@
config.headless()
config.set_interactive(false)
-config.set_log_level(LOG_L)
+config.set_log_level(LOG_LEVEL_WARNING)
config.set_use_color(True)
@@ -1,3 +1,3 @@
-
-
-import sys
+import json
```
## Edit History
# Edit History:
"#};
const UNIFIED_DIFF_REMINDER: &str = indoc! {"
---
Analyze the edit history and the files, then provide the unified diff for your predicted edits.
Please analyze the edit history and the files, then provide the unified diff for your predicted edits.
Do not include the cursor marker in your output.
Your diff should include edited file paths in its file headers (lines beginning with `---` and `+++`).
Do not include line numbers in the hunk headers, use `@@ ... @@`.
Removed lines begin with `-`.
Added lines begin with `+`.
Context lines begin with an extra space.
Context and removed lines are used to match the target edit location, so make sure to include enough of them
to uniquely identify it amongst all excerpts of code provided.
If you're editing multiple files, be sure to reflect filename in the hunk's header.
"};
const XML_TAGS_INSTRUCTIONS: &str = indoc! {r#"
# Instructions
You are an edit prediction agent in a code editor.
Your job is to predict the next edit that the user will make,
based on their last few edits and their current cursor location.
# Output Format
You must briefly explain your understanding of the user's goal, in one
or two sentences, and then specify their next edit, using the following
XML format:
<edits path="my-project/src/myapp/cli.py">
<old_text>
OLD TEXT 1 HERE
</old_text>
<new_text>
NEW TEXT 1 HERE
</new_text>
<old_text>
OLD TEXT 1 HERE
</old_text>
<new_text>
NEW TEXT 1 HERE
</new_text>
</edits>
- Specify the file to edit using the `path` attribute.
- Use `<old_text>` and `<new_text>` tags to replace content
- `<old_text>` must exactly match existing file content, including indentation
- `<old_text>` cannot be empty
- Do not escape quotes, newlines, or other characters within tags
- Always close all tags properly
- Don't include the <|user_cursor|> marker in your output.
# Edit History:
"#};
const OLD_TEXT_NEW_TEXT_REMINDER: &str = indoc! {r#"
---
Remember that the edits in the edit history have already been deployed.
The files are currently as shown in the Code Excerpts section.
"#};
pub fn build_prompt(
request: &predict_edits_v3::PredictEditsRequest,
) -> Result<(String, SectionLabels)> {
@@ -169,9 +121,8 @@ pub fn build_prompt(
EDITABLE_REGION_END_MARKER_WITH_NEWLINE,
),
],
PromptFormat::LabeledSections
| PromptFormat::NumLinesUniDiff
| PromptFormat::OldTextNewText => {
PromptFormat::LabeledSections => vec![(request.cursor_point, CURSOR_MARKER)],
PromptFormat::NumLinesUniDiff => {
vec![(request.cursor_point, CURSOR_MARKER)]
}
PromptFormat::OnlySnippets => vec![],
@@ -181,31 +132,45 @@ pub fn build_prompt(
PromptFormat::MarkedExcerpt => MARKED_EXCERPT_INSTRUCTIONS.to_string(),
PromptFormat::LabeledSections => LABELED_SECTIONS_INSTRUCTIONS.to_string(),
PromptFormat::NumLinesUniDiff => NUMBERED_LINES_INSTRUCTIONS.to_string(),
PromptFormat::OldTextNewText => XML_TAGS_INSTRUCTIONS.to_string(),
// only intended for use via zeta_cli
PromptFormat::OnlySnippets => String::new(),
};
if request.events.is_empty() {
prompt.push_str("(No edit history)\n\n");
} else {
prompt.push_str("Here are the latest edits made by the user, from earlier to later.\n\n");
prompt.push_str(
"The following are the latest edits made by the user, from earlier to later.\n\n",
);
push_events(&mut prompt, &request.events);
}
prompt.push_str(indoc! {"
# Code Excerpts
The cursor marker <|user_cursor|> indicates the current user cursor position.
The file is in current state, edits from edit history have been applied.
"});
if request.prompt_format == PromptFormat::NumLinesUniDiff {
prompt.push_str(indoc! {"
We prepend line numbers (e.g., `123|<actual line>`); they are not part of the file.
"});
}
if request.referenced_declarations.is_empty() {
prompt.push_str(indoc! {"
# File under the cursor:
prompt.push('\n');
The cursor marker <|user_cursor|> indicates the current user cursor position.
The file is in current state, edits from edit history have been applied.
We prepend line numbers (e.g., `123|<actual line>`); they are not part of the file.
"});
} else {
// Note: This hasn't been trained on yet
prompt.push_str(indoc! {"
# Code Excerpts:
The cursor marker <|user_cursor|> indicates the current user cursor position.
Other excerpts of code from the project have been included as context based on their similarity to the code under the cursor.
Context excerpts are not guaranteed to be relevant, so use your own judgement.
Files are in their current state, edits from edit history have been applied.
We prepend line numbers (e.g., `123|<actual line>`); they are not part of the file.
"});
}
} else {
prompt.push_str("\n## Code\n\n");
}
let mut section_labels = Default::default();
@@ -233,14 +198,8 @@ pub fn build_prompt(
}
}
match request.prompt_format {
PromptFormat::NumLinesUniDiff => {
prompt.push_str(UNIFIED_DIFF_REMINDER);
}
PromptFormat::OldTextNewText => {
prompt.push_str(OLD_TEXT_NEW_TEXT_REMINDER);
}
_ => {}
if request.prompt_format == PromptFormat::NumLinesUniDiff {
prompt.push_str(UNIFIED_DIFF_REMINDER);
}
Ok((prompt, section_labels))
@@ -665,7 +624,6 @@ impl<'a> SyntaxBasedPrompt<'a> {
match self.request.prompt_format {
PromptFormat::MarkedExcerpt
| PromptFormat::OnlySnippets
| PromptFormat::OldTextNewText
| PromptFormat::NumLinesUniDiff => {
if range.start.0 > 0 && !skipped_last_snippet {
output.push_str("\n");

View File

@@ -291,6 +291,29 @@ CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
CREATE INDEX "index_channel_chat_participants_on_channel_id" ON "channel_chat_participants" ("channel_id");
CREATE TABLE IF NOT EXISTS "channel_messages" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
"sender_id" INTEGER NOT NULL REFERENCES users (id),
"body" TEXT NOT NULL,
"sent_at" TIMESTAMP,
"edited_at" TIMESTAMP,
"nonce" BLOB NOT NULL,
"reply_to_message_id" INTEGER DEFAULT NULL
);
CREATE INDEX "index_channel_messages_on_channel_id" ON "channel_messages" ("channel_id");
CREATE UNIQUE INDEX "index_channel_messages_on_sender_id_nonce" ON "channel_messages" ("sender_id", "nonce");
CREATE TABLE "channel_message_mentions" (
"message_id" INTEGER NOT NULL REFERENCES channel_messages (id) ON DELETE CASCADE,
"start_offset" INTEGER NOT NULL,
"end_offset" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
PRIMARY KEY (message_id, start_offset)
);
CREATE TABLE "channel_members" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
@@ -385,6 +408,15 @@ CREATE TABLE "observed_buffer_edits" (
CREATE UNIQUE INDEX "index_observed_buffers_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id");
CREATE TABLE IF NOT EXISTS "observed_channel_messages" (
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
"channel_message_id" INTEGER NOT NULL,
PRIMARY KEY (user_id, channel_id)
);
CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id");
CREATE TABLE "notification_kinds" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"name" VARCHAR NOT NULL

View File

@@ -1,3 +0,0 @@
drop table observed_channel_messages;
drop table channel_message_mentions;
drop table channel_messages;

View File

@@ -1 +0,0 @@
drop table embeddings;

View File

@@ -5,6 +5,7 @@ pub mod buffers;
pub mod channels;
pub mod contacts;
pub mod contributors;
pub mod embeddings;
pub mod extensions;
pub mod notifications;
pub mod projects;

View File

@@ -0,0 +1,94 @@
use super::*;
use time::Duration;
use time::OffsetDateTime;
impl Database {
pub async fn get_embeddings(
&self,
model: &str,
digests: &[Vec<u8>],
) -> Result<HashMap<Vec<u8>, Vec<f32>>> {
self.transaction(|tx| async move {
let embeddings = {
let mut db_embeddings = embedding::Entity::find()
.filter(
embedding::Column::Model.eq(model).and(
embedding::Column::Digest
.is_in(digests.iter().map(|digest| digest.as_slice())),
),
)
.stream(&*tx)
.await?;
let mut embeddings = HashMap::default();
while let Some(db_embedding) = db_embeddings.next().await {
let db_embedding = db_embedding?;
embeddings.insert(db_embedding.digest, db_embedding.dimensions);
}
embeddings
};
if !embeddings.is_empty() {
let now = OffsetDateTime::now_utc();
let retrieved_at = PrimitiveDateTime::new(now.date(), now.time());
embedding::Entity::update_many()
.filter(
embedding::Column::Digest
.is_in(embeddings.keys().map(|digest| digest.as_slice())),
)
.col_expr(embedding::Column::RetrievedAt, Expr::value(retrieved_at))
.exec(&*tx)
.await?;
}
Ok(embeddings)
})
.await
}
pub async fn save_embeddings(
&self,
model: &str,
embeddings: &HashMap<Vec<u8>, Vec<f32>>,
) -> Result<()> {
self.transaction(|tx| async move {
embedding::Entity::insert_many(embeddings.iter().map(|(digest, dimensions)| {
let now_offset_datetime = OffsetDateTime::now_utc();
let retrieved_at =
PrimitiveDateTime::new(now_offset_datetime.date(), now_offset_datetime.time());
embedding::ActiveModel {
model: ActiveValue::set(model.to_string()),
digest: ActiveValue::set(digest.clone()),
dimensions: ActiveValue::set(dimensions.clone()),
retrieved_at: ActiveValue::set(retrieved_at),
}
}))
.on_conflict(
OnConflict::columns([embedding::Column::Model, embedding::Column::Digest])
.do_nothing()
.to_owned(),
)
.exec_without_returning(&*tx)
.await?;
Ok(())
})
.await
}
pub async fn purge_old_embeddings(&self) -> Result<()> {
self.transaction(|tx| async move {
embedding::Entity::delete_many()
.filter(
embedding::Column::RetrievedAt
.lte(OffsetDateTime::now_utc() - Duration::days(60)),
)
.exec(&*tx)
.await?;
Ok(())
})
.await
}
}

View File

@@ -66,6 +66,40 @@ impl Database {
.await
}
/// Returns all users flagged as staff.
pub async fn get_staff_users(&self) -> Result<Vec<user::Model>> {
self.transaction(|tx| async {
let tx = tx;
Ok(user::Entity::find()
.filter(user::Column::Admin.eq(true))
.all(&*tx)
.await?)
})
.await
}
/// Returns a user by email address. There are no access checks here, so this should only be used internally.
pub async fn get_user_by_email(&self, email: &str) -> Result<Option<User>> {
self.transaction(|tx| async move {
Ok(user::Entity::find()
.filter(user::Column::EmailAddress.eq(email))
.one(&*tx)
.await?)
})
.await
}
/// Returns a user by GitHub user ID. There are no access checks here, so this should only be used internally.
pub async fn get_user_by_github_user_id(&self, github_user_id: i32) -> Result<Option<User>> {
self.transaction(|tx| async move {
Ok(user::Entity::find()
.filter(user::Column::GithubUserId.eq(github_user_id))
.one(&*tx)
.await?)
})
.await
}
/// Returns a user by GitHub login. There are no access checks here, so this should only be used internally.
pub async fn get_user_by_github_login(&self, github_login: &str) -> Result<Option<User>> {
self.transaction(|tx| async move {
@@ -236,6 +270,39 @@ impl Database {
.await
}
/// Sets "accepted_tos_at" on the user to the given timestamp.
pub async fn set_user_accepted_tos_at(
&self,
id: UserId,
accepted_tos_at: Option<DateTime>,
) -> Result<()> {
self.transaction(|tx| async move {
user::Entity::update_many()
.filter(user::Column::Id.eq(id))
.set(user::ActiveModel {
accepted_tos_at: ActiveValue::set(accepted_tos_at),
..Default::default()
})
.exec(&*tx)
.await?;
Ok(())
})
.await
}
/// hard delete the user.
pub async fn destroy_user(&self, id: UserId) -> Result<()> {
self.transaction(|tx| async move {
access_token::Entity::delete_many()
.filter(access_token::Column::UserId.eq(id))
.exec(&*tx)
.await?;
user::Entity::delete_by_id(id).exec(&*tx).await?;
Ok(())
})
.await
}
/// Find users where github_login ILIKE name_query.
pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result<Vec<User>> {
self.transaction(|tx| async {
@@ -274,4 +341,14 @@ impl Database {
result.push('%');
result
}
pub async fn get_users_missing_github_user_created_at(&self) -> Result<Vec<user::Model>> {
self.transaction(|tx| async move {
Ok(user::Entity::find()
.filter(user::Column::GithubUserCreatedAt.is_null())
.all(&*tx)
.await?)
})
.await
}
}

View File

@@ -6,8 +6,11 @@ pub mod channel;
pub mod channel_buffer_collaborator;
pub mod channel_chat_participant;
pub mod channel_member;
pub mod channel_message;
pub mod channel_message_mention;
pub mod contact;
pub mod contributor;
pub mod embedding;
pub mod extension;
pub mod extension_version;
pub mod follower;
@@ -15,6 +18,7 @@ pub mod language_server;
pub mod notification;
pub mod notification_kind;
pub mod observed_buffer_edits;
pub mod observed_channel_messages;
pub mod project;
pub mod project_collaborator;
pub mod project_repository;

View File

@@ -0,0 +1,47 @@
use crate::db::{ChannelId, MessageId, UserId};
use sea_orm::entity::prelude::*;
use time::PrimitiveDateTime;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "channel_messages")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: MessageId,
pub channel_id: ChannelId,
pub sender_id: UserId,
pub body: String,
pub sent_at: PrimitiveDateTime,
pub edited_at: Option<PrimitiveDateTime>,
pub nonce: Uuid,
pub reply_to_message_id: Option<MessageId>,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id"
)]
Channel,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::SenderId",
to = "super::user::Column::Id"
)]
Sender,
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::Sender.def()
}
}

View File

@@ -0,0 +1,43 @@
use crate::db::{MessageId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "channel_message_mentions")]
pub struct Model {
#[sea_orm(primary_key)]
pub message_id: MessageId,
#[sea_orm(primary_key)]
pub start_offset: i32,
pub end_offset: i32,
pub user_id: UserId,
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::channel_message::Entity",
from = "Column::MessageId",
to = "super::channel_message::Column::Id"
)]
Message,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
MentionedUser,
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Message.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::MentionedUser.def()
}
}

View File

@@ -0,0 +1,18 @@
use sea_orm::entity::prelude::*;
use time::PrimitiveDateTime;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "embeddings")]
pub struct Model {
#[sea_orm(primary_key)]
pub model: String,
#[sea_orm(primary_key)]
pub digest: Vec<u8>,
pub dimensions: Vec<f32>,
pub retrieved_at: PrimitiveDateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -0,0 +1,41 @@
use crate::db::{ChannelId, MessageId, UserId};
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "observed_channel_messages")]
pub struct Model {
#[sea_orm(primary_key)]
pub user_id: UserId,
pub channel_id: ChannelId,
pub channel_message_id: MessageId,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::channel::Entity",
from = "Column::ChannelId",
to = "super::channel::Column::Id"
)]
Channel,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id"
)]
User,
}
impl Related<super::channel::Entity> for Entity {
fn to() -> RelationDef {
Relation::Channel.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -2,7 +2,11 @@ mod buffer_tests;
mod channel_tests;
mod contributor_tests;
mod db_tests;
// we only run postgres tests on macos right now
#[cfg(target_os = "macos")]
mod embedding_tests;
mod extension_tests;
mod user_tests;
use crate::migrations::run_database_migrations;

View File

@@ -1,7 +1,7 @@
use super::*;
use crate::test_both_dbs;
use chrono::Utc;
use pretty_assertions::assert_eq;
use pretty_assertions::{assert_eq, assert_ne};
use std::sync::Arc;
test_both_dbs!(
@@ -457,6 +457,53 @@ async fn test_add_contacts(db: &Arc<Database>) {
);
}
test_both_dbs!(
test_metrics_id,
test_metrics_id_postgres,
test_metrics_id_sqlite
);
async fn test_metrics_id(db: &Arc<Database>) {
let NewUserResult {
user_id: user1,
metrics_id: metrics_id1,
..
} = db
.create_user(
"person1@example.com",
None,
false,
NewUserParams {
github_login: "person1".into(),
github_user_id: 101,
},
)
.await
.unwrap();
let NewUserResult {
user_id: user2,
metrics_id: metrics_id2,
..
} = db
.create_user(
"person2@example.com",
None,
false,
NewUserParams {
github_login: "person2".into(),
github_user_id: 102,
},
)
.await
.unwrap();
assert_eq!(db.get_user_metrics_id(user1).await.unwrap(), metrics_id1);
assert_eq!(db.get_user_metrics_id(user2).await.unwrap(), metrics_id2);
assert_eq!(metrics_id1.len(), 36);
assert_eq!(metrics_id2.len(), 36);
assert_ne!(metrics_id1, metrics_id2);
}
test_both_dbs!(
test_project_count,
test_project_count_postgres,

View File

@@ -0,0 +1,87 @@
use super::TestDb;
use crate::db::embedding;
use collections::HashMap;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, sea_query::Expr};
use std::ops::Sub;
use time::{Duration, OffsetDateTime, PrimitiveDateTime};
// SQLite does not support array arguments, so we only test this against a real postgres instance
#[gpui::test]
async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) {
let test_db = TestDb::postgres(cx.executor());
let db = test_db.db();
let provider = "test_model";
let digest1 = vec![1, 2, 3];
let digest2 = vec![4, 5, 6];
let embeddings = HashMap::from_iter([
(digest1.clone(), vec![0.1, 0.2, 0.3]),
(digest2.clone(), vec![0.4, 0.5, 0.6]),
]);
// Save embeddings
db.save_embeddings(provider, &embeddings).await.unwrap();
// Retrieve embeddings
let retrieved_embeddings = db
.get_embeddings(provider, &[digest1.clone(), digest2.clone()])
.await
.unwrap();
assert_eq!(retrieved_embeddings.len(), 2);
assert!(retrieved_embeddings.contains_key(&digest1));
assert!(retrieved_embeddings.contains_key(&digest2));
// Check if the retrieved embeddings are correct
assert_eq!(retrieved_embeddings[&digest1], vec![0.1, 0.2, 0.3]);
assert_eq!(retrieved_embeddings[&digest2], vec![0.4, 0.5, 0.6]);
}
#[gpui::test]
async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
let test_db = TestDb::postgres(cx.executor());
let db = test_db.db();
let model = "test_model";
let digest = vec![7, 8, 9];
let embeddings = HashMap::from_iter([(digest.clone(), vec![0.7, 0.8, 0.9])]);
// Save old embeddings
db.save_embeddings(model, &embeddings).await.unwrap();
// Reach into the DB and change the retrieved at to be > 60 days
db.transaction(|tx| {
let digest = digest.clone();
async move {
let sixty_days_ago = OffsetDateTime::now_utc().sub(Duration::days(61));
let retrieved_at = PrimitiveDateTime::new(sixty_days_ago.date(), sixty_days_ago.time());
embedding::Entity::update_many()
.filter(
embedding::Column::Model
.eq(model)
.and(embedding::Column::Digest.eq(digest)),
)
.col_expr(embedding::Column::RetrievedAt, Expr::value(retrieved_at))
.exec(&*tx)
.await
.unwrap();
Ok(())
}
})
.await
.unwrap();
// Purge old embeddings
db.purge_old_embeddings().await.unwrap();
// Try to retrieve the purged embeddings
let retrieved_embeddings = db
.get_embeddings(model, std::slice::from_ref(&digest))
.await
.unwrap();
assert!(
retrieved_embeddings.is_empty(),
"Old embeddings should have been purged"
);
}

View File

@@ -0,0 +1,96 @@
use chrono::Utc;
use crate::{
db::{Database, NewUserParams},
test_both_dbs,
};
use std::sync::Arc;
test_both_dbs!(
test_accepted_tos,
test_accepted_tos_postgres,
test_accepted_tos_sqlite
);
async fn test_accepted_tos(db: &Arc<Database>) {
let user_id = db
.create_user(
"user1@example.com",
None,
false,
NewUserParams {
github_login: "user1".to_string(),
github_user_id: 1,
},
)
.await
.unwrap()
.user_id;
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
assert!(user.accepted_tos_at.is_none());
let accepted_tos_at = Utc::now().naive_utc();
db.set_user_accepted_tos_at(user_id, Some(accepted_tos_at))
.await
.unwrap();
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
assert!(user.accepted_tos_at.is_some());
assert_eq!(user.accepted_tos_at, Some(accepted_tos_at));
db.set_user_accepted_tos_at(user_id, None).await.unwrap();
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
assert!(user.accepted_tos_at.is_none());
}
test_both_dbs!(
test_destroy_user_cascade_deletes_access_tokens,
test_destroy_user_cascade_deletes_access_tokens_postgres,
test_destroy_user_cascade_deletes_access_tokens_sqlite
);
async fn test_destroy_user_cascade_deletes_access_tokens(db: &Arc<Database>) {
let user_id = db
.create_user(
"user1@example.com",
Some("user1"),
false,
NewUserParams {
github_login: "user1".to_string(),
github_user_id: 12345,
},
)
.await
.unwrap()
.user_id;
let user = db.get_user_by_id(user_id).await.unwrap();
assert!(user.is_some());
let token_1_id = db
.create_access_token(user_id, None, "token-1", 10)
.await
.unwrap();
let token_2_id = db
.create_access_token(user_id, None, "token-2", 10)
.await
.unwrap();
let token_1 = db.get_access_token(token_1_id).await;
let token_2 = db.get_access_token(token_2_id).await;
assert!(token_1.is_ok());
assert!(token_2.is_ok());
db.destroy_user(user_id).await.unwrap();
let user = db.get_user_by_id(user_id).await.unwrap();
assert!(user.is_none());
let token_1 = db.get_access_token(token_1_id).await;
let token_2 = db.get_access_token(token_2_id).await;
assert!(token_1.is_err());
assert!(token_2.is_err());
}

View File

@@ -13,7 +13,7 @@ use collab::llm::db::LlmDatabase;
use collab::migrations::run_database_migrations;
use collab::{
AppState, Config, Result, api::fetch_extensions_from_blob_store_periodically, db, env,
executor::Executor,
executor::Executor, rpc::ResultExt,
};
use db::Database;
use std::{
@@ -95,6 +95,8 @@ async fn main() -> Result<()> {
let state = AppState::new(config, Executor::Production).await?;
if mode.is_collab() {
state.db.purge_old_embeddings().await.trace_err();
let epoch = state
.db
.create_server(&state.config.zed_environment)

View File

@@ -2169,28 +2169,16 @@ async fn test_inlay_hint_refresh_is_forwarded(
} else {
"initial hint"
};
Ok(Some(vec![
lsp::InlayHint {
position: lsp::Position::new(0, character),
label: lsp::InlayHintLabel::String(label.to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
lsp::InlayHint {
position: lsp::Position::new(1090, 1090),
label: lsp::InlayHintLabel::String("out-of-bounds hint".to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
]))
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, character),
label: lsp::InlayHintLabel::String(label.to_string()),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}]))
}
})
.next()

View File

@@ -523,7 +523,7 @@ async fn test_basic_following(
});
// Client B activates a panel, and the previously-opened screen-sharing item gets activated.
let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
let panel = cx_b.new(|cx| TestPanel::new(DockPosition::Left, cx));
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.add_panel(panel, window, cx);
workspace.toggle_panel_focus::<TestPanel>(window, cx);

View File

@@ -294,10 +294,6 @@ pub enum ChatMessage {
content: ChatMessageContent,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
tool_calls: Vec<ToolCall>,
#[serde(default, skip_serializing_if = "Option::is_none")]
reasoning_opaque: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
reasoning_text: Option<String>,
},
User {
content: ChatMessageContent,
@@ -357,8 +353,6 @@ pub enum ToolCallContent {
pub struct FunctionContent {
pub name: String,
pub arguments: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thought_signature: Option<String>,
}
#[derive(Deserialize, Debug)]
@@ -390,8 +384,6 @@ pub struct ResponseDelta {
pub role: Option<Role>,
#[serde(default)]
pub tool_calls: Vec<ToolCallChunk>,
pub reasoning_opaque: Option<String>,
pub reasoning_text: Option<String>,
}
#[derive(Deserialize, Debug, Eq, PartialEq)]
pub struct ToolCallChunk {
@@ -404,7 +396,6 @@ pub struct ToolCallChunk {
pub struct FunctionChunk {
pub name: Option<String>,
pub arguments: Option<String>,
pub thought_signature: Option<String>,
}
#[derive(Deserialize)]
@@ -792,13 +783,13 @@ async fn stream_completion(
is_user_initiated: bool,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.iter().any(|message| match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
});
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
});
let request_initiator = if is_user_initiated { "user" } else { "agent" };

View File

@@ -127,8 +127,6 @@ pub enum ResponseInputItem {
arguments: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<ItemStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
thought_signature: Option<String>,
},
FunctionCallOutput {
call_id: String,
@@ -253,8 +251,6 @@ pub enum ResponseOutputItem {
arguments: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<ItemStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
thought_signature: Option<String>,
},
Reasoning {
id: String,
@@ -313,8 +309,7 @@ pub async fn stream_response(
};
let is_streaming = request.stream;
let json = serde_json::to_string(&request)?;
let request = request_builder.body(AsyncBody::from(json))?;
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;
if !response.status().is_success() {

View File

@@ -289,7 +289,6 @@ impl minidumper::ServerHandler for CrashServer {
pub fn panic_hook(info: &PanicHookInfo) {
// Don't handle a panic on threads that are not relevant to the main execution.
if extension_host::wasm_host::IS_WASM_THREAD.with(|v| v.load(Ordering::Acquire)) {
log::error!("wasm thread panicked!");
return;
}

View File

@@ -1029,11 +1029,13 @@ impl SearchableItem for DapLogView {
&mut self,
index: usize,
matches: &[Self::Match],
collapse: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor
.update(cx, |e, cx| e.activate_match(index, matches, window, cx))
self.editor.update(cx, |e, cx| {
e.activate_match(index, matches, collapse, window, cx)
})
}
fn select_matches(

View File

@@ -677,8 +677,6 @@ impl ConsoleQueryBarCompletionProvider {
),
new_text: string_match.string.clone(),
label: CodeLabel::plain(string_match.string.clone(), None),
match_start: None,
snippet_deduplication_key: None,
icon_path: None,
documentation: Some(CompletionDocumentation::MultiLineMarkdown(
variable_value.into(),
@@ -792,8 +790,6 @@ impl ConsoleQueryBarCompletionProvider {
documentation: completion.detail.map(|detail| {
CompletionDocumentation::MultiLineMarkdown(detail.into())
}),
match_start: None,
snippet_deduplication_key: None,
confirm: None,
source: project::CompletionSource::Dap { sort_text },
insert_text_mode: None,

View File

@@ -182,6 +182,7 @@ impl ProjectDiagnosticsEditor {
project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
log::debug!("disk based diagnostics finished for server {language_server_id}");
this.close_diagnosticless_buffers(
window,
cx,
this.editor.focus_handle(cx).contains_focused(window, cx)
|| this.focus_handle.contains_focused(window, cx),
@@ -246,10 +247,10 @@ impl ProjectDiagnosticsEditor {
window.focus(&this.focus_handle);
}
}
EditorEvent::Blurred => this.close_diagnosticless_buffers(cx, false),
EditorEvent::Saved => this.close_diagnosticless_buffers(cx, true),
EditorEvent::Blurred => this.close_diagnosticless_buffers(window, cx, false),
EditorEvent::Saved => this.close_diagnosticless_buffers(window, cx, true),
EditorEvent::SelectionsChanged { .. } => {
this.close_diagnosticless_buffers(cx, true)
this.close_diagnosticless_buffers(window, cx, true)
}
_ => {}
}
@@ -297,7 +298,12 @@ impl ProjectDiagnosticsEditor {
/// - have no diagnostics anymore
/// - are saved (not dirty)
/// - and, if `retain_selections` is true, do not have selections within them
fn close_diagnosticless_buffers(&mut self, cx: &mut Context<Self>, retain_selections: bool) {
fn close_diagnosticless_buffers(
&mut self,
_window: &mut Window,
cx: &mut Context<Self>,
retain_selections: bool,
) {
let snapshot = self
.editor
.update(cx, |editor, cx| editor.display_snapshot(cx));
@@ -441,7 +447,7 @@ impl ProjectDiagnosticsEditor {
fn focus_out(&mut self, _: FocusOutEvent, window: &mut Window, cx: &mut Context<Self>) {
if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
{
self.close_diagnosticless_buffers(cx, false);
self.close_diagnosticless_buffers(window, cx, false);
}
}
@@ -455,7 +461,8 @@ impl ProjectDiagnosticsEditor {
});
}
});
self.close_diagnosticless_buffers(cx, false);
self.multibuffer
.update(cx, |multibuffer, cx| multibuffer.clear(cx));
self.project.update(cx, |project, cx| {
self.paths_to_update = project
.diagnostic_summaries(false, cx)
@@ -491,7 +498,7 @@ impl ProjectDiagnosticsEditor {
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let was_empty = self.multibuffer.read(cx).is_empty();
let buffer_snapshot = buffer.read(cx).snapshot();
let mut buffer_snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer_snapshot.remote_id();
let max_severity = if self.include_warnings {
@@ -557,20 +564,6 @@ impl ProjectDiagnosticsEditor {
blocks.extend(more);
}
let cmp_excerpts = |buffer_snapshot: &BufferSnapshot,
a: &ExcerptRange<text::Anchor>,
b: &ExcerptRange<text::Anchor>| {
let context_start = || a.context.start.cmp(&b.context.start, buffer_snapshot);
let context_end = || a.context.end.cmp(&b.context.end, buffer_snapshot);
let primary_start = || a.primary.start.cmp(&b.primary.start, buffer_snapshot);
let primary_end = || a.primary.end.cmp(&b.primary.end, buffer_snapshot);
context_start()
.then_with(context_end)
.then_with(primary_start)
.then_with(primary_end)
.then(cmp::Ordering::Greater)
};
let mut excerpt_ranges: Vec<ExcerptRange<_>> = this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multi_buffer, cx| {
let is_dirty = multi_buffer
@@ -582,12 +575,10 @@ impl ProjectDiagnosticsEditor {
.excerpts_for_buffer(buffer_id, cx)
.into_iter()
.map(|(_, range)| range)
.sorted_by(|a, b| cmp_excerpts(&buffer_snapshot, a, b))
.collect(),
}
})
})?;
let mut result_blocks = vec![None; excerpt_ranges.len()];
let context_lines = cx.update(|_, cx| multibuffer_context_lines(cx))?;
for b in blocks {
@@ -598,16 +589,43 @@ impl ProjectDiagnosticsEditor {
cx,
)
.await;
buffer_snapshot = cx.update(|_, cx| buffer.read(cx).snapshot())?;
let initial_range = buffer_snapshot.anchor_after(b.initial_range.start)
..buffer_snapshot.anchor_before(b.initial_range.end);
let excerpt_range = ExcerptRange {
context: excerpt_range,
primary: initial_range,
let bin_search = |probe: &ExcerptRange<text::Anchor>| {
let context_start = || {
probe
.context
.start
.cmp(&excerpt_range.start, &buffer_snapshot)
};
let context_end =
|| probe.context.end.cmp(&excerpt_range.end, &buffer_snapshot);
let primary_start = || {
probe
.primary
.start
.cmp(&initial_range.start, &buffer_snapshot)
};
let primary_end =
|| probe.primary.end.cmp(&initial_range.end, &buffer_snapshot);
context_start()
.then_with(context_end)
.then_with(primary_start)
.then_with(primary_end)
.then(cmp::Ordering::Greater)
};
let i = excerpt_ranges
.binary_search_by(|probe| cmp_excerpts(&buffer_snapshot, probe, &excerpt_range))
.binary_search_by(bin_search)
.unwrap_or_else(|i| i);
excerpt_ranges.insert(i, excerpt_range);
excerpt_ranges.insert(
i,
ExcerptRange {
context: excerpt_range,
primary: initial_range,
},
);
result_blocks.insert(i, Some(b));
}
@@ -1005,14 +1023,11 @@ async fn heuristic_syntactic_expand(
snapshot: BufferSnapshot,
cx: &mut AsyncApp,
) -> Option<RangeInclusive<BufferRow>> {
let start = snapshot.clip_point(input_range.start, Bias::Right);
let end = snapshot.clip_point(input_range.end, Bias::Left);
let input_row_count = input_range.end.row - input_range.start.row;
if input_row_count > max_row_count {
return None;
}
let input_range = start..end;
// If the outline node contains the diagnostic and is small enough, just use that.
let outline_range = snapshot.outline_range_containing(input_range.clone());
if let Some(outline_range) = outline_range.clone() {

View File

@@ -43,8 +43,7 @@ actions!(
]
);
const COPILOT_SETTINGS_PATH: &str = "/settings/copilot";
const COPILOT_SETTINGS_URL: &str = concat!("https://github.com", "/settings/copilot");
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security";
struct CopilotErrorToast;
@@ -129,21 +128,20 @@ impl Render for EditPredictionButton {
}),
);
}
let this = cx.weak_entity();
let this = cx.entity();
div().child(
PopoverMenu::new("copilot")
.menu(move |window, cx| {
let current_status = Copilot::global(cx)?.read(cx).status();
match current_status {
Some(match current_status {
Status::Authorized => this.update(cx, |this, cx| {
this.build_copilot_context_menu(window, cx)
}),
_ => this.update(cx, |this, cx| {
this.build_copilot_start_menu(window, cx)
}),
}
.ok()
})
})
.anchor(Corner::BottomRight)
.trigger_with_tooltip(
@@ -184,7 +182,7 @@ impl Render for EditPredictionButton {
let icon = status.to_icon();
let tooltip_text = status.to_tooltip();
let has_menu = status.has_menu();
let this = cx.weak_entity();
let this = cx.entity();
let fs = self.fs.clone();
div().child(
@@ -211,11 +209,9 @@ impl Render for EditPredictionButton {
)
}))
}
SupermavenButtonStatus::Ready => this
.update(cx, |this, cx| {
this.build_supermaven_context_menu(window, cx)
})
.ok(),
SupermavenButtonStatus::Ready => Some(this.update(cx, |this, cx| {
this.build_supermaven_context_menu(window, cx)
})),
_ => None,
})
.anchor(Corner::BottomRight)
@@ -237,16 +233,15 @@ impl Render for EditPredictionButton {
let enabled = self.editor_enabled.unwrap_or(true);
let has_api_key = CodestralCompletionProvider::has_api_key(cx);
let fs = self.fs.clone();
let this = cx.weak_entity();
let this = cx.entity();
div().child(
PopoverMenu::new("codestral")
.menu(move |window, cx| {
if has_api_key {
this.update(cx, |this, cx| {
Some(this.update(cx, |this, cx| {
this.build_codestral_context_menu(window, cx)
})
.ok()
}))
} else {
Some(ContextMenu::build(window, cx, |menu, _, _| {
let fs = fs.clone();
@@ -384,12 +379,11 @@ impl Render for EditPredictionButton {
})
});
let this = cx.weak_entity();
let this = cx.entity();
let mut popover_menu = PopoverMenu::new("zeta")
.menu(move |window, cx| {
this.update(cx, |this, cx| this.build_zeta_context_menu(window, cx))
.ok()
Some(this.update(cx, |this, cx| this.build_zeta_context_menu(window, cx)))
})
.anchor(Corner::BottomRight)
.with_handle(self.popover_menu_handle.clone());
@@ -837,16 +831,6 @@ impl EditPredictionButton {
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
let all_language_settings = all_language_settings(None, cx);
let copilot_config = copilot::copilot_chat::CopilotChatConfiguration {
enterprise_uri: all_language_settings
.edit_predictions
.copilot
.enterprise_uri
.clone(),
};
let settings_url = copilot_settings_url(copilot_config.enterprise_uri.as_deref());
ContextMenu::build(window, cx, |menu, window, cx| {
let menu = self.build_language_settings_menu(menu, window, cx);
let menu =
@@ -855,7 +839,10 @@ impl EditPredictionButton {
menu.separator()
.link(
"Go to Copilot Settings",
OpenBrowser { url: settings_url }.boxed_clone(),
OpenBrowser {
url: COPILOT_SETTINGS_URL.to_string(),
}
.boxed_clone(),
)
.action("Sign Out", copilot::SignOut.boxed_clone())
})
@@ -1184,99 +1171,3 @@ fn toggle_edit_prediction_mode(fs: Arc<dyn Fs>, mode: EditPredictionsMode, cx: &
});
}
}
fn copilot_settings_url(enterprise_uri: Option<&str>) -> String {
match enterprise_uri {
Some(uri) => {
format!("{}{}", uri.trim_end_matches('/'), COPILOT_SETTINGS_PATH)
}
None => COPILOT_SETTINGS_URL.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
#[gpui::test]
async fn test_copilot_settings_url_with_enterprise_uri(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
});
cx.update_global(|settings_store: &mut SettingsStore, cx| {
settings_store
.set_user_settings(
r#"{"edit_predictions":{"copilot":{"enterprise_uri":"https://my-company.ghe.com"}}}"#,
cx,
)
.unwrap();
});
let url = cx.update(|cx| {
let all_language_settings = all_language_settings(None, cx);
copilot_settings_url(
all_language_settings
.edit_predictions
.copilot
.enterprise_uri
.as_deref(),
)
});
assert_eq!(url, "https://my-company.ghe.com/settings/copilot");
}
#[gpui::test]
async fn test_copilot_settings_url_with_enterprise_uri_trailing_slash(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
});
cx.update_global(|settings_store: &mut SettingsStore, cx| {
settings_store
.set_user_settings(
r#"{"edit_predictions":{"copilot":{"enterprise_uri":"https://my-company.ghe.com/"}}}"#,
cx,
)
.unwrap();
});
let url = cx.update(|cx| {
let all_language_settings = all_language_settings(None, cx);
copilot_settings_url(
all_language_settings
.edit_predictions
.copilot
.enterprise_uri
.as_deref(),
)
});
assert_eq!(url, "https://my-company.ghe.com/settings/copilot");
}
#[gpui::test]
async fn test_copilot_settings_url_without_enterprise_uri(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
});
let url = cx.update(|cx| {
let all_language_settings = all_language_settings(None, cx);
copilot_settings_url(
all_language_settings
.edit_predictions
.copilot
.enterprise_uri
.as_deref(),
)
});
assert_eq!(url, "https://github.com/settings/copilot");
}
}

View File

@@ -213,6 +213,15 @@ pub struct ExpandExcerptsDown {
pub(super) lines: u32,
}
/// Shows code completion suggestions at the cursor position.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
pub struct ShowCompletions {
#[serde(default)]
pub(super) trigger: Option<String>,
}
/// Handles text input in the editor.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
@@ -727,8 +736,6 @@ actions!(
SelectToStartOfParagraph,
/// Extends selection up.
SelectUp,
/// Shows code completion suggestions at the cursor position.
ShowCompletions,
/// Shows the system character palette.
ShowCharacterPalette,
/// Shows edit prediction at cursor.

View File

@@ -305,8 +305,6 @@ impl CompletionBuilder {
icon_path: None,
insert_text_mode: None,
confirm: None,
match_start: None,
snippet_deduplication_key: None,
}
}
}

View File

@@ -17,6 +17,7 @@ use project::{CompletionDisplayOptions, CompletionSource};
use task::DebugScenario;
use task::TaskContext;
use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{
@@ -35,13 +36,12 @@ use util::ResultExt;
use crate::hover_popover::{hover_markdown_style, open_markdown_url};
use crate::{
CodeActionProvider, CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle,
ResolvedTasks,
CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
EditorStyle, ResolvedTasks,
actions::{ConfirmCodeAction, ConfirmCompletion},
split_words, styled_runs_for_code_label,
};
use crate::{CodeActionSource, EditorSettings};
use collections::{HashSet, VecDeque};
use settings::{Settings, SnippetSortOrder};
pub const MENU_GAP: Pixels = px(4.);
@@ -220,9 +220,7 @@ pub struct CompletionsMenu {
pub is_incomplete: bool,
pub buffer: Entity<Buffer>,
pub completions: Rc<RefCell<Box<[Completion]>>>,
/// String match candidate for each completion, grouped by `match_start`.
match_candidates: Arc<[(Option<text::Anchor>, Vec<StringMatchCandidate>)]>,
/// Entries displayed in the menu, which is a filtered and sorted subset of `match_candidates`.
match_candidates: Arc<[StringMatchCandidate]>,
pub entries: Rc<RefCell<Box<[StringMatch]>>>,
pub selected_item: usize,
filter_task: Task<()>,
@@ -254,17 +252,8 @@ enum MarkdownCacheKey {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CompletionsMenuSource {
/// Show all completions (words, snippets, LSP)
Normal,
/// Show only snippets (not words or LSP)
///
/// Used after typing a non-word character
SnippetsOnly,
/// Tab stops within a snippet that have a predefined finite set of choices
SnippetChoices,
/// Show only words (not snippets or LSP)
///
/// Used when word completions are explicitly triggered
Words { ignore_threshold: bool },
}
@@ -310,8 +299,6 @@ impl CompletionsMenu {
.iter()
.enumerate()
.map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
.into_group_map_by(|candidate| completions[candidate.id].match_start)
.into_iter()
.collect();
let completions_menu = Self {
@@ -359,8 +346,6 @@ impl CompletionsMenu {
replace_range: selection.start.text_anchor..selection.end.text_anchor,
new_text: choice.to_string(),
label: CodeLabel::plain(choice.to_string(), None),
match_start: None,
snippet_deduplication_key: None,
icon_path: None,
documentation: None,
confirm: None,
@@ -369,14 +354,11 @@ impl CompletionsMenu {
})
.collect();
let match_candidates = Arc::new([(
None,
choices
.iter()
.enumerate()
.map(|(id, completion)| StringMatchCandidate::new(id, completion))
.collect(),
)]);
let match_candidates = choices
.iter()
.enumerate()
.map(|(id, completion)| StringMatchCandidate::new(id, completion))
.collect();
let entries = choices
.iter()
.enumerate()
@@ -957,7 +939,7 @@ impl CompletionsMenu {
}
let mat = &self.entries.borrow()[self.selected_item];
let completions = self.completions.borrow();
let completions = self.completions.borrow_mut();
let multiline_docs = match completions[mat.candidate_id].documentation.as_ref() {
Some(CompletionDocumentation::MultiLinePlainText(text)) => div().child(text.clone()),
Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
@@ -1035,74 +1017,57 @@ impl CompletionsMenu {
pub fn filter(
&mut self,
query: Arc<String>,
query_end: text::Anchor,
buffer: &Entity<Buffer>,
query: Option<Arc<String>>,
provider: Option<Rc<dyn CompletionProvider>>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
self.cancel_filter.store(true, Ordering::Relaxed);
self.cancel_filter = Arc::new(AtomicBool::new(false));
let matches = self.do_async_filtering(query, query_end, buffer, cx);
let id = self.id;
self.filter_task = cx.spawn_in(window, async move |editor, cx| {
let matches = matches.await;
editor
.update_in(cx, |editor, window, cx| {
editor.with_completions_menu_matching_id(id, |this| {
if let Some(this) = this {
this.set_filter_results(matches, provider, window, cx);
}
});
})
.ok();
});
if let Some(query) = query {
self.cancel_filter = Arc::new(AtomicBool::new(false));
let matches = self.do_async_filtering(query, cx);
let id = self.id;
self.filter_task = cx.spawn_in(window, async move |editor, cx| {
let matches = matches.await;
editor
.update_in(cx, |editor, window, cx| {
editor.with_completions_menu_matching_id(id, |this| {
if let Some(this) = this {
this.set_filter_results(matches, provider, window, cx);
}
});
})
.ok();
});
} else {
self.filter_task = Task::ready(());
let matches = self.unfiltered_matches();
self.set_filter_results(matches, provider, window, cx);
}
}
pub fn do_async_filtering(
&self,
query: Arc<String>,
query_end: text::Anchor,
buffer: &Entity<Buffer>,
cx: &Context<Editor>,
) -> Task<Vec<StringMatch>> {
let buffer_snapshot = buffer.read(cx).snapshot();
let background_executor = cx.background_executor().clone();
let match_candidates = self.match_candidates.clone();
let cancel_filter = self.cancel_filter.clone();
let default_query = query.clone();
let matches_task = cx.background_spawn(async move {
let queries_and_candidates = match_candidates
.iter()
.map(|(query_start, candidates)| {
let query_for_batch = match query_start {
Some(start) => {
Arc::new(buffer_snapshot.text_for_range(*start..query_end).collect())
}
None => default_query.clone(),
};
(query_for_batch, candidates)
})
.collect_vec();
let mut results = vec![];
for (query, match_candidates) in queries_and_candidates {
results.extend(
fuzzy::match_strings(
&match_candidates,
&query,
query.chars().any(|c| c.is_uppercase()),
false,
1000,
&cancel_filter,
background_executor.clone(),
)
.await,
);
let matches_task = cx.background_spawn({
let query = query.clone();
let match_candidates = self.match_candidates.clone();
let cancel_filter = self.cancel_filter.clone();
let background_executor = cx.background_executor().clone();
async move {
fuzzy::match_strings(
&match_candidates,
&query,
query.chars().any(|c| c.is_uppercase()),
false,
1000,
&cancel_filter,
background_executor,
)
.await
}
results
});
let completions = self.completions.clone();
@@ -1111,31 +1076,45 @@ impl CompletionsMenu {
cx.foreground_executor().spawn(async move {
let mut matches = matches_task.await;
let completions_ref = completions.borrow();
if sort_completions {
matches = Self::sort_string_matches(
matches,
Some(&query), // used for non-snippets only
Some(&query),
snippet_sort_order,
&completions_ref,
completions.borrow().as_ref(),
);
}
// Remove duplicate snippet prefixes (e.g., "cool code" will match
// the text "c c" in two places; we should only show the longer one)
let mut snippets_seen = HashSet::<(usize, usize)>::default();
matches.retain(|result| {
match completions_ref[result.candidate_id].snippet_deduplication_key {
Some(key) => snippets_seen.insert(key),
None => true,
}
});
matches
})
}
/// Like `do_async_filtering` but there is no filter query, so no need to spawn tasks.
pub fn unfiltered_matches(&self) -> Vec<StringMatch> {
let mut matches = self
.match_candidates
.iter()
.enumerate()
.map(|(candidate_id, candidate)| StringMatch {
candidate_id,
score: Default::default(),
positions: Default::default(),
string: candidate.string.clone(),
})
.collect();
if self.sort_completions {
matches = Self::sort_string_matches(
matches,
None,
self.snippet_sort_order,
self.completions.borrow().as_ref(),
);
}
matches
}
pub fn set_filter_results(
&mut self,
matches: Vec<StringMatch>,
@@ -1178,13 +1157,28 @@ impl CompletionsMenu {
.and_then(|c| c.to_lowercase().next());
if snippet_sort_order == SnippetSortOrder::None {
matches
.retain(|string_match| !completions[string_match.candidate_id].is_snippet_kind());
matches.retain(|string_match| {
let completion = &completions[string_match.candidate_id];
let is_snippet = matches!(
&completion.source,
CompletionSource::Lsp { lsp_completion, .. }
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
);
!is_snippet
});
}
matches.sort_unstable_by_key(|string_match| {
let completion = &completions[string_match.candidate_id];
let is_snippet = matches!(
&completion.source,
CompletionSource::Lsp { lsp_completion, .. }
if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
);
let sort_text = match &completion.source {
CompletionSource::Lsp { lsp_completion, .. } => lsp_completion.sort_text.as_deref(),
CompletionSource::Dap { sort_text } => Some(sort_text.as_str()),
@@ -1196,17 +1190,14 @@ impl CompletionsMenu {
let score = string_match.score;
let sort_score = Reverse(OrderedFloat(score));
// Snippets do their own first-letter matching logic elsewhere.
let is_snippet = completion.is_snippet_kind();
let query_start_doesnt_match_split_words = !is_snippet
&& query_start_lower
.map(|query_char| {
!split_words(&string_match.string).any(|word| {
word.chars().next().and_then(|c| c.to_lowercase().next())
== Some(query_char)
})
let query_start_doesnt_match_split_words = query_start_lower
.map(|query_char| {
!split_words(&string_match.string).any(|word| {
word.chars().next().and_then(|c| c.to_lowercase().next())
== Some(query_char)
})
.unwrap_or(false);
})
.unwrap_or(false);
if query_start_doesnt_match_split_words {
MatchTier::OtherMatch { sort_score }
@@ -1218,7 +1209,6 @@ impl CompletionsMenu {
SnippetSortOrder::None => Reverse(0),
};
let sort_positions = string_match.positions.clone();
// This exact matching won't work for multi-word snippets, but it's fine
let sort_exact = Reverse(if Some(completion.label.filter_text()) == query {
1
} else {

View File

@@ -1097,7 +1097,7 @@ impl DisplaySnapshot {
details: &TextLayoutDetails,
) -> u32 {
let layout_line = self.layout_row(display_row, details);
layout_line.closest_index_for_x(x) as u32
layout_line.index_for_x(x) as u32
}
pub fn grapheme_at(&self, mut point: DisplayPoint) -> Option<SharedString> {

View File

@@ -19,7 +19,7 @@ use std::{
cell::RefCell,
cmp::{self, Ordering},
fmt::Debug,
ops::{Deref, DerefMut, Not, Range, RangeBounds, RangeInclusive},
ops::{Deref, DerefMut, Range, RangeBounds, RangeInclusive},
sync::{
Arc,
atomic::{AtomicUsize, Ordering::SeqCst},
@@ -1879,14 +1879,18 @@ impl Iterator for BlockRows<'_> {
}
let transform = self.transforms.item()?;
if transform.block.as_ref().is_none_or(|block| {
block.is_replacement()
&& self.transforms.start().0 == self.output_row
&& matches!(block, Block::FoldedBuffer { .. }).not()
}) {
self.input_rows.next()
if let Some(block) = transform.block.as_ref() {
if block.is_replacement() && self.transforms.start().0 == self.output_row {
if matches!(block, Block::FoldedBuffer { .. }) {
Some(RowInfo::default())
} else {
Some(self.input_rows.next().unwrap())
}
} else {
Some(RowInfo::default())
}
} else {
Some(RowInfo::default())
Some(self.input_rows.next().unwrap())
}
}
}

View File

@@ -965,7 +965,7 @@ impl<'a> Iterator for WrapChunks<'a> {
}
if self.input_chunk.text.is_empty() {
self.input_chunk = self.input_chunks.next()?;
self.input_chunk = self.input_chunks.next().unwrap();
}
let mut input_len = 0;

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,6 @@ pub struct EditorSettings {
pub horizontal_scroll_margin: f32,
pub scroll_sensitivity: f32,
pub fast_scroll_sensitivity: f32,
pub sticky_scroll: StickyScroll,
pub relative_line_numbers: RelativeLineNumbers,
pub seed_search_query_from_cursor: SeedQuerySetting,
pub use_smartcase_search: bool,
@@ -66,11 +65,6 @@ pub struct Jupyter {
pub enabled: bool,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct StickyScroll {
pub enabled: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Toolbar {
pub breadcrumbs: bool,
@@ -162,15 +156,10 @@ pub struct DragAndDropSelection {
pub struct SearchSettings {
/// Whether to show the project search button in the status bar.
pub button: bool,
/// Whether to only match on whole words.
pub whole_word: bool,
/// Whether to match case sensitively.
pub case_sensitive: bool,
/// Whether to include gitignored files in search results.
pub include_ignored: bool,
/// Whether to interpret the search query as a regular expression.
pub regex: bool,
/// Whether to center the cursor on each search match when navigating.
pub center_on_match: bool,
}
@@ -196,7 +185,6 @@ impl Settings for EditorSettings {
let toolbar = editor.toolbar.unwrap();
let search = editor.search.unwrap();
let drag_and_drop_selection = editor.drag_and_drop_selection.unwrap();
let sticky_scroll = editor.sticky_scroll.unwrap();
Self {
cursor_blink: editor.cursor_blink.unwrap(),
cursor_shape: editor.cursor_shape.map(Into::into),
@@ -247,9 +235,6 @@ impl Settings for EditorSettings {
horizontal_scroll_margin: editor.horizontal_scroll_margin.unwrap(),
scroll_sensitivity: editor.scroll_sensitivity.unwrap(),
fast_scroll_sensitivity: editor.fast_scroll_sensitivity.unwrap(),
sticky_scroll: StickyScroll {
enabled: sticky_scroll.enabled.unwrap(),
},
relative_line_numbers: editor.relative_line_numbers.unwrap(),
seed_search_query_from_cursor: editor.seed_search_query_from_cursor.unwrap(),
use_smartcase_search: editor.use_smartcase_search.unwrap(),

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,10 @@ use crate::{
EditDisplayMode, EditPrediction, Editor, EditorMode, EditorSettings, EditorSnapshot,
EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp,
HandleInput, HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp,
MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown,
PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
SelectedTextHighlight, Selection, SelectionDragState, SelectionEffects, SizingBehavior,
SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
OpenExcerptsSplit, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt,
SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SizingBehavior, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
@@ -29,7 +29,7 @@ use crate::{
items::BufferSearchHighlights,
mouse_context_menu::{self, MenuPosition},
scroll::{
ActiveScrollbarState, Autoscroll, ScrollOffset, ScrollPixelOffset, ScrollbarThumbState,
ActiveScrollbarState, ScrollOffset, ScrollPixelOffset, ScrollbarThumbState,
scroll_amount::ScrollAmount,
},
};
@@ -3255,9 +3255,11 @@ impl EditorElement {
(newest_selection_head, relative)
});
let relative_line_numbers_enabled = relative.enabled();
let relative_to = relative_line_numbers_enabled.then(|| newest_selection_head.row());
let relative_to = if relative.enabled() {
Some(newest_selection_head.row())
} else {
None
};
let relative_rows =
self.calculate_relative_line_numbers(snapshot, &rows, relative_to, relative.wrapped());
let mut line_number = String::new();
@@ -3269,18 +3271,17 @@ impl EditorElement {
} else {
row_info.buffer_row? + 1
};
let relative_number = relative_rows.get(&display_row);
if !(relative_line_numbers_enabled && relative_number.is_some())
&& row_info
.diff_status
.is_some_and(|status| status.is_deleted())
let number = relative_rows
.get(&display_row)
.unwrap_or(&non_relative_number);
write!(&mut line_number, "{number}").unwrap();
if row_info
.diff_status
.is_some_and(|status| status.is_deleted())
{
return None;
}
let number = relative_number.unwrap_or(&non_relative_number);
write!(&mut line_number, "{number}").unwrap();
let color = active_rows
.get(&display_row)
.map(|spec| {
@@ -4042,17 +4043,24 @@ impl EditorElement {
)
.group_hover("", |div| div.underline()),
)
.on_click(window.listener_for(&self.editor, {
let jump_data = jump_data.clone();
move |editor, e: &ClickEvent, window, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
.on_click({
let focus_handle = focus_handle.clone();
move |event, window, cx| {
if event.modifiers().secondary() {
focus_handle.dispatch_action(
&OpenExcerptsSplit,
window,
cx,
);
} else {
focus_handle.dispatch_action(
&OpenExcerpts,
window,
cx,
);
}
}
})),
}),
)
.when_some(parent_path, |then, path| {
then.child(div().child(path).text_color(
@@ -4080,17 +4088,24 @@ impl EditorElement {
cx,
)),
)
.on_click(window.listener_for(&self.editor, {
let jump_data = jump_data.clone();
move |editor, e: &ClickEvent, window, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
.on_click({
let focus_handle = focus_handle.clone();
move |event, window, cx| {
if event.modifiers().secondary() {
focus_handle.dispatch_action(
&OpenExcerptsSplit,
window,
cx,
);
} else {
focus_handle.dispatch_action(
&OpenExcerpts,
window,
cx,
);
}
}
})),
}),
)
},
)
@@ -4540,138 +4555,6 @@ impl EditorElement {
header
}
fn layout_sticky_headers(
&self,
snapshot: &EditorSnapshot,
editor_width: Pixels,
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
line_height: Pixels,
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
content_origin: gpui::Point<Pixels>,
gutter_dimensions: &GutterDimensions,
gutter_hitbox: &Hitbox,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut App,
) -> Option<StickyHeaders> {
let show_line_numbers = snapshot
.show_line_numbers
.unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers);
let rows = Self::sticky_headers(self.editor.read(cx), snapshot, cx);
let mut lines = Vec::<StickyHeaderLine>::new();
for StickyHeader {
item,
sticky_row,
start_point,
offset,
} in rows.into_iter().rev()
{
let line = layout_line(
sticky_row,
snapshot,
&self.style,
editor_width,
is_row_soft_wrapped,
window,
cx,
);
let line_number = show_line_numbers.then(|| {
let number = (start_point.row + 1).to_string();
let color = cx.theme().colors().editor_line_number;
self.shape_line_number(SharedString::from(number), color, window)
});
lines.push(StickyHeaderLine::new(
sticky_row,
line_height * offset as f32,
line,
line_number,
item.range.start,
line_height,
scroll_pixel_position,
content_origin,
gutter_hitbox,
text_hitbox,
window,
cx,
));
}
lines.reverse();
if lines.is_empty() {
return None;
}
Some(StickyHeaders {
lines,
gutter_background: cx.theme().colors().editor_gutter_background,
content_background: self.style.background,
gutter_right_padding: gutter_dimensions.right_padding,
})
}
pub(crate) fn sticky_headers(
editor: &Editor,
snapshot: &EditorSnapshot,
cx: &App,
) -> Vec<StickyHeader> {
let scroll_top = snapshot.scroll_position().y;
let mut end_rows = Vec::<DisplayRow>::new();
let mut rows = Vec::<StickyHeader>::new();
let items = editor.sticky_headers(cx).unwrap_or_default();
for item in items {
let start_point = item.range.start.to_point(snapshot.buffer_snapshot());
let end_point = item.range.end.to_point(snapshot.buffer_snapshot());
let sticky_row = snapshot
.display_snapshot
.point_to_display_point(start_point, Bias::Left)
.row();
let end_row = snapshot
.display_snapshot
.point_to_display_point(end_point, Bias::Left)
.row();
let max_sticky_row = end_row.previous_row();
if max_sticky_row <= sticky_row {
continue;
}
while end_rows
.last()
.is_some_and(|&last_end| last_end < sticky_row)
{
end_rows.pop();
}
let depth = end_rows.len();
let adjusted_scroll_top = scroll_top + depth as f64;
if sticky_row.as_f64() >= adjusted_scroll_top || end_row.as_f64() <= adjusted_scroll_top
{
continue;
}
let max_scroll_offset = max_sticky_row.as_f64() - scroll_top;
let offset = (depth as f64).min(max_scroll_offset);
end_rows.push(end_row);
rows.push(StickyHeader {
item,
sticky_row,
start_point,
offset,
});
}
rows
}
fn layout_cursor_popovers(
&self,
line_height: Pixels,
@@ -6524,89 +6407,6 @@ impl EditorElement {
}
}
fn paint_sticky_headers(
&mut self,
layout: &mut EditorLayout,
window: &mut Window,
cx: &mut App,
) {
let Some(mut sticky_headers) = layout.sticky_headers.take() else {
return;
};
if sticky_headers.lines.is_empty() {
layout.sticky_headers = Some(sticky_headers);
return;
}
let whitespace_setting = self
.editor
.read(cx)
.buffer
.read(cx)
.language_settings(cx)
.show_whitespaces;
sticky_headers.paint(layout, whitespace_setting, window, cx);
let sticky_header_hitboxes: Vec<Hitbox> = sticky_headers
.lines
.iter()
.map(|line| line.hitbox.clone())
.collect();
let hovered_hitbox = sticky_header_hitboxes
.iter()
.find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id));
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, _cx| {
if !phase.bubble() {
return;
}
let current_hover = sticky_header_hitboxes
.iter()
.find_map(|hitbox| hitbox.is_hovered(window).then_some(hitbox.id));
if hovered_hitbox != current_hover {
window.refresh();
}
});
for (line_index, line) in sticky_headers.lines.iter().enumerate() {
let editor = self.editor.clone();
let hitbox = line.hitbox.clone();
let target_anchor = line.target_anchor;
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
if !phase.bubble() {
return;
}
if event.button == MouseButton::Left && hitbox.is_hovered(window) {
editor.update(cx, |editor, cx| {
editor.change_selections(
SelectionEffects::scroll(Autoscroll::top_relative(line_index)),
window,
cx,
|selections| selections.select_ranges([target_anchor..target_anchor]),
);
cx.stop_propagation();
});
}
});
}
let text_bounds = layout.position_map.text_hitbox.bounds;
let border_top = text_bounds.top()
+ sticky_headers.lines.last().unwrap().offset
+ layout.position_map.line_height;
let separator_height = px(1.);
let border_bounds = Bounds::from_corners(
point(layout.gutter_hitbox.bounds.left(), border_top),
point(text_bounds.right(), border_top + separator_height),
);
window.paint_quad(fill(border_bounds, cx.theme().colors().border_variant));
layout.sticky_headers = Some(sticky_headers);
}
fn paint_lines_background(
&mut self,
layout: &mut EditorLayout,
@@ -8307,27 +8107,6 @@ impl LineWithInvisibles {
cx: &mut App,
) {
let line_y = f32::from(line_height) * Pixels::from(row.as_f64() - scroll_position.y);
self.prepaint_with_custom_offset(
line_height,
scroll_pixel_position,
content_origin,
line_y,
line_elements,
window,
cx,
);
}
fn prepaint_with_custom_offset(
&mut self,
line_height: Pixels,
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
content_origin: gpui::Point<Pixels>,
line_y: Pixels,
line_elements: &mut SmallVec<[AnyElement; 1]>,
window: &mut Window,
cx: &mut App,
) {
let mut fragment_origin =
content_origin + gpui::point(Pixels::from(-scroll_pixel_position.x), line_y);
for fragment in &mut self.fragments {
@@ -8361,32 +8140,10 @@ impl LineWithInvisibles {
selection_ranges: &[Range<DisplayPoint>],
window: &mut Window,
cx: &mut App,
) {
self.draw_with_custom_offset(
layout,
row,
content_origin,
layout.position_map.line_height
* (row.as_f64() - layout.position_map.scroll_position.y) as f32,
whitespace_setting,
selection_ranges,
window,
cx,
);
}
fn draw_with_custom_offset(
&self,
layout: &EditorLayout,
row: DisplayRow,
content_origin: gpui::Point<Pixels>,
line_y: Pixels,
whitespace_setting: ShowWhitespaceSetting,
selection_ranges: &[Range<DisplayPoint>],
window: &mut Window,
cx: &mut App,
) {
let line_height = layout.position_map.line_height;
let line_y = line_height * (row.as_f64() - layout.position_map.scroll_position.y) as f32;
let mut fragment_origin = content_origin
+ gpui::point(
Pixels::from(-layout.position_map.scroll_pixel_position.x),
@@ -8606,7 +8363,7 @@ impl LineWithInvisibles {
let fragment_end_x = fragment_start_x + shaped_line.width;
if x < fragment_end_x {
return Some(
fragment_start_index + shaped_line.index_for_x(x - fragment_start_x)?,
fragment_start_index + shaped_line.index_for_x(x - fragment_start_x),
);
}
fragment_start_x = fragment_end_x;
@@ -8825,7 +8582,6 @@ impl Element for EditorElement {
};
let is_minimap = self.editor.read(cx).mode.is_minimap();
let is_singleton = self.editor.read(cx).buffer_kind(cx) == ItemBufferKind::Singleton;
if !is_minimap {
let focus_handle = self.editor.focus_handle(cx);
@@ -9472,26 +9228,6 @@ impl Element for EditorElement {
scroll_position.x * f64::from(em_advance),
scroll_position.y * f64::from(line_height),
);
let sticky_headers = if !is_minimap
&& is_singleton
&& EditorSettings::get_global(cx).sticky_scroll.enabled
{
self.layout_sticky_headers(
&snapshot,
editor_width,
is_row_soft_wrapped,
line_height,
scroll_pixel_position,
content_origin,
&gutter_dimensions,
&gutter_hitbox,
&text_hitbox,
window,
cx,
)
} else {
None
};
let indent_guides = self.layout_indent_guides(
content_origin,
text_hitbox.origin,
@@ -9961,7 +9697,6 @@ impl Element for EditorElement {
tab_invisible,
space_invisible,
sticky_buffer_header,
sticky_headers,
expand_toggles,
}
})
@@ -10032,7 +9767,6 @@ impl Element for EditorElement {
}
});
self.paint_sticky_headers(layout, window, cx);
self.paint_minimap(layout, window, cx);
self.paint_scrollbars(layout, window, cx);
self.paint_edit_prediction_popover(layout, window, cx);
@@ -10141,180 +9875,15 @@ pub struct EditorLayout {
tab_invisible: ShapedLine,
space_invisible: ShapedLine,
sticky_buffer_header: Option<AnyElement>,
sticky_headers: Option<StickyHeaders>,
document_colors: Option<(DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>)>,
}
struct StickyHeaders {
lines: Vec<StickyHeaderLine>,
gutter_background: Hsla,
content_background: Hsla,
gutter_right_padding: Pixels,
}
struct StickyHeaderLine {
row: DisplayRow,
offset: Pixels,
line: LineWithInvisibles,
line_number: Option<ShapedLine>,
elements: SmallVec<[AnyElement; 1]>,
available_text_width: Pixels,
target_anchor: Anchor,
hitbox: Hitbox,
}
impl EditorLayout {
fn line_end_overshoot(&self) -> Pixels {
0.15 * self.position_map.line_height
}
}
impl StickyHeaders {
fn paint(
&mut self,
layout: &mut EditorLayout,
whitespace_setting: ShowWhitespaceSetting,
window: &mut Window,
cx: &mut App,
) {
let line_height = layout.position_map.line_height;
for line in self.lines.iter_mut().rev() {
window.paint_layer(
Bounds::new(
layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset),
size(line.hitbox.size.width, line_height),
),
|window| {
let gutter_bounds = Bounds::new(
layout.gutter_hitbox.origin + point(Pixels::ZERO, line.offset),
size(layout.gutter_hitbox.size.width, line_height),
);
window.paint_quad(fill(gutter_bounds, self.gutter_background));
let text_bounds = Bounds::new(
layout.position_map.text_hitbox.origin + point(Pixels::ZERO, line.offset),
size(line.available_text_width, line_height),
);
window.paint_quad(fill(text_bounds, self.content_background));
if line.hitbox.is_hovered(window) {
let hover_overlay = cx.theme().colors().panel_overlay_hover;
window.paint_quad(fill(gutter_bounds, hover_overlay));
window.paint_quad(fill(text_bounds, hover_overlay));
}
line.paint(
layout,
self.gutter_right_padding,
line.available_text_width,
layout.content_origin,
line_height,
whitespace_setting,
window,
cx,
);
},
);
window.set_cursor_style(CursorStyle::PointingHand, &line.hitbox);
}
}
}
impl StickyHeaderLine {
fn new(
row: DisplayRow,
offset: Pixels,
mut line: LineWithInvisibles,
line_number: Option<ShapedLine>,
target_anchor: Anchor,
line_height: Pixels,
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
content_origin: gpui::Point<Pixels>,
gutter_hitbox: &Hitbox,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut App,
) -> Self {
let mut elements = SmallVec::<[AnyElement; 1]>::new();
line.prepaint_with_custom_offset(
line_height,
scroll_pixel_position,
content_origin,
offset,
&mut elements,
window,
cx,
);
let hitbox_bounds = Bounds::new(
gutter_hitbox.origin + point(Pixels::ZERO, offset),
size(text_hitbox.right() - gutter_hitbox.left(), line_height),
);
let available_text_width =
(hitbox_bounds.size.width - gutter_hitbox.size.width).max(Pixels::ZERO);
Self {
row,
offset,
line,
line_number,
elements,
available_text_width,
target_anchor,
hitbox: window.insert_hitbox(hitbox_bounds, HitboxBehavior::BlockMouseExceptScroll),
}
}
fn paint(
&mut self,
layout: &EditorLayout,
gutter_right_padding: Pixels,
available_text_width: Pixels,
content_origin: gpui::Point<Pixels>,
line_height: Pixels,
whitespace_setting: ShowWhitespaceSetting,
window: &mut Window,
cx: &mut App,
) {
window.with_content_mask(
Some(ContentMask {
bounds: Bounds::new(
layout.position_map.text_hitbox.bounds.origin
+ point(Pixels::ZERO, self.offset),
size(available_text_width, line_height),
),
}),
|window| {
self.line.draw_with_custom_offset(
layout,
self.row,
content_origin,
self.offset,
whitespace_setting,
&[],
window,
cx,
);
for element in &mut self.elements {
element.paint(window, cx);
}
},
);
if let Some(line_number) = &self.line_number {
let gutter_origin = layout.gutter_hitbox.origin + point(Pixels::ZERO, self.offset);
let gutter_width = layout.gutter_hitbox.size.width;
let origin = point(
gutter_origin.x + gutter_width - gutter_right_padding - line_number.width,
gutter_origin.y,
);
line_number.paint(origin, line_height, window, cx).log_err();
}
}
}
#[derive(Debug)]
struct LineNumberSegment {
shaped_line: ShapedLine,
@@ -11161,13 +10730,6 @@ impl HighlightedRange {
}
}
pub(crate) struct StickyHeader {
pub item: language::OutlineItem<Anchor>,
pub sticky_row: DisplayRow,
pub start_point: Point,
pub offset: ScrollOffset,
}
enum CursorPopoverType {
CodeContextMenu,
EditPrediction,
@@ -11440,46 +11002,6 @@ mod tests {
assert_eq!(relative_rows[&DisplayRow(0)], 5);
assert_eq!(relative_rows[&DisplayRow(1)], 4);
assert_eq!(relative_rows[&DisplayRow(2)], 3);
const DELETED_LINE: u32 = 3;
let layouts = cx
.update_window(*window, |_, window, cx| {
element.layout_line_numbers(
None,
GutterDimensions {
left_padding: Pixels::ZERO,
right_padding: Pixels::ZERO,
width: px(30.0),
margin: Pixels::ZERO,
git_blame_entries_width: None,
},
line_height,
gpui::Point::default(),
DisplayRow(0)..DisplayRow(6),
&(0..6)
.map(|row| RowInfo {
buffer_row: Some(row),
diff_status: (row == DELETED_LINE).then(|| {
DiffHunkStatus::deleted(
buffer_diff::DiffHunkSecondaryStatus::NoSecondaryHunk,
)
}),
..Default::default()
})
.collect::<Vec<_>>(),
&BTreeMap::default(),
Some(DisplayPoint::new(DisplayRow(0), 0)),
&snapshot,
window,
cx,
)
})
.unwrap();
assert_eq!(layouts.len(), 5,);
assert!(
layouts.get(&MultiBufferRow(DELETED_LINE)).is_none(),
"Deleted line should not have a line number"
);
}
#[gpui::test]
@@ -11555,62 +11077,6 @@ mod tests {
// current line has no relative number
assert_eq!(relative_rows[&DisplayRow(4)], 1);
assert_eq!(relative_rows[&DisplayRow(5)], 2);
let layouts = cx
.update_window(*window, |_, window, cx| {
element.layout_line_numbers(
None,
GutterDimensions {
left_padding: Pixels::ZERO,
right_padding: Pixels::ZERO,
width: px(30.0),
margin: Pixels::ZERO,
git_blame_entries_width: None,
},
line_height,
gpui::Point::default(),
DisplayRow(0)..DisplayRow(6),
&(0..6)
.map(|row| RowInfo {
buffer_row: Some(row),
diff_status: Some(DiffHunkStatus::deleted(
buffer_diff::DiffHunkSecondaryStatus::NoSecondaryHunk,
)),
..Default::default()
})
.collect::<Vec<_>>(),
&BTreeMap::from_iter([(DisplayRow(0), LineHighlightSpec::default())]),
Some(DisplayPoint::new(DisplayRow(0), 0)),
&snapshot,
window,
cx,
)
})
.unwrap();
assert!(
layouts.is_empty(),
"Deleted lines should have no line number"
);
let relative_rows = window
.update(cx, |editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
element.calculate_relative_line_numbers(
&snapshot,
&(DisplayRow(0)..DisplayRow(6)),
Some(DisplayRow(3)),
true,
)
})
.unwrap();
// Deleted lines should still have relative numbers
assert_eq!(relative_rows[&DisplayRow(0)], 3);
assert_eq!(relative_rows[&DisplayRow(1)], 2);
assert_eq!(relative_rows[&DisplayRow(2)], 1);
// current line, even if deleted, has no relative number
assert_eq!(relative_rows[&DisplayRow(4)], 1);
assert_eq!(relative_rows[&DisplayRow(5)], 2);
}
#[gpui::test]

View File

@@ -1,5 +1,4 @@
use std::{
collections::hash_map,
ops::{ControlFlow, Range},
time::Duration,
};
@@ -779,7 +778,6 @@ impl Editor {
}
let excerpts = self.buffer.read(cx).excerpt_ids();
let mut inserted_hint_text = HashMap::default();
let hints_to_insert = new_hints
.into_iter()
.filter_map(|(chunk_range, hints_result)| {
@@ -806,35 +804,8 @@ impl Editor {
}
}
})
.flat_map(|new_hints| {
let mut hints_deduplicated = Vec::new();
if new_hints.len() > 1 {
for (server_id, new_hints) in new_hints {
for (new_id, new_hint) in new_hints {
let hints_text_for_position = inserted_hint_text
.entry(new_hint.position)
.or_insert_with(HashMap::default);
let insert =
match hints_text_for_position.entry(new_hint.text().to_string()) {
hash_map::Entry::Occupied(o) => o.get() == &server_id,
hash_map::Entry::Vacant(v) => {
v.insert(server_id);
true
}
};
if insert {
hints_deduplicated.push((new_id, new_hint));
}
}
}
} else {
hints_deduplicated.extend(new_hints.into_values().flatten());
}
hints_deduplicated
})
.flat_map(|hints| hints.into_values())
.flatten()
.filter_map(|(hint_id, lsp_hint)| {
if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
&& inlay_hints
@@ -3761,7 +3732,6 @@ let c = 3;"#
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "rust-analyzer",
capabilities: lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
@@ -3834,78 +3804,6 @@ let c = 3;"#
},
);
// Add another server that does send the same, duplicate hints back
let mut fake_servers_2 = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
name: "CrabLang-ls",
capabilities: lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
initializer: Some(Box::new(move |fake_server| {
fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
move |params, _| async move {
if params.text_document.uri
== lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
{
Ok(Some(vec![
lsp::InlayHint {
position: lsp::Position::new(1, 9),
label: lsp::InlayHintLabel::String(": i32".to_owned()),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
lsp::InlayHint {
position: lsp::Position::new(19, 9),
label: lsp::InlayHintLabel::String(": i33".to_owned()),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
]))
} else if params.text_document.uri
== lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
{
Ok(Some(vec![
lsp::InlayHint {
position: lsp::Position::new(1, 10),
label: lsp::InlayHintLabel::String(": i34".to_owned()),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
lsp::InlayHint {
position: lsp::Position::new(29, 10),
label: lsp::InlayHintLabel::String(": i35".to_owned()),
kind: Some(lsp::InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
},
]))
} else {
panic!("Unexpected file path {:?}", params.text_document.uri);
}
},
);
})),
..FakeLspAdapter::default()
},
);
let (buffer_1, _handle_1) = project
.update(cx, |project, cx| {
project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
@@ -3949,7 +3847,6 @@ let c = 3;"#
});
let fake_server = fake_servers.next().await.unwrap();
let _fake_server_2 = fake_servers_2.next().await.unwrap();
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
@@ -3958,16 +3855,11 @@ let c = 3;"#
assert_eq!(
vec![
": i32".to_string(),
": i32".to_string(),
": i33".to_string(),
": i33".to_string(),
": i34".to_string(),
": i34".to_string(),
": i35".to_string(),
": i35".to_string(),
],
sorted_cached_hint_labels(editor, cx),
"We receive duplicate hints from 2 servers and cache them all"
);
assert_eq!(
vec![
@@ -3977,7 +3869,7 @@ let c = 3;"#
": i33".to_string(),
],
visible_hint_labels(editor, cx),
"lib.rs is added before main.rs , so its excerpts should be visible first; hints should be deduplicated per label"
"lib.rs is added before main.rs , so its excerpts should be visible first"
);
})
.unwrap();
@@ -4027,12 +3919,8 @@ let c = 3;"#
assert_eq!(
vec![
": i32".to_string(),
": i32".to_string(),
": i33".to_string(),
": i33".to_string(),
": i34".to_string(),
": i34".to_string(),
": i35".to_string(),
": i35".to_string(),
],
sorted_cached_hint_labels(editor, cx),

View File

@@ -1586,11 +1586,12 @@ impl SearchableItem for Editor {
&mut self,
index: usize,
matches: &[Range<Anchor>],
collapse: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.unfold_ranges(&[matches[index].clone()], false, true, cx);
let range = self.range_for_match(&matches[index]);
let range = self.range_for_match(&matches[index], collapse);
let autoscroll = if EditorSettings::get_global(cx).search.center_on_match {
Autoscroll::center()
} else {
@@ -1795,14 +1796,6 @@ impl SearchableItem for Editor {
fn search_bar_visibility_changed(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {
self.expect_bounds_change = self.last_bounds;
}
fn set_search_is_case_sensitive(
&mut self,
case_sensitive: Option<bool>,
_cx: &mut Context<Self>,
) {
self.select_next_is_case_sensitive = case_sensitive;
}
}
pub fn active_match_index(

View File

@@ -372,7 +372,7 @@ impl SelectionsCollection {
let is_empty = positions.start == positions.end;
let line_len = display_map.line_len(row);
let line = display_map.layout_row(row, text_layout_details);
let start_col = line.closest_index_for_x(positions.start) as u32;
let start_col = line.index_for_x(positions.start) as u32;
let (start, end) = if is_empty {
let point = DisplayPoint::new(row, std::cmp::min(start_col, line_len));
@@ -382,7 +382,7 @@ impl SelectionsCollection {
return None;
}
let start = DisplayPoint::new(row, start_col);
let end_col = line.closest_index_for_x(positions.end) as u32;
let end_col = line.index_for_x(positions.end) as u32;
let end = DisplayPoint::new(row, end_col);
(start, end)
};
@@ -487,43 +487,6 @@ impl<'snap, 'a> MutableSelectionsCollection<'snap, 'a> {
self.selections_changed |= changed;
}
pub fn remove_selections_from_buffer(&mut self, buffer_id: language::BufferId) {
let mut changed = false;
let filtered_selections: Arc<[Selection<Anchor>]> = {
self.disjoint
.iter()
.filter(|selection| {
if let Some(selection_buffer_id) =
self.snapshot.buffer_id_for_anchor(selection.start)
{
let should_remove = selection_buffer_id == buffer_id;
changed |= should_remove;
!should_remove
} else {
true
}
})
.cloned()
.collect()
};
if filtered_selections.is_empty() {
let default_anchor = self.snapshot.anchor_before(0);
self.collection.disjoint = Arc::from([Selection {
id: post_inc(&mut self.collection.next_selection_id),
start: default_anchor,
end: default_anchor,
reversed: false,
goal: SelectionGoal::None,
}]);
} else {
self.collection.disjoint = filtered_selections;
}
self.selections_changed |= changed;
}
pub fn clear_pending(&mut self) {
if self.collection.pending.is_some() {
self.collection.pending = None;

View File

@@ -6,7 +6,6 @@ use buffer_diff::DiffHunkStatusKind;
use collections::BTreeMap;
use futures::Future;
use git::repository::RepoPath;
use gpui::{
AnyWindowHandle, App, Context, Entity, Focusable as _, Keystroke, Pixels, Point,
VisualTestContext, Window, WindowHandle, prelude::*,
@@ -59,17 +58,6 @@ impl EditorTestContext {
})
.await
.unwrap();
let language = project
.read_with(cx, |project, _cx| {
project.languages().language_for_name("Plain Text")
})
.await
.unwrap();
buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(language), cx);
});
let editor = cx.add_window(|window, cx| {
let editor = build_editor_with_project(
project,
@@ -346,10 +334,7 @@ impl EditorTestContext {
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
let mut found = None;
fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| {
found = git_state
.index_contents
.get(&RepoPath::from_rel_path(&path))
.cloned();
found = git_state.index_contents.get(&path.into()).cloned();
})
.unwrap();
assert_eq!(expected, found.as_deref());

View File

@@ -463,8 +463,8 @@ pub fn find_model(
.ok_or_else(|| {
anyhow::anyhow!(
"No language model with ID {}/{} was available. Available models: {}",
selected.provider.0,
selected.model.0,
selected.provider.0,
model_registry
.available_models(cx)
.map(|model| format!("{}/{}", model.provider_id().0, model.id().0))

View File

@@ -322,7 +322,7 @@ impl ExampleInstance {
thread.add_default_tools(Rc::new(EvalThreadEnvironment {
project: project.clone(),
}), cx);
thread.set_profile(meta.profile_id.clone(), cx);
thread.set_profile(meta.profile_id.clone());
thread.set_model(
LanguageModelInterceptor::new(
LanguageModelRegistry::read_global(cx).default_model().expect("Missing model").model.clone(),
@@ -553,7 +553,6 @@ impl ExampleInstance {
role: Role::User,
content: vec![MessageContent::Text(to_prompt(assertion.description))],
cache: false,
reasoning_details: None,
}],
temperature: None,
tools: Vec::new(),
@@ -1253,8 +1252,7 @@ pub fn response_events_to_markdown(
Ok(
LanguageModelCompletionEvent::UsageUpdate(_)
| LanguageModelCompletionEvent::StartMessage { .. }
| LanguageModelCompletionEvent::StatusUpdate(_)
| LanguageModelCompletionEvent::ReasoningDetails(_),
| LanguageModelCompletionEvent::StatusUpdate { .. },
) => {}
Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
json_parse_error, ..
@@ -1339,9 +1337,8 @@ impl ThreadDialog {
// Skip these
Ok(LanguageModelCompletionEvent::UsageUpdate(_))
| Ok(LanguageModelCompletionEvent::RedactedThinking { .. })
| Ok(LanguageModelCompletionEvent::StatusUpdate(_))
| Ok(LanguageModelCompletionEvent::StatusUpdate { .. })
| Ok(LanguageModelCompletionEvent::StartMessage { .. })
| Ok(LanguageModelCompletionEvent::ReasoningDetails(_))
| Ok(LanguageModelCompletionEvent::Stop(_)) => {}
Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
@@ -1369,7 +1366,6 @@ impl ThreadDialog {
role: Role::Assistant,
content,
cache: false,
reasoning_details: None,
})
} else {
None

View File

@@ -267,9 +267,10 @@ impl ExtensionManifest {
let mut extension_manifest_path = extension_dir.join("extension.json");
if fs.is_file(&extension_manifest_path).await {
let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| {
format!("loading {extension_name} extension.json, {extension_manifest_path:?}")
})?;
let manifest_content = fs
.load(&extension_manifest_path)
.await
.with_context(|| format!("failed to load {extension_name} extension.json"))?;
let manifest_json = serde_json::from_str::<OldExtensionManifest>(&manifest_content)
.with_context(|| {
format!("invalid extension.json for extension {extension_name}")
@@ -278,9 +279,10 @@ impl ExtensionManifest {
Ok(manifest_from_old_manifest(manifest_json, extension_name))
} else {
extension_manifest_path.set_extension("toml");
let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| {
format!("loading {extension_name} extension.toml, {extension_manifest_path:?}")
})?;
let manifest_content = fs
.load(&extension_manifest_path)
.await
.with_context(|| format!("failed to load {extension_name} extension.toml"))?;
toml::from_str(&manifest_content).map_err(|err| {
anyhow!("Invalid extension.toml for extension {extension_name}:\n{err}")
})

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