Compare commits
142 Commits
git-panel-
...
load_diffs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fc72f136d | ||
|
|
f213251e2c | ||
|
|
8e79ab6f6a | ||
|
|
ecddbd470f | ||
|
|
42787904b0 | ||
|
|
2bdc385fdf | ||
|
|
615803646f | ||
|
|
101bbebe52 | ||
|
|
d6af4d3cdd | ||
|
|
a32319374d | ||
|
|
716937c9c9 | ||
|
|
097024d46f | ||
|
|
f1c2afdee0 | ||
|
|
ea120dfe18 | ||
|
|
d2988ffc77 | ||
|
|
f17d2c92b6 | ||
|
|
6ee35cb43e | ||
|
|
c1d9dc369c | ||
|
|
696fdd8fed | ||
|
|
980f8bff2a | ||
|
|
2a3bcbfe0f | ||
|
|
5225a84aff | ||
|
|
5c70f8391f | ||
|
|
10efbd5eb4 | ||
|
|
0386f240a9 | ||
|
|
a39ba03bcc | ||
|
|
2c7bcfcb7b | ||
|
|
6bea23e990 | ||
|
|
98da1ea169 | ||
|
|
98a83b47e6 | ||
|
|
5f356d04ff | ||
|
|
73d3f9611e | ||
|
|
d9cfc2c883 | ||
|
|
ee420d530e | ||
|
|
d801d0950e | ||
|
|
3f25d36b3c | ||
|
|
f015368586 | ||
|
|
4bf3b9d62e | ||
|
|
599a217ea5 | ||
|
|
b0a7defd09 | ||
|
|
57e3bcfcf8 | ||
|
|
b2f561165f | ||
|
|
fd1494c31a | ||
|
|
faa1136651 | ||
|
|
6bf5e92a25 | ||
|
|
46ad6c0bbb | ||
|
|
671500de1b | ||
|
|
0519c645fb | ||
|
|
23872b0523 | ||
|
|
4b050b651a | ||
|
|
bb46bc167a | ||
|
|
b274f80dd9 | ||
|
|
d77ab99ab1 | ||
|
|
97792f7fb9 | ||
|
|
9bebf314e0 | ||
|
|
4092e81ada | ||
|
|
e0b64773d9 | ||
|
|
d76c326ff5 | ||
|
|
f1bebd79d1 | ||
|
|
a66a539a09 | ||
|
|
a2d3e3baf9 | ||
|
|
175162af4f | ||
|
|
cdcc068906 | ||
|
|
86484aaded | ||
|
|
d32934a893 | ||
|
|
b463266fa1 | ||
|
|
b0525a26a6 | ||
|
|
1683052e6c | ||
|
|
07cc87b288 | ||
|
|
1277f328c4 | ||
|
|
b3097cfc8a | ||
|
|
305206fd48 | ||
|
|
c387203ac8 | ||
|
|
a260ba6428 | ||
|
|
a8e0de37ac | ||
|
|
a1a599dac5 | ||
|
|
524b97d729 | ||
|
|
8772727034 | ||
|
|
aaa116d129 | ||
|
|
c1096d8b63 | ||
|
|
092071a2f0 | ||
|
|
723f9b1371 | ||
|
|
37523b0007 | ||
|
|
b4167caaf1 | ||
|
|
020f518231 | ||
|
|
ead4f26b52 | ||
|
|
3de3a369f5 | ||
|
|
28a0b82618 | ||
|
|
e2c95a8d84 | ||
|
|
3da4d3aac3 | ||
|
|
6f99eeffa8 | ||
|
|
15ab96af6b | ||
|
|
e80b490ac0 | ||
|
|
3c577ba019 | ||
|
|
e1d295a6b4 | ||
|
|
84f24e4b62 | ||
|
|
03fad4b951 | ||
|
|
c626e770a0 | ||
|
|
fa0c7500c1 | ||
|
|
e91be9e98e | ||
|
|
46eb9e5223 | ||
|
|
cb7bd5fe19 | ||
|
|
b900ac2ac7 | ||
|
|
b709996ec6 | ||
|
|
b6972d70a5 | ||
|
|
ec1664f61a | ||
|
|
c2c5fceb5b | ||
|
|
eadc2301e0 | ||
|
|
b500470391 | ||
|
|
55e4258147 | ||
|
|
8467a1b08b | ||
|
|
fb90b12073 | ||
|
|
92e64f9cf0 | ||
|
|
f318bb5fd7 | ||
|
|
430b55405a | ||
|
|
27f700e2b2 | ||
|
|
b5633f5bc7 | ||
|
|
b9ce52dc95 | ||
|
|
34a7cfb2e5 | ||
|
|
99016e3a85 | ||
|
|
dea3c8c949 | ||
|
|
7eac6d242c | ||
|
|
b92b28314f | ||
|
|
1fc0642de1 | ||
|
|
045ac6d1b6 | ||
|
|
1936f16c62 | ||
|
|
b32559f07d | ||
|
|
28adedf1fa | ||
|
|
c9e231043a | ||
|
|
ede3b1dae6 | ||
|
|
b0700a4625 | ||
|
|
f2a1eb9963 | ||
|
|
0c1ca2a45a | ||
|
|
8fd8b989a6 | ||
|
|
fd837b348f | ||
|
|
6b239c3a9a | ||
|
|
73e5df6445 | ||
|
|
b403c199df | ||
|
|
6effe1f48e | ||
|
|
8d3153abd4 | ||
|
|
3a301afbc6 | ||
|
|
78add792c7 |
@@ -16,9 +16,7 @@ rustflags = ["-D", "warnings"]
|
||||
debug = "limited"
|
||||
|
||||
# Use Mold on Linux, because it's faster than GNU ld and LLD.
|
||||
#
|
||||
# We no longer set this in the default `config.toml` so that developers can opt in to Wild, which
|
||||
# is faster than Mold, in their own ~/.cargo/config.toml.
|
||||
# We dont use wild in CI as its not production ready.
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
@@ -8,6 +8,14 @@ perf-test = ["test", "--profile", "release-fast", "--lib", "--bins", "--tests",
|
||||
# Keep similar flags here to share some ccache
|
||||
perf-compare = ["run", "--profile", "release-fast", "-p", "perf", "--config", "target.'cfg(true)'.rustflags=[\"--cfg\", \"perf_enabled\"]", "--", "compare"]
|
||||
|
||||
# [target.x86_64-unknown-linux-gnu]
|
||||
# linker = "clang"
|
||||
# rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = [
|
||||
"--cfg",
|
||||
|
||||
6
.github/actions/run_tests/action.yml
vendored
6
.github/actions/run_tests/action.yml
vendored
@@ -4,10 +4,8 @@ description: "Runs the tests"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Rust
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
cargo install cargo-nextest --locked
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
|
||||
3
.github/actions/run_tests_windows/action.yml
vendored
3
.github/actions/run_tests_windows/action.yml
vendored
@@ -11,9 +11,8 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install test runner
|
||||
shell: powershell
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: cargo install cargo-nextest --locked
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
|
||||
30
.github/workflows/after_release.yml
vendored
30
.github/workflows/after_release.yml
vendored
@@ -56,14 +56,14 @@ jobs:
|
||||
- id: set-package-name
|
||||
name: after_release::publish_winget::set_package_name
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
PACKAGE_NAME=ZedIndustries.Zed.Preview
|
||||
else
|
||||
PACKAGE_NAME=ZedIndustries.Zed
|
||||
fi
|
||||
if ("${{ github.event.release.prerelease }}" -eq "true") {
|
||||
$PACKAGE_NAME = "ZedIndustries.Zed.Preview"
|
||||
} else {
|
||||
$PACKAGE_NAME = "ZedIndustries.Zed"
|
||||
}
|
||||
|
||||
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT
|
||||
shell: pwsh
|
||||
- name: after_release::publish_winget::winget_releaser
|
||||
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
|
||||
with:
|
||||
@@ -86,3 +86,19 @@ 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 }}
|
||||
|
||||
2
.github/workflows/bump_patch_version.yml
vendored
2
.github/workflows/bump_patch_version.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
which cargo-set-version > /dev/null || cargo install cargo-edit
|
||||
which cargo-set-version > /dev/null || cargo install cargo-edit -f --no-default-features --features "set-version"
|
||||
output="$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')"
|
||||
export GIT_COMMITTER_NAME="Zed Bot"
|
||||
export GIT_COMMITTER_EMAIL="hi@zed.dev"
|
||||
|
||||
5
.github/workflows/cherry_pick.yml
vendored
5
.github/workflows/cherry_pick.yml
vendored
@@ -1,6 +1,7 @@
|
||||
# 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:
|
||||
@@ -16,6 +17,10 @@ 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
|
||||
|
||||
@@ -26,3 +26,4 @@ jobs:
|
||||
ascending: true
|
||||
enable-statistics: true
|
||||
stale-issue-label: "stale"
|
||||
exempt-issue-labels: "never stale"
|
||||
|
||||
3
.github/workflows/compare_perf.yml
vendored
3
.github/workflows/compare_perf.yml
vendored
@@ -39,8 +39,7 @@ jobs:
|
||||
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}
|
||||
uses: taiki-e/install-action@hyperfine
|
||||
- name: steps::git_checkout
|
||||
run: git fetch origin ${{ inputs.base }} && git checkout ${{ inputs.base }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
4
.github/workflows/deploy_collab.yml
vendored
4
.github/workflows/deploy_collab.yml
vendored
@@ -43,9 +43,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install cargo nextest
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: |
|
||||
cargo install cargo-nextest --locked
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Limit target directory size
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@@ -29,9 +29,6 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
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 300
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -78,8 +75,7 @@ jobs:
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: bash -euxo pipefail {0}
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than 250
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -112,9 +108,6 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::cargo_install_nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||
shell: pwsh
|
||||
@@ -484,6 +477,20 @@ 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
|
||||
|
||||
21
.github/workflows/release_nightly.yml
vendored
21
.github/workflows/release_nightly.yml
vendored
@@ -47,9 +47,6 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::cargo_install_nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||
shell: pwsh
|
||||
@@ -493,3 +490,21 @@ 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 }}
|
||||
|
||||
3
.github/workflows/run_cron_unit_evals.yml
vendored
3
.github/workflows/run_cron_unit_evals.yml
vendored
@@ -37,8 +37,7 @@ jobs:
|
||||
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}
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than 250
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
9
.github/workflows/run_tests.yml
vendored
9
.github/workflows/run_tests.yml
vendored
@@ -113,9 +113,6 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::cargo_install_nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than.ps1 250
|
||||
shell: pwsh
|
||||
@@ -164,8 +161,7 @@ jobs:
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
run: cargo install cargo-nextest --locked
|
||||
shell: bash -euxo pipefail {0}
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than 250
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -200,9 +196,6 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
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 300
|
||||
shell: bash -euxo pipefail {0}
|
||||
|
||||
5
.github/workflows/run_unit_evals.yml
vendored
5
.github/workflows/run_unit_evals.yml
vendored
@@ -46,8 +46,7 @@ jobs:
|
||||
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}
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
run: ./script/clear-target-dir-if-larger-than 250
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -66,5 +65,5 @@ jobs:
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
124
Cargo.lock
generated
124
Cargo.lock
generated
@@ -322,6 +322,7 @@ dependencies = [
|
||||
"assistant_slash_command",
|
||||
"assistant_slash_commands",
|
||||
"assistant_text_thread",
|
||||
"async-fs",
|
||||
"audio",
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
@@ -343,6 +344,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"html_to_markdown",
|
||||
"http_client",
|
||||
"image",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"jsonschema",
|
||||
@@ -1461,6 +1463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"untrusted 0.7.1",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -2614,26 +2617,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "calloop"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
|
||||
version = "0.14.3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"log",
|
||||
"polling",
|
||||
"rustix 0.38.44",
|
||||
"rustix 1.1.2",
|
||||
"slab",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop-wayland-source"
|
||||
version = "0.3.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
|
||||
checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
|
||||
dependencies = [
|
||||
"calloop",
|
||||
"rustix 0.38.44",
|
||||
"rustix 1.1.2",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
]
|
||||
@@ -5311,6 +5311,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"supermaven",
|
||||
"sweep_ai",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -5860,6 +5861,7 @@ dependencies = [
|
||||
"lsp",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"proto",
|
||||
"semantic_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -8658,23 +8660,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-protocol"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c047f6b5e551563af2ddb13dafed833f0ec5a5b0f9621d5ad740a9ff1e1095"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bytes 1.10.1",
|
||||
"chrono",
|
||||
"futures 0.3.31",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-websocket-client"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4197fa926a6b0bddfed7377d9fed3d00a0dec44a1501e020097bd26604699cae"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -8683,6 +8687,7 @@ dependencies = [
|
||||
"jupyter-protocol",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
@@ -8872,6 +8877,7 @@ dependencies = [
|
||||
"icons",
|
||||
"image",
|
||||
"log",
|
||||
"open_ai",
|
||||
"open_router",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
@@ -10021,6 +10027,7 @@ name = "miniprofiler_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"log",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"util",
|
||||
@@ -10232,8 +10239,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nbformat"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89c7229d604d847227002715e1235cd84e81919285d904ccb290a42ecc409348"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -10519,11 +10527,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
|
||||
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"libm",
|
||||
"num-integer",
|
||||
@@ -11026,6 +11033,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum 0.27.2",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13065,6 +13073,23 @@ dependencies = [
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project_benchmarks"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"client",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
"node_runtime",
|
||||
"project",
|
||||
"settings",
|
||||
"watch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project_panel"
|
||||
version = "0.1.0"
|
||||
@@ -14011,6 +14036,7 @@ dependencies = [
|
||||
"paths",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rayon",
|
||||
"release_channel",
|
||||
@@ -14256,7 +14282,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.2.16",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"untrusted 0.9.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -14385,9 +14411,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.8"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
|
||||
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"digest",
|
||||
@@ -14437,25 +14463,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "runtimelib"
|
||||
version = "0.25.0"
|
||||
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "481b48894073a0096f28cbe9860af01fc1b861e55b3bc96afafc645ee3de62dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-dispatcher",
|
||||
"async-std",
|
||||
"aws-lc-rs",
|
||||
"base64 0.22.1",
|
||||
"bytes 1.10.1",
|
||||
"chrono",
|
||||
"data-encoding",
|
||||
"dirs 5.0.1",
|
||||
"dirs 6.0.0",
|
||||
"futures 0.3.31",
|
||||
"glob",
|
||||
"jupyter-protocol",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand 3.1.1",
|
||||
"smol",
|
||||
"thiserror 2.0.17",
|
||||
"uuid",
|
||||
"zeromq",
|
||||
]
|
||||
@@ -14723,7 +14750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14735,7 +14762,7 @@ dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14965,7 +14992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -16562,6 +16589,33 @@ dependencies = [
|
||||
"zeno",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sweep_ai"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"brotli",
|
||||
"client",
|
||||
"collections",
|
||||
"edit_prediction",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"indoc",
|
||||
"language",
|
||||
"project",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tree-sitter-rust",
|
||||
"util",
|
||||
"workspace",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia"
|
||||
version = "0.5.5"
|
||||
@@ -18558,6 +18612,12 @@ version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@@ -21183,7 +21243,6 @@ dependencies = [
|
||||
"audio",
|
||||
"auto_update",
|
||||
"auto_update_ui",
|
||||
"backtrace",
|
||||
"bincode 1.3.3",
|
||||
"breadcrumbs",
|
||||
"call",
|
||||
@@ -21248,7 +21307,6 @@ dependencies = [
|
||||
"mimalloc",
|
||||
"miniprofiler_ui",
|
||||
"nc",
|
||||
"nix 0.29.0",
|
||||
"node_runtime",
|
||||
"notifications",
|
||||
"onboarding",
|
||||
@@ -21284,13 +21342,13 @@ dependencies = [
|
||||
"snippets_ui",
|
||||
"supermaven",
|
||||
"svg_preview",
|
||||
"sweep_ai",
|
||||
"sysinfo 0.37.2",
|
||||
"system_specs",
|
||||
"tab_switcher",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
"theme_extension",
|
||||
|
||||
35
Cargo.toml
35
Cargo.toml
@@ -127,6 +127,7 @@ members = [
|
||||
"crates/picker",
|
||||
"crates/prettier",
|
||||
"crates/project",
|
||||
"crates/project_benchmarks",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/prompt_store",
|
||||
@@ -164,6 +165,7 @@ members = [
|
||||
"crates/sum_tree",
|
||||
"crates/supermaven",
|
||||
"crates/supermaven_api",
|
||||
"crates/sweep_ai",
|
||||
"crates/codestral",
|
||||
"crates/svg_preview",
|
||||
"crates/system_specs",
|
||||
@@ -397,6 +399,7 @@ streaming_diff = { path = "crates/streaming_diff" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
supermaven_api = { path = "crates/supermaven_api" }
|
||||
sweep_ai = { path = "crates/sweep_ai" }
|
||||
codestral = { path = "crates/codestral" }
|
||||
system_specs = { path = "crates/system_specs" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
@@ -477,6 +480,7 @@ bitflags = "2.6.0"
|
||||
blade-graphics = { version = "0.7.0" }
|
||||
blade-macros = { version = "0.3.0" }
|
||||
blade-util = { version = "0.3.0" }
|
||||
brotli = "8.0.2"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
@@ -484,7 +488,7 @@ cfg-if = "1.0.3"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ciborium = "0.2"
|
||||
circular-buffer = "1.0"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clap = { version = "4.4", features = ["derive", "wrap_help"] }
|
||||
cocoa = "=0.26.0"
|
||||
cocoa-foundation = "=0.2.0"
|
||||
convert_case = "0.8.0"
|
||||
@@ -533,8 +537,8 @@ itertools = "0.14.0"
|
||||
json_dotpath = "1.1"
|
||||
jsonschema = "0.30.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
jupyter-protocol = "0.10.0"
|
||||
jupyter-websocket-client = "0.15.0"
|
||||
libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
@@ -547,7 +551,7 @@ minidumper = "0.8"
|
||||
moka = { version = "0.12.10", features = ["sync"] }
|
||||
naga = { version = "25.0", features = ["wgsl-in"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nbformat = "0.15.0"
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
num-traits = "0.2"
|
||||
@@ -618,8 +622,8 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662
|
||||
"stream",
|
||||
], package = "zed-reqwest", version = "0.12.15-zed" }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
runtimelib = { version = "0.30.0", default-features = false, features = [
|
||||
"async-dispatcher-runtime", "aws-lc-rs"
|
||||
] }
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
rustc-hash = "2.1.0"
|
||||
@@ -630,6 +634,7 @@ scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
||||
serde_derive = "1.0.221"
|
||||
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
|
||||
serde_json_lenient = { version = "0.2", features = [
|
||||
"preserve_order",
|
||||
@@ -723,6 +728,7 @@ yawc = "0.2.5"
|
||||
zeroize = "1.8"
|
||||
zstd = "0.11"
|
||||
|
||||
|
||||
[workspace.dependencies.windows]
|
||||
version = "0.61"
|
||||
features = [
|
||||
@@ -778,6 +784,7 @@ features = [
|
||||
notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
|
||||
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
|
||||
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
||||
calloop = { path = "/home/davidsk/tmp/calloop" }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
@@ -791,6 +798,19 @@ codegen-units = 16
|
||||
codegen-units = 16
|
||||
|
||||
[profile.dev.package]
|
||||
# proc-macros start
|
||||
gpui_macros = { opt-level = 3 }
|
||||
derive_refineable = { opt-level = 3 }
|
||||
settings_macros = { opt-level = 3 }
|
||||
sqlez_macros = { opt-level = 3, codegen-units = 1 }
|
||||
ui_macros = { opt-level = 3 }
|
||||
util_macros = { opt-level = 3 }
|
||||
serde_derive = { opt-level = 3 }
|
||||
quote = { opt-level = 3 }
|
||||
syn = { opt-level = 3 }
|
||||
proc-macro2 = { opt-level = 3 }
|
||||
# proc-macros end
|
||||
|
||||
taffy = { opt-level = 3 }
|
||||
cranelift-codegen = { opt-level = 3 }
|
||||
cranelift-codegen-meta = { opt-level = 3 }
|
||||
@@ -832,7 +852,6 @@ semantic_version = { codegen-units = 1 }
|
||||
session = { codegen-units = 1 }
|
||||
snippet = { codegen-units = 1 }
|
||||
snippets_ui = { codegen-units = 1 }
|
||||
sqlez_macros = { codegen-units = 1 }
|
||||
story = { codegen-units = 1 }
|
||||
supermaven_api = { codegen-units = 1 }
|
||||
telemetry_events = { codegen-units = 1 }
|
||||
@@ -842,7 +861,7 @@ ui_input = { codegen-units = 1 }
|
||||
zed_actions = { codegen-units = 1 }
|
||||
|
||||
[profile.release]
|
||||
debug = "limited"
|
||||
debug = "full"
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.90-bookworm as builder
|
||||
FROM rust:1.91.1-bookworm as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ design
|
||||
|
||||
docs
|
||||
= @probably-neb
|
||||
= @miguelraz
|
||||
|
||||
extension
|
||||
= @kubkon
|
||||
@@ -98,6 +99,9 @@ settings_ui
|
||||
= @danilo-leal
|
||||
= @probably-neb
|
||||
|
||||
support
|
||||
= @miguelraz
|
||||
|
||||
tasks
|
||||
= @SomeoneToIgnore
|
||||
= @Veykril
|
||||
|
||||
1
assets/icons/sweep_ai.svg
Normal file
1
assets/icons/sweep_ai.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
@@ -313,7 +313,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewTextThread",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
"cmd-alt-n": "agent::NewExternalAgentThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -421,12 +421,6 @@
|
||||
"ctrl-[": "editor::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == helix_select && !menu",
|
||||
"bindings": {
|
||||
"escape": "vim::SwitchToHelixNormalMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
|
||||
"bindings": {
|
||||
|
||||
@@ -742,6 +742,16 @@
|
||||
// "never"
|
||||
"show": "always"
|
||||
},
|
||||
// Sort order for entries in the project panel.
|
||||
// This setting can take three values:
|
||||
//
|
||||
// 1. Show directories first, then files:
|
||||
// "directories_first"
|
||||
// 2. Mix directories and files together:
|
||||
// "mixed"
|
||||
// 3. Show files first, then directories:
|
||||
// "files_first"
|
||||
"sort_mode": "directories_first",
|
||||
// Whether to enable drag-and-drop operations in the project panel.
|
||||
"drag_and_drop": true,
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
|
||||
@@ -15,12 +15,14 @@ const SEPARATOR_MARKER: &str = "=======";
|
||||
const REPLACE_MARKER: &str = ">>>>>>> REPLACE";
|
||||
const SONNET_PARAMETER_INVOKE_1: &str = "</parameter>\n</invoke>";
|
||||
const SONNET_PARAMETER_INVOKE_2: &str = "</parameter></invoke>";
|
||||
const END_TAGS: [&str; 5] = [
|
||||
const SONNET_PARAMETER_INVOKE_3: &str = "</parameter>";
|
||||
const END_TAGS: [&str; 6] = [
|
||||
OLD_TEXT_END_TAG,
|
||||
NEW_TEXT_END_TAG,
|
||||
EDITS_END_TAG,
|
||||
SONNET_PARAMETER_INVOKE_1, // Remove this after switching to streaming tool call
|
||||
SONNET_PARAMETER_INVOKE_1, // Remove these after switching to streaming tool call
|
||||
SONNET_PARAMETER_INVOKE_2,
|
||||
SONNET_PARAMETER_INVOKE_3,
|
||||
];
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -567,21 +569,29 @@ mod tests {
|
||||
parse_random_chunks(
|
||||
indoc! {"
|
||||
<old_text>some text</old_text><new_text>updated text</parameter></invoke>
|
||||
<old_text>more text</old_text><new_text>upd</parameter></new_text>
|
||||
"},
|
||||
&mut parser,
|
||||
&mut rng
|
||||
),
|
||||
vec![Edit {
|
||||
old_text: "some text".to_string(),
|
||||
new_text: "updated text".to_string(),
|
||||
line_hint: None,
|
||||
},]
|
||||
vec![
|
||||
Edit {
|
||||
old_text: "some text".to_string(),
|
||||
new_text: "updated text".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
Edit {
|
||||
old_text: "more text".to_string(),
|
||||
new_text: "upd".to_string(),
|
||||
line_hint: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
parser.finish(),
|
||||
EditParserMetrics {
|
||||
tags: 2,
|
||||
mismatched_tags: 1
|
||||
tags: 4,
|
||||
mismatched_tags: 2
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -607,6 +607,8 @@ pub struct Thread {
|
||||
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||
pub(crate) project: Entity<Project>,
|
||||
pub(crate) action_log: Entity<ActionLog>,
|
||||
/// Tracks the last time files were read by the agent, to detect external modifications
|
||||
pub(crate) file_read_times: HashMap<PathBuf, fs::MTime>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
@@ -665,6 +667,7 @@ impl Thread {
|
||||
prompt_capabilities_rx,
|
||||
project,
|
||||
action_log,
|
||||
file_read_times: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,6 +863,7 @@ impl Thread {
|
||||
updated_at: db_thread.updated_at,
|
||||
prompt_capabilities_tx,
|
||||
prompt_capabilities_rx,
|
||||
file_read_times: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -999,6 +1003,7 @@ impl Thread {
|
||||
self.add_tool(NowTool);
|
||||
self.add_tool(OpenTool::new(self.project.clone()));
|
||||
self.add_tool(ReadFileTool::new(
|
||||
cx.weak_entity(),
|
||||
self.project.clone(),
|
||||
self.action_log.clone(),
|
||||
));
|
||||
|
||||
@@ -309,6 +309,40 @@ impl AgentTool for EditFileTool {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
// Check if the file has been modified since the agent last read it
|
||||
if let Some(abs_path) = abs_path.as_ref() {
|
||||
let (last_read_mtime, current_mtime, is_dirty) = self.thread.update(cx, |thread, cx| {
|
||||
let last_read = thread.file_read_times.get(abs_path).copied();
|
||||
let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime());
|
||||
let dirty = buffer.read(cx).is_dirty();
|
||||
(last_read, current, dirty)
|
||||
})?;
|
||||
|
||||
// Check for unsaved changes first - these indicate modifications we don't know about
|
||||
if is_dirty {
|
||||
anyhow::bail!(
|
||||
"This file cannot be written to because it has unsaved changes. \
|
||||
Please end the current conversation immediately by telling the user you want to write to this file (mention its path explicitly) but you can't write to it because it has unsaved changes. \
|
||||
Ask the user to save that buffer's changes and to inform you when it's ok to proceed."
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the file was modified on disk since we last read it
|
||||
if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) {
|
||||
// MTime can be unreliable for comparisons, so our newtype intentionally
|
||||
// doesn't support comparing them. If the mtime at all different
|
||||
// (which could be because of a modification or because e.g. system clock changed),
|
||||
// we pessimistically assume it was modified.
|
||||
if current != last_read {
|
||||
anyhow::bail!(
|
||||
"The file {} has been modified since you last read it. \
|
||||
Please read the file again to get the current state before editing it.",
|
||||
input.path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
|
||||
event_stream.update_diff(diff.clone());
|
||||
let _finalize_diff = util::defer({
|
||||
@@ -421,6 +455,17 @@ impl AgentTool for EditFileTool {
|
||||
log.buffer_edited(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
// Update the recorded read time after a successful edit so consecutive edits work
|
||||
if let Some(abs_path) = abs_path.as_ref() {
|
||||
if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| {
|
||||
buffer.file().and_then(|file| file.disk_state().mtime())
|
||||
})? {
|
||||
self.thread.update(cx, |thread, _| {
|
||||
thread.file_read_times.insert(abs_path.to_path_buf(), new_mtime);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let (new_text, unified_diff) = cx
|
||||
.background_spawn({
|
||||
@@ -1748,10 +1793,426 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_read_times_tracking(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
// Initially, file_read_times should be empty
|
||||
let is_empty = thread.read_with(cx, |thread, _| thread.file_read_times.is_empty());
|
||||
assert!(is_empty, "file_read_times should start empty");
|
||||
|
||||
// Create read tool
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
|
||||
// Read the file to record the read time
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that file_read_times now contains an entry for the file
|
||||
let has_entry = thread.read_with(cx, |thread, _| {
|
||||
thread.file_read_times.len() == 1
|
||||
&& thread
|
||||
.file_read_times
|
||||
.keys()
|
||||
.any(|path| path.ends_with("test.txt"))
|
||||
});
|
||||
assert!(
|
||||
has_entry,
|
||||
"file_read_times should contain an entry after reading the file"
|
||||
);
|
||||
|
||||
// Read the file again - should update the entry
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Should still have exactly one entry
|
||||
let has_one_entry = thread.read_with(cx, |thread, _| thread.file_read_times.len() == 1);
|
||||
assert!(
|
||||
has_one_entry,
|
||||
"file_read_times should still have one entry after re-reading"
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_consecutive_edits_work(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let languages = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
let edit_tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Read the file first
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// First edit should work
|
||||
let edit_result = {
|
||||
let edit_task = cx.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "First edit".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
model.send_last_completion_stream_text_chunk(
|
||||
"<old_text>original content</old_text><new_text>modified content</new_text>"
|
||||
.to_string(),
|
||||
);
|
||||
model.end_last_completion_stream();
|
||||
|
||||
edit_task.await
|
||||
};
|
||||
assert!(
|
||||
edit_result.is_ok(),
|
||||
"First edit should succeed, got error: {:?}",
|
||||
edit_result.as_ref().err()
|
||||
);
|
||||
|
||||
// Second edit should also work because the edit updated the recorded read time
|
||||
let edit_result = {
|
||||
let edit_task = cx.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "Second edit".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
model.send_last_completion_stream_text_chunk(
|
||||
"<old_text>modified content</old_text><new_text>further modified content</new_text>".to_string(),
|
||||
);
|
||||
model.end_last_completion_stream();
|
||||
|
||||
edit_task.await
|
||||
};
|
||||
assert!(
|
||||
edit_result.is_ok(),
|
||||
"Second consecutive edit should succeed, got error: {:?}",
|
||||
edit_result.as_ref().err()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_external_modification_detected(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let languages = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
let edit_tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Read the file first
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Simulate external modification - advance time and save file
|
||||
cx.background_executor
|
||||
.advance_clock(std::time::Duration::from_secs(2));
|
||||
fs.save(
|
||||
path!("/root/test.txt").as_ref(),
|
||||
&"externally modified content".into(),
|
||||
language::LineEnding::Unix,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Reload the buffer to pick up the new mtime
|
||||
let project_path = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.find_project_path("root/test.txt", cx)
|
||||
})
|
||||
.expect("Should find project path");
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
buffer
|
||||
.update(cx, |buffer, cx| buffer.reload(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
// Try to edit - should fail because file was modified externally
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit after external change".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Edit should fail after external modification"
|
||||
);
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("has been modified since you last read it"),
|
||||
"Error should mention file modification, got: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dirty_buffer_detected(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"test.txt": "original content"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let languages = project.read_with(cx, |project, _| project.languages().clone());
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let read_tool = Arc::new(crate::ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log,
|
||||
));
|
||||
let edit_tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages,
|
||||
Templates::new(),
|
||||
));
|
||||
|
||||
// Read the file first
|
||||
cx.update(|cx| {
|
||||
read_tool.clone().run(
|
||||
crate::ReadFileToolInput {
|
||||
path: "root/test.txt".to_string(),
|
||||
start_line: None,
|
||||
end_line: None,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Open the buffer and make it dirty by editing without saving
|
||||
let project_path = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.find_project_path("root/test.txt", cx)
|
||||
})
|
||||
.expect("Should find project path");
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make an in-memory edit to the buffer (making it dirty)
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let end_point = buffer.max_point();
|
||||
buffer.edit([(end_point..end_point, " added text")], None, cx);
|
||||
});
|
||||
|
||||
// Verify buffer is dirty
|
||||
let is_dirty = buffer.read_with(cx, |buffer, _| buffer.is_dirty());
|
||||
assert!(is_dirty, "Buffer should be dirty after in-memory edit");
|
||||
|
||||
// Try to edit - should fail because buffer has unsaved changes
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
edit_tool.clone().run(
|
||||
EditFileToolInput {
|
||||
display_description: "Edit with dirty buffer".into(),
|
||||
path: "root/test.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
},
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
assert!(result.is_err(), "Edit should fail when buffer is dirty");
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("cannot be written to because it has unsaved changes"),
|
||||
"Error should mention unsaved changes, got: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, ToolCallUpdateFields};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use gpui::{App, Entity, SharedString, Task, WeakEntity};
|
||||
use indoc::formatdoc;
|
||||
use language::Point;
|
||||
use language_model::{LanguageModelImage, LanguageModelToolResultContent};
|
||||
@@ -12,7 +12,7 @@ use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
|
||||
use crate::{AgentTool, ToolCallEventStream, outline};
|
||||
use crate::{AgentTool, Thread, ToolCallEventStream, outline};
|
||||
|
||||
/// Reads the content of the given file in the project.
|
||||
///
|
||||
@@ -42,13 +42,19 @@ pub struct ReadFileToolInput {
|
||||
}
|
||||
|
||||
pub struct ReadFileTool {
|
||||
thread: WeakEntity<Thread>,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
}
|
||||
|
||||
impl ReadFileTool {
|
||||
pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
|
||||
pub fn new(
|
||||
thread: WeakEntity<Thread>,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
project,
|
||||
action_log,
|
||||
}
|
||||
@@ -195,6 +201,17 @@ impl AgentTool for ReadFileTool {
|
||||
anyhow::bail!("{file_path} not found");
|
||||
}
|
||||
|
||||
// Record the file read time and mtime
|
||||
if let Some(mtime) = buffer.read_with(cx, |buffer, _| {
|
||||
buffer.file().and_then(|file| file.disk_state().mtime())
|
||||
})? {
|
||||
self.thread
|
||||
.update(cx, |thread, _| {
|
||||
thread.file_read_times.insert(abs_path.to_path_buf(), mtime);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
let mut anchor = None;
|
||||
|
||||
// Check if specific line ranges are provided
|
||||
@@ -285,11 +302,15 @@ impl AgentTool for ReadFileTool {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{ContextServerRegistry, Templates, Thread};
|
||||
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use project::{FakeFs, Project};
|
||||
use prompt_store::ProjectContext;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -300,7 +321,20 @@ mod test {
|
||||
fs.insert_tree(path!("/root"), json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let (event_stream, _) = ToolCallEventStream::test();
|
||||
|
||||
let result = cx
|
||||
@@ -333,7 +367,20 @@ mod test {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = ReadFileToolInput {
|
||||
@@ -363,7 +410,20 @@ mod test {
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(Arc::new(rust_lang()));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = ReadFileToolInput {
|
||||
@@ -435,7 +495,20 @@ mod test {
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = ReadFileToolInput {
|
||||
@@ -463,7 +536,20 @@ mod test {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
|
||||
// start_line of 0 should be treated as 1
|
||||
let result = cx
|
||||
@@ -607,7 +693,20 @@ mod test {
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project, action_log));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
|
||||
|
||||
// Reading a file outside the project worktree should fail
|
||||
let result = cx
|
||||
@@ -821,7 +920,24 @@ mod test {
|
||||
.await;
|
||||
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(ReadFileTool::new(
|
||||
thread.downgrade(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
));
|
||||
|
||||
// Test reading allowed files in worktree1
|
||||
let result = cx
|
||||
|
||||
@@ -98,6 +98,8 @@ util.workspace = true
|
||||
watch.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
image.workspace = true
|
||||
async-fs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
acp_thread = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -28,6 +28,7 @@ use gpui::{
|
||||
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
|
||||
Subscription, Task, TextStyle, WeakEntity, pulsating_between,
|
||||
};
|
||||
use itertools::Either;
|
||||
use language::{Buffer, Language, language_settings::InlayHintKind};
|
||||
use language_model::LanguageModelImage;
|
||||
use postage::stream::Stream as _;
|
||||
@@ -912,74 +913,114 @@ impl MessageEditor {
|
||||
if !self.prompt_capabilities.borrow().image {
|
||||
return;
|
||||
}
|
||||
|
||||
let images = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| {
|
||||
item.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::Image(image) = entry {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if images.is_empty() {
|
||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||
return;
|
||||
}
|
||||
cx.stop_propagation();
|
||||
};
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
use itertools::Itertools;
|
||||
let (mut images, paths) = clipboard
|
||||
.into_entries()
|
||||
.filter_map(|entry| match entry {
|
||||
ClipboardEntry::Image(image) => Some(Either::Left(image)),
|
||||
ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
|
||||
_ => None,
|
||||
})
|
||||
.partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
|
||||
|
||||
let replacement_text = MentionUri::PastedImage.as_link().to_string();
|
||||
for image in images {
|
||||
let (excerpt_id, text_anchor, multibuffer_anchor) =
|
||||
self.editor.update(cx, |message_editor, cx| {
|
||||
let snapshot = message_editor.snapshot(window, cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.buffer_snapshot().as_singleton().unwrap();
|
||||
if !paths.is_empty() {
|
||||
images.extend(
|
||||
cx.background_spawn(async move {
|
||||
let mut images = vec![];
|
||||
for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
|
||||
let Ok(content) = async_fs::read(path).await else {
|
||||
continue;
|
||||
};
|
||||
let Ok(format) = image::guess_format(&content) else {
|
||||
continue;
|
||||
};
|
||||
images.push(gpui::Image::from_bytes(
|
||||
match format {
|
||||
image::ImageFormat::Png => gpui::ImageFormat::Png,
|
||||
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
|
||||
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
|
||||
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
|
||||
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
|
||||
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
|
||||
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
|
||||
_ => continue,
|
||||
},
|
||||
content,
|
||||
));
|
||||
}
|
||||
images
|
||||
})
|
||||
.await,
|
||||
);
|
||||
}
|
||||
|
||||
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
|
||||
let multibuffer_anchor = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_in_excerpt(*excerpt_id, text_anchor);
|
||||
message_editor.edit(
|
||||
[(
|
||||
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
|
||||
format!("{replacement_text} "),
|
||||
)],
|
||||
if images.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let replacement_text = MentionUri::PastedImage.as_link().to_string();
|
||||
let Ok(editor) = this.update(cx, |this, cx| {
|
||||
cx.stop_propagation();
|
||||
this.editor.clone()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
for image in images {
|
||||
let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
|
||||
editor.update_in(cx, |message_editor, window, cx| {
|
||||
let snapshot = message_editor.snapshot(window, cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.buffer_snapshot().as_singleton().unwrap();
|
||||
|
||||
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
|
||||
let multibuffer_anchor = snapshot
|
||||
.buffer_snapshot()
|
||||
.anchor_in_excerpt(*excerpt_id, text_anchor);
|
||||
message_editor.edit(
|
||||
[(
|
||||
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
|
||||
format!("{replacement_text} "),
|
||||
)],
|
||||
cx,
|
||||
);
|
||||
(*excerpt_id, text_anchor, multibuffer_anchor)
|
||||
})
|
||||
else {
|
||||
break;
|
||||
};
|
||||
|
||||
let content_len = replacement_text.len();
|
||||
let Some(start_anchor) = multibuffer_anchor else {
|
||||
continue;
|
||||
};
|
||||
let Ok(end_anchor) = editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let image = Arc::new(image);
|
||||
let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
|
||||
insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
MentionUri::PastedImage.name().into(),
|
||||
IconName::Image.path().into(),
|
||||
Some(Task::ready(Ok(image.clone())).shared()),
|
||||
editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
(*excerpt_id, text_anchor, multibuffer_anchor)
|
||||
});
|
||||
|
||||
let content_len = replacement_text.len();
|
||||
let Some(start_anchor) = multibuffer_anchor else {
|
||||
continue;
|
||||
};
|
||||
let end_anchor = self.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
|
||||
});
|
||||
let image = Arc::new(image);
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
MentionUri::PastedImage.name().into(),
|
||||
IconName::Image.path().into(),
|
||||
Some(Task::ready(Ok(image.clone())).shared()),
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let task = cx
|
||||
.spawn_in(window, {
|
||||
async move |_, cx| {
|
||||
)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let task = cx
|
||||
.spawn(async move |cx| {
|
||||
let format = image.format;
|
||||
let image = cx
|
||||
.update(|_, cx| LanguageModelImage::from_image(image, cx))
|
||||
@@ -994,15 +1035,16 @@ impl MessageEditor {
|
||||
} else {
|
||||
Err("Failed to convert image".into())
|
||||
}
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.mention_set
|
||||
.mentions
|
||||
.insert(crease_id, (MentionUri::PastedImage, task.clone()))
|
||||
})
|
||||
.shared();
|
||||
.ok();
|
||||
|
||||
self.mention_set
|
||||
.mentions
|
||||
.insert(crease_id, (MentionUri::PastedImage, task.clone()));
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if task.await.notify_async_err(cx).is_none() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.editor.update(cx, |editor, cx| {
|
||||
@@ -1012,9 +1054,9 @@ impl MessageEditor {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn insert_dragged_files(
|
||||
|
||||
@@ -251,17 +251,17 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.start_slot::<Icon>(model_info.icon.map(|icon| {
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pl_0p5()
|
||||
.gap_1p5()
|
||||
.w(px(240.))
|
||||
.when_some(model_info.icon, |this, icon| {
|
||||
this.child(
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small)
|
||||
)
|
||||
})
|
||||
.child(Label::new(model_info.name.clone()).truncate()),
|
||||
)
|
||||
.end_slot(div().pr_3().when(is_selected, |this| {
|
||||
|
||||
@@ -51,7 +51,7 @@ use ui::{
|
||||
PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use workspace::{CollaboratorId, NewTerminal, Workspace};
|
||||
use zed_actions::agent::{Chat, ToggleModelSelector};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
|
||||
@@ -69,8 +69,8 @@ use crate::ui::{
|
||||
};
|
||||
use crate::{
|
||||
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
|
||||
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, OpenHistory, RejectAll,
|
||||
RejectOnce, ToggleBurnMode, ToggleProfileSelector,
|
||||
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory,
|
||||
RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
@@ -278,6 +278,7 @@ pub struct AcpThreadView {
|
||||
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
|
||||
thread_retry_status: Option<RetryStatus>,
|
||||
thread_error: Option<ThreadError>,
|
||||
thread_error_markdown: Option<Entity<Markdown>>,
|
||||
thread_feedback: ThreadFeedbackState,
|
||||
list_state: ListState,
|
||||
auth_task: Option<Task<()>>,
|
||||
@@ -415,6 +416,7 @@ impl AcpThreadView {
|
||||
list_state: list_state,
|
||||
thread_retry_status: None,
|
||||
thread_error: None,
|
||||
thread_error_markdown: None,
|
||||
thread_feedback: Default::default(),
|
||||
auth_task: None,
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
@@ -798,6 +800,7 @@ impl AcpThreadView {
|
||||
|
||||
if should_retry {
|
||||
self.thread_error = None;
|
||||
self.thread_error_markdown = None;
|
||||
self.reset(window, cx);
|
||||
}
|
||||
}
|
||||
@@ -1327,6 +1330,7 @@ impl AcpThreadView {
|
||||
|
||||
fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
|
||||
self.thread_error = None;
|
||||
self.thread_error_markdown = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -3140,7 +3144,7 @@ impl AcpThreadView {
|
||||
.text_ui_sm(cx)
|
||||
.h_full()
|
||||
.children(terminal_view.map(|terminal_view| {
|
||||
if terminal_view
|
||||
let element = if terminal_view
|
||||
.read(cx)
|
||||
.content_mode(window, cx)
|
||||
.is_scrollable()
|
||||
@@ -3148,7 +3152,15 @@ impl AcpThreadView {
|
||||
div().h_72().child(terminal_view).into_any_element()
|
||||
} else {
|
||||
terminal_view.into_any_element()
|
||||
}
|
||||
};
|
||||
|
||||
div()
|
||||
.on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
|
||||
window.dispatch_action(NewThread.boxed_clone(), cx);
|
||||
cx.stop_propagation();
|
||||
}))
|
||||
.child(element)
|
||||
.into_any_element()
|
||||
})),
|
||||
)
|
||||
})
|
||||
@@ -5344,9 +5356,9 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_thread_error(&self, cx: &mut Context<Self>) -> Option<Div> {
|
||||
fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
let content = match self.thread_error.as_ref()? {
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), window, cx),
|
||||
ThreadError::Refusal => self.render_refusal_error(cx),
|
||||
ThreadError::AuthenticationRequired(error) => {
|
||||
self.render_authentication_required_error(error.clone(), cx)
|
||||
@@ -5431,7 +5443,12 @@ impl AcpThreadView {
|
||||
.dismiss_action(self.dismiss_error_button(cx))
|
||||
}
|
||||
|
||||
fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
|
||||
fn render_any_thread_error(
|
||||
&mut self,
|
||||
error: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) -> Callout {
|
||||
let can_resume = self
|
||||
.thread()
|
||||
.map_or(false, |thread| thread.read(cx).can_resume(cx));
|
||||
@@ -5444,11 +5461,24 @@ impl AcpThreadView {
|
||||
supports_burn_mode && thread.completion_mode() == CompletionMode::Normal
|
||||
});
|
||||
|
||||
let markdown = if let Some(markdown) = &self.thread_error_markdown {
|
||||
markdown.clone()
|
||||
} else {
|
||||
let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
|
||||
self.thread_error_markdown = Some(markdown.clone());
|
||||
markdown
|
||||
};
|
||||
|
||||
let markdown_style = default_markdown_style(false, true, window, cx);
|
||||
let description = self
|
||||
.render_markdown(markdown, markdown_style)
|
||||
.into_any_element();
|
||||
|
||||
Callout::new()
|
||||
.severity(Severity::Error)
|
||||
.title("Error")
|
||||
.icon(IconName::XCircle)
|
||||
.description(error.clone())
|
||||
.title("An Error Happened")
|
||||
.description_slot(description)
|
||||
.actions_slot(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
@@ -5467,11 +5497,9 @@ impl AcpThreadView {
|
||||
})
|
||||
.when(can_resume, |this| {
|
||||
this.child(
|
||||
Button::new("retry", "Retry")
|
||||
.icon(IconName::RotateCw)
|
||||
.icon_position(IconPosition::Start)
|
||||
IconButton::new("retry", IconName::RotateCw)
|
||||
.icon_size(IconSize::Small)
|
||||
.label_size(LabelSize::Small)
|
||||
.tooltip(Tooltip::text("Retry Generation"))
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.resume_chat(cx);
|
||||
})),
|
||||
@@ -5613,7 +5641,6 @@ impl AcpThreadView {
|
||||
|
||||
IconButton::new("copy", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Copy Error Message"))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
||||
@@ -5623,7 +5650,6 @@ impl AcpThreadView {
|
||||
fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss Error"))
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _, cx| {
|
||||
@@ -5841,7 +5867,7 @@ impl Render for AcpThreadView {
|
||||
None
|
||||
}
|
||||
})
|
||||
.children(self.render_thread_error(cx))
|
||||
.children(self.render_thread_error(window, cx))
|
||||
.when_some(
|
||||
self.new_server_version_available.as_ref().filter(|_| {
|
||||
!has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
|
||||
@@ -5907,7 +5933,6 @@ fn default_markdown_style(
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: colors.element_selection_background,
|
||||
code_block_overflow_x_scroll: true,
|
||||
table_overflow_x_scroll: true,
|
||||
heading_level_styles: Some(HeadingLevelStyles {
|
||||
h1: Some(TextStyleRefinement {
|
||||
font_size: Some(rems(1.15).into()),
|
||||
@@ -5975,6 +6000,7 @@ fn default_markdown_style(
|
||||
},
|
||||
link: TextStyleRefinement {
|
||||
background_color: Some(colors.editor_foreground.opacity(0.025)),
|
||||
color: Some(colors.text_accent),
|
||||
underline: Some(UnderlineStyle {
|
||||
color: Some(colors.text_accent.opacity(0.5)),
|
||||
thickness: px(1.),
|
||||
|
||||
@@ -7,8 +7,8 @@ use anyhow::{Context as _, Result};
|
||||
use context_server::{ContextServerCommand, ContextServerId};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
|
||||
TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
|
||||
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle,
|
||||
Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
|
||||
};
|
||||
use language::{Language, LanguageRegistry};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
@@ -23,7 +23,8 @@ use project::{
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*,
|
||||
CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip,
|
||||
WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{ModalView, Workspace};
|
||||
@@ -252,6 +253,7 @@ pub struct ConfigureContextServerModal {
|
||||
source: ConfigurationSource,
|
||||
state: State,
|
||||
original_server_id: Option<ContextServerId>,
|
||||
scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
impl ConfigureContextServerModal {
|
||||
@@ -361,6 +363,7 @@ impl ConfigureContextServerModal {
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -680,6 +683,7 @@ impl ConfigureContextServerModal {
|
||||
|
||||
impl Render for ConfigureContextServerModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
div()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
@@ -699,14 +703,29 @@ impl Render for ConfigureContextServerModal {
|
||||
Modal::new("configure-context-server", None)
|
||||
.header(self.render_modal_header())
|
||||
.section(
|
||||
Section::new()
|
||||
.child(self.render_modal_description(window, cx))
|
||||
.child(self.render_modal_content(cx))
|
||||
.child(match &self.state {
|
||||
State::Idle => div(),
|
||||
State::Waiting => Self::render_waiting_for_context_server(),
|
||||
State::Error(error) => Self::render_modal_error(error.clone()),
|
||||
}),
|
||||
Section::new().child(
|
||||
div()
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.id("modal-content")
|
||||
.max_h(vh(0.7, window))
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&scroll_handle)
|
||||
.child(self.render_modal_description(window, cx))
|
||||
.child(self.render_modal_content(cx))
|
||||
.child(match &self.state {
|
||||
State::Idle => div(),
|
||||
State::Waiting => {
|
||||
Self::render_waiting_for_context_server()
|
||||
}
|
||||
State::Error(error) => {
|
||||
Self::render_modal_error(error.clone())
|
||||
}
|
||||
}),
|
||||
)
|
||||
.vertical_scrollbar_for(scroll_handle, window, cx),
|
||||
),
|
||||
)
|
||||
.footer(self.render_modal_footer(cx)),
|
||||
)
|
||||
|
||||
@@ -1892,6 +1892,9 @@ impl AgentPanel {
|
||||
.anchor(Corner::TopRight)
|
||||
.with_handle(self.new_thread_menu_handle.clone())
|
||||
.menu({
|
||||
let selected_agent = self.selected_agent.clone();
|
||||
let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let is_via_collab = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
@@ -1905,7 +1908,6 @@ impl AgentPanel {
|
||||
let active_thread = active_thread.clone();
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, cx| {
|
||||
menu.context(focus_handle.clone())
|
||||
.header("Zed Agent")
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
|
||||
@@ -1929,9 +1931,11 @@ impl AgentPanel {
|
||||
}
|
||||
})
|
||||
.item(
|
||||
ContextMenuEntry::new("New Thread")
|
||||
.action(NewThread.boxed_clone())
|
||||
.icon(IconName::Thread)
|
||||
ContextMenuEntry::new("Zed Agent")
|
||||
.when(is_agent_selected(AgentType::NativeAgent) | is_agent_selected(AgentType::TextThread) , |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
.icon(IconName::ZedAgent)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
@@ -1955,10 +1959,10 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Text Thread")
|
||||
ContextMenuEntry::new("Text Thread")
|
||||
.action(NewTextThread.boxed_clone())
|
||||
.icon(IconName::TextThread)
|
||||
.icon_color(Color::Muted)
|
||||
.action(NewTextThread.boxed_clone())
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
@@ -1983,7 +1987,10 @@ impl AgentPanel {
|
||||
.separator()
|
||||
.header("External Agents")
|
||||
.item(
|
||||
ContextMenuEntry::new("New Claude Code")
|
||||
ContextMenuEntry::new("Claude Code")
|
||||
.when(is_agent_selected(AgentType::ClaudeCode), |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
.icon(IconName::AiClaude)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -2009,7 +2016,10 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Codex CLI")
|
||||
ContextMenuEntry::new("Codex CLI")
|
||||
.when(is_agent_selected(AgentType::Codex), |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
.icon(IconName::AiOpenAi)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -2035,7 +2045,10 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.item(
|
||||
ContextMenuEntry::new("New Gemini CLI")
|
||||
ContextMenuEntry::new("Gemini CLI")
|
||||
.when(is_agent_selected(AgentType::Gemini), |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
})
|
||||
.icon(IconName::AiGemini)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_via_collab)
|
||||
@@ -2061,8 +2074,8 @@ impl AgentPanel {
|
||||
}),
|
||||
)
|
||||
.map(|mut menu| {
|
||||
let agent_server_store_read = agent_server_store.read(cx);
|
||||
let agent_names = agent_server_store_read
|
||||
let agent_server_store = agent_server_store.read(cx);
|
||||
let agent_names = agent_server_store
|
||||
.external_agents()
|
||||
.filter(|name| {
|
||||
name.0 != GEMINI_NAME
|
||||
@@ -2071,21 +2084,38 @@ impl AgentPanel {
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let custom_settings = cx
|
||||
.global::<SettingsStore>()
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.clone();
|
||||
|
||||
for agent_name in agent_names {
|
||||
let icon_path = agent_server_store_read.agent_icon(&agent_name);
|
||||
let mut entry =
|
||||
ContextMenuEntry::new(format!("New {}", agent_name));
|
||||
let icon_path = agent_server_store.agent_icon(&agent_name);
|
||||
|
||||
let mut entry = ContextMenuEntry::new(agent_name.clone());
|
||||
|
||||
let command = custom_settings
|
||||
.get(&agent_name.0)
|
||||
.map(|settings| settings.command.clone())
|
||||
.unwrap_or(placeholder_command());
|
||||
|
||||
if let Some(icon_path) = icon_path {
|
||||
entry = entry.custom_icon_svg(icon_path);
|
||||
} else {
|
||||
entry = entry.icon(IconName::Terminal);
|
||||
}
|
||||
entry = entry
|
||||
.when(
|
||||
is_agent_selected(AgentType::Custom {
|
||||
name: agent_name.0.clone(),
|
||||
command: command.clone(),
|
||||
}),
|
||||
|this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_via_collab)
|
||||
.handler({
|
||||
@@ -2125,6 +2155,7 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
menu = menu.item(entry);
|
||||
}
|
||||
|
||||
@@ -2157,7 +2188,7 @@ impl AgentPanel {
|
||||
.id("selected_agent_icon")
|
||||
.when_some(selected_agent_custom_icon, |this, icon_path| {
|
||||
let label = selected_agent_label.clone();
|
||||
this.px(DynamicSpacing::Base02.rems(cx))
|
||||
this.px_1()
|
||||
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
|
||||
@@ -2166,7 +2197,7 @@ impl AgentPanel {
|
||||
.when(!has_custom_icon, |this| {
|
||||
this.when_some(self.selected_agent.icon(), |this, icon| {
|
||||
let label = selected_agent_label.clone();
|
||||
this.px(DynamicSpacing::Base02.rems(cx))
|
||||
this.px_1()
|
||||
.child(Icon::new(icon).color(Color::Muted))
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
|
||||
|
||||
@@ -30,7 +30,10 @@ use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, Entity, SharedString, actions};
|
||||
use language::LanguageRegistry;
|
||||
use language::{
|
||||
LanguageRegistry,
|
||||
language_settings::{AllLanguageSettings, EditPredictionProvider},
|
||||
};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
@@ -286,7 +289,25 @@ pub fn init(
|
||||
|
||||
fn update_command_palette_filter(cx: &mut App) {
|
||||
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
||||
let agent_enabled = AgentSettings::get_global(cx).enabled;
|
||||
let edit_prediction_provider = AllLanguageSettings::get_global(cx)
|
||||
.edit_predictions
|
||||
.provider;
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _| {
|
||||
use editor::actions::{
|
||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
};
|
||||
let edit_prediction_actions = [
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||
TypeId::of::<ShowEditPrediction>(),
|
||||
TypeId::of::<NextEditPrediction>(),
|
||||
TypeId::of::<PreviousEditPrediction>(),
|
||||
TypeId::of::<ToggleEditPrediction>(),
|
||||
];
|
||||
|
||||
if disable_ai {
|
||||
filter.hide_namespace("agent");
|
||||
filter.hide_namespace("assistant");
|
||||
@@ -295,42 +316,47 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
filter.hide_namespace("zed_predict_onboarding");
|
||||
filter.hide_namespace("edit_prediction");
|
||||
|
||||
use editor::actions::{
|
||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
};
|
||||
let edit_prediction_actions = [
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||
TypeId::of::<ShowEditPrediction>(),
|
||||
TypeId::of::<NextEditPrediction>(),
|
||||
TypeId::of::<PreviousEditPrediction>(),
|
||||
TypeId::of::<ToggleEditPrediction>(),
|
||||
];
|
||||
filter.hide_action_types(&edit_prediction_actions);
|
||||
filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||
} else {
|
||||
filter.show_namespace("agent");
|
||||
if agent_enabled {
|
||||
filter.show_namespace("agent");
|
||||
} else {
|
||||
filter.hide_namespace("agent");
|
||||
}
|
||||
|
||||
filter.show_namespace("assistant");
|
||||
filter.show_namespace("copilot");
|
||||
|
||||
match edit_prediction_provider {
|
||||
EditPredictionProvider::None => {
|
||||
filter.hide_namespace("edit_prediction");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
filter.hide_action_types(&edit_prediction_actions);
|
||||
}
|
||||
EditPredictionProvider::Copilot => {
|
||||
filter.show_namespace("edit_prediction");
|
||||
filter.show_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
}
|
||||
EditPredictionProvider::Supermaven => {
|
||||
filter.show_namespace("edit_prediction");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.show_namespace("supermaven");
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
}
|
||||
EditPredictionProvider::Zed
|
||||
| EditPredictionProvider::Codestral
|
||||
| EditPredictionProvider::Experimental(_) => {
|
||||
filter.show_namespace("edit_prediction");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
}
|
||||
}
|
||||
|
||||
filter.show_namespace("zed_predict_onboarding");
|
||||
|
||||
filter.show_namespace("edit_prediction");
|
||||
|
||||
use editor::actions::{
|
||||
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
|
||||
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
|
||||
};
|
||||
let edit_prediction_actions = [
|
||||
TypeId::of::<AcceptEditPrediction>(),
|
||||
TypeId::of::<AcceptPartialEditPrediction>(),
|
||||
TypeId::of::<ShowEditPrediction>(),
|
||||
TypeId::of::<NextEditPrediction>(),
|
||||
TypeId::of::<PreviousEditPrediction>(),
|
||||
TypeId::of::<ToggleEditPrediction>(),
|
||||
];
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
|
||||
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||
}
|
||||
});
|
||||
@@ -420,3 +446,137 @@ fn register_slash_commands(cx: &mut App) {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use editor::actions::AcceptEditPrediction;
|
||||
use gpui::{BorrowAppContext, TestAppContext, px};
|
||||
use project::DisableAiSettings;
|
||||
use settings::{
|
||||
DefaultAgentView, DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
|
||||
// Init settings
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
command_palette_hooks::init(cx);
|
||||
AgentSettings::register(cx);
|
||||
DisableAiSettings::register(cx);
|
||||
AllLanguageSettings::register(cx);
|
||||
});
|
||||
|
||||
let agent_settings = AgentSettings {
|
||||
enabled: true,
|
||||
button: true,
|
||||
dock: DockPosition::Right,
|
||||
default_width: px(300.),
|
||||
default_height: px(600.),
|
||||
default_model: None,
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: vec![],
|
||||
default_profile: AgentProfileId::default(),
|
||||
default_view: DefaultAgentView::Thread,
|
||||
profiles: Default::default(),
|
||||
always_allow_tool_actions: false,
|
||||
notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
|
||||
play_sound_when_agent_done: false,
|
||||
single_file_review: false,
|
||||
model_parameters: vec![],
|
||||
preferred_completion_mode: CompletionMode::Normal,
|
||||
enable_feedback: false,
|
||||
expand_edit_card: true,
|
||||
expand_terminal_card: true,
|
||||
use_modifier_to_send: true,
|
||||
message_editor_min_lines: 1,
|
||||
};
|
||||
|
||||
cx.update(|cx| {
|
||||
AgentSettings::override_global(agent_settings.clone(), cx);
|
||||
DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
|
||||
|
||||
// Initial update
|
||||
update_command_palette_filter(cx);
|
||||
});
|
||||
|
||||
// Assert visible
|
||||
cx.update(|cx| {
|
||||
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
||||
assert!(
|
||||
!filter.is_hidden(&NewThread),
|
||||
"NewThread should be visible by default"
|
||||
);
|
||||
});
|
||||
|
||||
// Disable agent
|
||||
cx.update(|cx| {
|
||||
let mut new_settings = agent_settings.clone();
|
||||
new_settings.enabled = false;
|
||||
AgentSettings::override_global(new_settings, cx);
|
||||
|
||||
// Trigger update
|
||||
update_command_palette_filter(cx);
|
||||
});
|
||||
|
||||
// Assert hidden
|
||||
cx.update(|cx| {
|
||||
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
||||
assert!(
|
||||
filter.is_hidden(&NewThread),
|
||||
"NewThread should be hidden when agent is disabled"
|
||||
);
|
||||
});
|
||||
|
||||
// Test EditPredictionProvider
|
||||
// Enable EditPredictionProvider::Copilot
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project
|
||||
.all_languages
|
||||
.features
|
||||
.get_or_insert(Default::default())
|
||||
.edit_prediction_provider = Some(EditPredictionProvider::Copilot);
|
||||
});
|
||||
});
|
||||
update_command_palette_filter(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
||||
assert!(
|
||||
!filter.is_hidden(&AcceptEditPrediction),
|
||||
"EditPrediction should be visible when provider is Copilot"
|
||||
);
|
||||
});
|
||||
|
||||
// Disable EditPredictionProvider (None)
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |s| {
|
||||
s.project
|
||||
.all_languages
|
||||
.features
|
||||
.get_or_insert(Default::default())
|
||||
.edit_prediction_provider = Some(EditPredictionProvider::None);
|
||||
});
|
||||
});
|
||||
update_command_palette_filter(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
let filter = CommandPaletteFilter::try_global(cx).unwrap();
|
||||
assert!(
|
||||
filter.is_hidden(&AcceptEditPrediction),
|
||||
"EditPrediction should be hidden when provider is None"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,17 +492,15 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.start_slot(
|
||||
Icon::new(model_info.icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pl_0p5()
|
||||
.gap_1p5()
|
||||
.w(px(240.))
|
||||
.child(
|
||||
Icon::new(model_info.icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(Label::new(model_info.model.name().0).truncate()),
|
||||
)
|
||||
.end_slot(div().pr_3().when(is_selected, |this| {
|
||||
|
||||
@@ -1679,7 +1679,7 @@ impl TextThreadEditor {
|
||||
) {
|
||||
cx.stop_propagation();
|
||||
|
||||
let images = if let Some(item) = cx.read_from_clipboard() {
|
||||
let mut images = if let Some(item) = cx.read_from_clipboard() {
|
||||
item.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::Image(image) = entry {
|
||||
@@ -1693,6 +1693,40 @@ impl TextThreadEditor {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if let Some(paths) = cx.read_from_clipboard() {
|
||||
for path in paths
|
||||
.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::ExternalPaths(paths) = entry {
|
||||
Some(paths.paths().to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
{
|
||||
let Ok(content) = std::fs::read(path) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(format) = image::guess_format(&content) else {
|
||||
continue;
|
||||
};
|
||||
images.push(gpui::Image::from_bytes(
|
||||
match format {
|
||||
image::ImageFormat::Png => gpui::ImageFormat::Png,
|
||||
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
|
||||
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
|
||||
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
|
||||
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
|
||||
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
|
||||
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
|
||||
_ => continue,
|
||||
},
|
||||
content,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = if let Some(item) = cx.read_from_clipboard() {
|
||||
item.entries().first().and_then(|entry| {
|
||||
if let ClipboardEntry::String(text) = entry {
|
||||
@@ -2592,12 +2626,11 @@ 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, collapse, window, cx);
|
||||
editor.activate_match(index, matches, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -205,13 +205,9 @@ impl PasswordProxy {
|
||||
} else {
|
||||
ShellKind::Posix
|
||||
};
|
||||
let askpass_program = ASKPASS_PROGRAM
|
||||
.get_or_init(|| current_exec)
|
||||
.try_shell_safe(shell_kind)
|
||||
.context("Failed to shell-escape Askpass program path.")?
|
||||
.to_string();
|
||||
let askpass_program = ASKPASS_PROGRAM.get_or_init(|| current_exec);
|
||||
// Create an askpass script that communicates back to this process.
|
||||
let askpass_script = generate_askpass_script(&askpass_program, &askpass_socket);
|
||||
let askpass_script = generate_askpass_script(shell_kind, askpass_program, &askpass_socket)?;
|
||||
let _task = executor.spawn(async move {
|
||||
maybe!(async move {
|
||||
let listener =
|
||||
@@ -334,23 +330,51 @@ pub fn set_askpass_program(path: std::path::PathBuf) {
|
||||
|
||||
#[inline]
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
|
||||
format!(
|
||||
fn generate_askpass_script(
|
||||
shell_kind: ShellKind,
|
||||
askpass_program: &std::path::Path,
|
||||
askpass_socket: &std::path::Path,
|
||||
) -> Result<String> {
|
||||
let askpass_program = shell_kind.prepend_command_prefix(
|
||||
askpass_program
|
||||
.to_str()
|
||||
.context("Askpass program is on a non-utf8 path")?,
|
||||
);
|
||||
let askpass_program = shell_kind
|
||||
.try_quote_prefix_aware(&askpass_program)
|
||||
.context("Failed to shell-escape Askpass program path")?;
|
||||
let askpass_socket = askpass_socket
|
||||
.try_shell_safe(shell_kind)
|
||||
.context("Failed to shell-escape Askpass socket path")?;
|
||||
let print_args = "printf '%s\\0' \"$@\"";
|
||||
let shebang = "#!/bin/sh";
|
||||
Ok(format!(
|
||||
"{shebang}\n{print_args} | {askpass_program} --askpass={askpass_socket} 2> /dev/null \n",
|
||||
askpass_socket = askpass_socket.display(),
|
||||
print_args = "printf '%s\\0' \"$@\"",
|
||||
shebang = "#!/bin/sh",
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn generate_askpass_script(askpass_program: &str, askpass_socket: &std::path::Path) -> String {
|
||||
format!(
|
||||
fn generate_askpass_script(
|
||||
shell_kind: ShellKind,
|
||||
askpass_program: &std::path::Path,
|
||||
askpass_socket: &std::path::Path,
|
||||
) -> Result<String> {
|
||||
let askpass_program = shell_kind.prepend_command_prefix(
|
||||
askpass_program
|
||||
.to_str()
|
||||
.context("Askpass program is on a non-utf8 path")?,
|
||||
);
|
||||
let askpass_program = shell_kind
|
||||
.try_quote_prefix_aware(&askpass_program)
|
||||
.context("Failed to shell-escape Askpass program path")?;
|
||||
let askpass_socket = askpass_socket
|
||||
.try_shell_safe(shell_kind)
|
||||
.context("Failed to shell-escape Askpass socket path")?;
|
||||
Ok(format!(
|
||||
r#"
|
||||
$ErrorActionPreference = 'Stop';
|
||||
($args -join [char]0) | & {askpass_program} --askpass={askpass_socket} 2> $null
|
||||
"#,
|
||||
askpass_socket = askpass_socket.display(),
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ use paths::remote_servers_dir;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{RegisterSetting, Settings, SettingsStore};
|
||||
use smol::fs::File;
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
use smol::{fs::File, process::Command};
|
||||
use std::mem;
|
||||
use std::{
|
||||
env::{
|
||||
@@ -23,6 +23,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::command::new_smol_command;
|
||||
use workspace::Workspace;
|
||||
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
@@ -121,7 +122,7 @@ impl Drop for MacOsUnmounter<'_> {
|
||||
let mount_path = mem::take(&mut self.mount_path);
|
||||
self.background_executor
|
||||
.spawn(async move {
|
||||
let unmount_output = Command::new("hdiutil")
|
||||
let unmount_output = new_smol_command("hdiutil")
|
||||
.args(["detach", "-force"])
|
||||
.arg(&mount_path)
|
||||
.output()
|
||||
@@ -799,7 +800,7 @@ async fn install_release_linux(
|
||||
.await
|
||||
.context("failed to create directory into which to extract update")?;
|
||||
|
||||
let output = Command::new("tar")
|
||||
let output = new_smol_command("tar")
|
||||
.arg("-xzf")
|
||||
.arg(&downloaded_tar_gz)
|
||||
.arg("-C")
|
||||
@@ -834,7 +835,7 @@ async fn install_release_linux(
|
||||
to = PathBuf::from(prefix);
|
||||
}
|
||||
|
||||
let output = Command::new("rsync")
|
||||
let output = new_smol_command("rsync")
|
||||
.args(["-av", "--delete"])
|
||||
.arg(&from)
|
||||
.arg(&to)
|
||||
@@ -866,7 +867,7 @@ async fn install_release_macos(
|
||||
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
|
||||
|
||||
mounted_app_path.push("/");
|
||||
let output = Command::new("hdiutil")
|
||||
let output = new_smol_command("hdiutil")
|
||||
.args(["attach", "-nobrowse"])
|
||||
.arg(&downloaded_dmg)
|
||||
.arg("-mountroot")
|
||||
@@ -886,7 +887,7 @@ async fn install_release_macos(
|
||||
background_executor: cx.background_executor(),
|
||||
};
|
||||
|
||||
let output = Command::new("rsync")
|
||||
let output = new_smol_command("rsync")
|
||||
.args(["-av", "--delete"])
|
||||
.arg(&mounted_app_path)
|
||||
.arg(&running_app_path)
|
||||
@@ -917,7 +918,7 @@ async fn cleanup_windows() -> Result<()> {
|
||||
}
|
||||
|
||||
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
|
||||
let output = Command::new(downloaded_installer)
|
||||
let output = new_smol_command(downloaded_installer)
|
||||
.arg("/verysilent")
|
||||
.arg("/update=true")
|
||||
.arg("!desktopicon")
|
||||
|
||||
@@ -1683,7 +1683,9 @@ impl LiveKitRoom {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum LocalTrack<Stream: ?Sized> {
|
||||
#[default]
|
||||
None,
|
||||
Pending {
|
||||
publish_id: usize,
|
||||
@@ -1694,12 +1696,6 @@ enum LocalTrack<Stream: ?Sized> {
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Default for LocalTrack<T> {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum RoomStatus {
|
||||
Online,
|
||||
|
||||
@@ -293,10 +293,11 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn metrics_enabled(self: &Arc<Self>) -> bool {
|
||||
let state = self.state.lock();
|
||||
let enabled = state.settings.metrics;
|
||||
drop(state);
|
||||
enabled
|
||||
self.state.lock().settings.metrics
|
||||
}
|
||||
|
||||
pub fn diagnostics_enabled(self: &Arc<Self>) -> bool {
|
||||
self.state.lock().settings.diagnostics
|
||||
}
|
||||
|
||||
pub fn set_authenticated_user_info(
|
||||
|
||||
@@ -267,6 +267,7 @@ impl UserStore {
|
||||
Status::SignedOut => {
|
||||
current_user_tx.send(None).await.ok();
|
||||
this.update(cx, |this, cx| {
|
||||
this.clear_plan_and_usage();
|
||||
cx.emit(Event::PrivateUserInfoUpdated);
|
||||
cx.notify();
|
||||
this.clear_contacts()
|
||||
@@ -779,6 +780,12 @@ impl UserStore {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear_plan_and_usage(&mut self) {
|
||||
self.plan_info = None;
|
||||
self.model_request_usage = None;
|
||||
self.edit_prediction_usage = None;
|
||||
}
|
||||
|
||||
fn update_authenticated_user(
|
||||
&mut self,
|
||||
response: GetAuthenticatedUserResponse,
|
||||
|
||||
@@ -58,6 +58,9 @@ pub const SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str =
|
||||
/// The name of the header used by the client to indicate that it supports receiving xAI models.
|
||||
pub const CLIENT_SUPPORTS_X_AI_HEADER_NAME: &str = "x-zed-client-supports-x-ai";
|
||||
|
||||
/// The maximum number of edit predictions that can be rejected per request.
|
||||
pub const MAX_EDIT_PREDICTION_REJECTIONS_PER_REQUEST: usize = 100;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UsageLimit {
|
||||
@@ -192,6 +195,17 @@ pub struct AcceptEditPredictionBody {
|
||||
pub request_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RejectEditPredictionsBody {
|
||||
pub rejections: Vec<EditPredictionRejection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EditPredictionRejection {
|
||||
pub request_id: String,
|
||||
pub was_shown: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionMode {
|
||||
|
||||
@@ -76,6 +76,10 @@ pub enum PromptFormat {
|
||||
OldTextNewText,
|
||||
/// Prompt format intended for use via zeta_cli
|
||||
OnlySnippets,
|
||||
/// One-sentence instructions used in fine-tuned models
|
||||
Minimal,
|
||||
/// One-sentence instructions + FIM-like template
|
||||
MinimalQwen,
|
||||
}
|
||||
|
||||
impl PromptFormat {
|
||||
@@ -102,6 +106,8 @@ impl std::fmt::Display for PromptFormat {
|
||||
PromptFormat::OnlySnippets => write!(f, "Only Snippets"),
|
||||
PromptFormat::NumLinesUniDiff => write!(f, "Numbered Lines / Unified Diff"),
|
||||
PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
|
||||
PromptFormat::Minimal => write!(f, "Minimal"),
|
||||
PromptFormat::MinimalQwen => write!(f, "Minimal + Qwen FIM"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ pub mod retrieval_prompt;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use cloud_llm_client::predict_edits_v3::{
|
||||
self, DiffPathFmt, Excerpt, Line, Point, PromptFormat, ReferencedDeclaration,
|
||||
self, DiffPathFmt, Event, Excerpt, IncludedFile, Line, Point, PromptFormat,
|
||||
ReferencedDeclaration,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use ordered_float::OrderedFloat;
|
||||
@@ -31,7 +32,7 @@ const MARKED_EXCERPT_INSTRUCTIONS: &str = indoc! {"
|
||||
|
||||
Other code is provided for context, and `…` indicates when code has been skipped.
|
||||
|
||||
# Edit History:
|
||||
## Edit History
|
||||
|
||||
"};
|
||||
|
||||
@@ -49,7 +50,7 @@ const LABELED_SECTIONS_INSTRUCTIONS: &str = indoc! {r#"
|
||||
println!("{i}");
|
||||
}
|
||||
|
||||
# Edit History:
|
||||
## Edit History
|
||||
|
||||
"#};
|
||||
|
||||
@@ -86,6 +87,13 @@ const NUMBERED_LINES_INSTRUCTIONS: &str = indoc! {r#"
|
||||
|
||||
"#};
|
||||
|
||||
const STUDENT_MODEL_INSTRUCTIONS: &str = indoc! {r#"
|
||||
You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.
|
||||
|
||||
## Edit History
|
||||
|
||||
"#};
|
||||
|
||||
const UNIFIED_DIFF_REMINDER: &str = indoc! {"
|
||||
---
|
||||
|
||||
@@ -100,18 +108,27 @@ const UNIFIED_DIFF_REMINDER: &str = indoc! {"
|
||||
to uniquely identify it amongst all excerpts of code provided.
|
||||
"};
|
||||
|
||||
const MINIMAL_PROMPT_REMINDER: &str = indoc! {"
|
||||
---
|
||||
|
||||
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.
|
||||
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
|
||||
Analyze the history of edits made by the user in order to infer what they are currently trying to accomplish.
|
||||
Then complete the remainder of the current change if it is incomplete, or predict the next edit the user intends to make.
|
||||
Always continue along the user's current trajectory, rather than changing course.
|
||||
|
||||
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:
|
||||
## Output Format
|
||||
|
||||
You should briefly explain your understanding of the user's overall goal in one sentence, then explain what the next change
|
||||
along the users current trajectory will be in another, and finally specify the next edit using the following XML-like format:
|
||||
|
||||
<edits path="my-project/src/myapp/cli.py">
|
||||
<old_text>
|
||||
@@ -137,20 +154,34 @@ const XML_TAGS_INSTRUCTIONS: &str = indoc! {r#"
|
||||
- Always close all tags properly
|
||||
- Don't include the <|user_cursor|> marker in your output.
|
||||
|
||||
# Edit History:
|
||||
## 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.
|
||||
Remember that the edits in the edit history have already been applied.
|
||||
"#};
|
||||
|
||||
pub fn build_prompt(
|
||||
request: &predict_edits_v3::PredictEditsRequest,
|
||||
) -> Result<(String, SectionLabels)> {
|
||||
let mut section_labels = Default::default();
|
||||
|
||||
match request.prompt_format {
|
||||
PromptFormat::MinimalQwen => {
|
||||
let prompt = MinimalQwenPrompt {
|
||||
events: request.events.clone(),
|
||||
cursor_point: request.cursor_point,
|
||||
cursor_path: request.excerpt_path.clone(),
|
||||
included_files: request.included_files.clone(),
|
||||
};
|
||||
return Ok((prompt.render(), section_labels));
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
let mut insertions = match request.prompt_format {
|
||||
PromptFormat::MarkedExcerpt => vec![
|
||||
(
|
||||
@@ -171,10 +202,12 @@ pub fn build_prompt(
|
||||
],
|
||||
PromptFormat::LabeledSections
|
||||
| PromptFormat::NumLinesUniDiff
|
||||
| PromptFormat::Minimal
|
||||
| PromptFormat::OldTextNewText => {
|
||||
vec![(request.cursor_point, CURSOR_MARKER)]
|
||||
}
|
||||
PromptFormat::OnlySnippets => vec![],
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
};
|
||||
|
||||
let mut prompt = match request.prompt_format {
|
||||
@@ -183,32 +216,59 @@ pub fn build_prompt(
|
||||
PromptFormat::NumLinesUniDiff => NUMBERED_LINES_INSTRUCTIONS.to_string(),
|
||||
PromptFormat::OldTextNewText => XML_TAGS_INSTRUCTIONS.to_string(),
|
||||
PromptFormat::OnlySnippets => String::new(),
|
||||
PromptFormat::Minimal => STUDENT_MODEL_INSTRUCTIONS.to_string(),
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
};
|
||||
|
||||
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");
|
||||
let edit_preamble = if request.prompt_format == PromptFormat::Minimal {
|
||||
"The following are the latest edits made by the user, from earlier to later.\n\n"
|
||||
} else {
|
||||
"Here are the latest edits made by the user, from earlier to later.\n\n"
|
||||
};
|
||||
prompt.push_str(edit_preamble);
|
||||
push_events(&mut prompt, &request.events);
|
||||
}
|
||||
|
||||
prompt.push_str(indoc! {"
|
||||
# Code Excerpts
|
||||
let excerpts_preamble = match request.prompt_format {
|
||||
PromptFormat::Minimal => indoc! {"
|
||||
## Part of the file under the cursor
|
||||
|
||||
The cursor marker <|user_cursor|> indicates the current user cursor position.
|
||||
The file is in current state, edits from edit history have been applied.
|
||||
"});
|
||||
(The cursor marker <|user_cursor|> indicates the current user cursor position.
|
||||
The file is in current state, edits from edit history has been applied.
|
||||
We only show part of the file around the cursor.
|
||||
You can only edit exactly this part of the file.
|
||||
We prepend line numbers (e.g., `123|<actual line>`); they are not part of the file.)
|
||||
"},
|
||||
PromptFormat::NumLinesUniDiff | PromptFormat::OldTextNewText => indoc! {"
|
||||
## Code Excerpts
|
||||
|
||||
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.
|
||||
"});
|
||||
}
|
||||
Here is some excerpts of code that you should take into account to predict the next edit.
|
||||
|
||||
The cursor position is marked by `<|user_cursor|>` as it stands after the last edit in the history.
|
||||
|
||||
In addition other excerpts are included to better understand what the edit will be, including the declaration
|
||||
or references of symbols around the cursor, or other similar code snippets that may need to be updated
|
||||
following patterns that appear in the edit history.
|
||||
|
||||
Consider each of them carefully in relation to the edit history, and that the user may not have navigated
|
||||
to the next place they want to edit yet.
|
||||
|
||||
Lines starting with `…` indicate omitted line ranges. These may appear inside multi-line code constructs.
|
||||
"},
|
||||
_ => 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.
|
||||
"},
|
||||
};
|
||||
|
||||
prompt.push_str(excerpts_preamble);
|
||||
prompt.push('\n');
|
||||
|
||||
let mut section_labels = Default::default();
|
||||
|
||||
if !request.referenced_declarations.is_empty() || !request.signatures.is_empty() {
|
||||
let syntax_based_prompt = SyntaxBasedPrompt::populate(request)?;
|
||||
section_labels = syntax_based_prompt.write(&mut insertions, &mut prompt)?;
|
||||
@@ -217,19 +277,38 @@ pub fn build_prompt(
|
||||
anyhow::bail!("PromptFormat::LabeledSections cannot be used with ContextMode::Llm");
|
||||
}
|
||||
|
||||
let include_line_numbers = matches!(
|
||||
request.prompt_format,
|
||||
PromptFormat::NumLinesUniDiff | PromptFormat::Minimal
|
||||
);
|
||||
for related_file in &request.included_files {
|
||||
write_codeblock(
|
||||
&related_file.path,
|
||||
&related_file.excerpts,
|
||||
if related_file.path == request.excerpt_path {
|
||||
&insertions
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
related_file.max_row,
|
||||
request.prompt_format == PromptFormat::NumLinesUniDiff,
|
||||
&mut prompt,
|
||||
);
|
||||
if request.prompt_format == PromptFormat::Minimal {
|
||||
write_codeblock_with_filename(
|
||||
&related_file.path,
|
||||
&related_file.excerpts,
|
||||
if related_file.path == request.excerpt_path {
|
||||
&insertions
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut prompt,
|
||||
);
|
||||
} else {
|
||||
write_codeblock(
|
||||
&related_file.path,
|
||||
&related_file.excerpts,
|
||||
if related_file.path == request.excerpt_path {
|
||||
&insertions
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut prompt,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,6 +319,9 @@ pub fn build_prompt(
|
||||
PromptFormat::OldTextNewText => {
|
||||
prompt.push_str(OLD_TEXT_NEW_TEXT_REMINDER);
|
||||
}
|
||||
PromptFormat::Minimal => {
|
||||
prompt.push_str(MINIMAL_PROMPT_REMINDER);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -255,6 +337,27 @@ pub fn write_codeblock<'a>(
|
||||
output: &'a mut String,
|
||||
) {
|
||||
writeln!(output, "`````{}", DiffPathFmt(path)).unwrap();
|
||||
|
||||
write_excerpts(
|
||||
excerpts,
|
||||
sorted_insertions,
|
||||
file_line_count,
|
||||
include_line_numbers,
|
||||
output,
|
||||
);
|
||||
write!(output, "`````\n\n").unwrap();
|
||||
}
|
||||
|
||||
fn write_codeblock_with_filename<'a>(
|
||||
path: &Path,
|
||||
excerpts: impl IntoIterator<Item = &'a Excerpt>,
|
||||
sorted_insertions: &[(Point, &str)],
|
||||
file_line_count: Line,
|
||||
include_line_numbers: bool,
|
||||
output: &'a mut String,
|
||||
) {
|
||||
writeln!(output, "`````filename={}", DiffPathFmt(path)).unwrap();
|
||||
|
||||
write_excerpts(
|
||||
excerpts,
|
||||
sorted_insertions,
|
||||
@@ -666,6 +769,7 @@ impl<'a> SyntaxBasedPrompt<'a> {
|
||||
PromptFormat::MarkedExcerpt
|
||||
| PromptFormat::OnlySnippets
|
||||
| PromptFormat::OldTextNewText
|
||||
| PromptFormat::Minimal
|
||||
| PromptFormat::NumLinesUniDiff => {
|
||||
if range.start.0 > 0 && !skipped_last_snippet {
|
||||
output.push_str("…\n");
|
||||
@@ -681,6 +785,7 @@ impl<'a> SyntaxBasedPrompt<'a> {
|
||||
writeln!(output, "<|section_{}|>", section_index).ok();
|
||||
}
|
||||
}
|
||||
PromptFormat::MinimalQwen => unreachable!(),
|
||||
}
|
||||
|
||||
let push_full_snippet = |output: &mut String| {
|
||||
@@ -790,3 +895,69 @@ fn declaration_size(declaration: &ReferencedDeclaration, style: DeclarationStyle
|
||||
DeclarationStyle::Declaration => declaration.text.len(),
|
||||
}
|
||||
}
|
||||
|
||||
struct MinimalQwenPrompt {
|
||||
events: Vec<Event>,
|
||||
cursor_point: Point,
|
||||
cursor_path: Arc<Path>, // TODO: make a common struct with cursor_point
|
||||
included_files: Vec<IncludedFile>,
|
||||
}
|
||||
|
||||
impl MinimalQwenPrompt {
|
||||
const INSTRUCTIONS: &str = "You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.\n";
|
||||
|
||||
fn render(&self) -> String {
|
||||
let edit_history = self.fmt_edit_history();
|
||||
let context = self.fmt_context();
|
||||
|
||||
format!(
|
||||
"{instructions}\n\n{edit_history}\n\n{context}",
|
||||
instructions = MinimalQwenPrompt::INSTRUCTIONS,
|
||||
edit_history = edit_history,
|
||||
context = context
|
||||
)
|
||||
}
|
||||
|
||||
fn fmt_edit_history(&self) -> String {
|
||||
if self.events.is_empty() {
|
||||
"(No edit history)\n\n".to_string()
|
||||
} else {
|
||||
let mut events_str = String::new();
|
||||
push_events(&mut events_str, &self.events);
|
||||
format!(
|
||||
"The following are the latest edits made by the user, from earlier to later.\n\n{}",
|
||||
events_str
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_context(&self) -> String {
|
||||
let mut context = String::new();
|
||||
let include_line_numbers = true;
|
||||
|
||||
for related_file in &self.included_files {
|
||||
writeln!(context, "<|file_sep|>{}", DiffPathFmt(&related_file.path)).unwrap();
|
||||
|
||||
if related_file.path == self.cursor_path {
|
||||
write!(context, "<|fim_prefix|>").unwrap();
|
||||
write_excerpts(
|
||||
&related_file.excerpts,
|
||||
&[(self.cursor_point, "<|fim_suffix|>")],
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut context,
|
||||
);
|
||||
writeln!(context, "<|fim_middle|>").unwrap();
|
||||
} else {
|
||||
write_excerpts(
|
||||
&related_file.excerpts,
|
||||
&[],
|
||||
related_file.max_row,
|
||||
include_line_numbers,
|
||||
&mut context,
|
||||
);
|
||||
}
|
||||
}
|
||||
context
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ pub fn build_prompt(request: predict_edits_v3::PlanContextRetrievalRequest) -> R
|
||||
let mut prompt = SEARCH_INSTRUCTIONS.to_string();
|
||||
|
||||
if !request.events.is_empty() {
|
||||
writeln!(&mut prompt, "## User Edits\n")?;
|
||||
writeln!(&mut prompt, "\n## User Edits\n\n")?;
|
||||
push_events(&mut prompt, &request.events);
|
||||
}
|
||||
|
||||
writeln!(&mut prompt, "## Cursor context")?;
|
||||
writeln!(&mut prompt, "## Cursor context\n")?;
|
||||
write_codeblock(
|
||||
&request.excerpt_path,
|
||||
&[Excerpt {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
alter table billing_customers
|
||||
add column external_id text;
|
||||
|
||||
create unique index uix_billing_customers_on_external_id on billing_customers (external_id);
|
||||
@@ -1005,6 +1005,7 @@ impl Database {
|
||||
is_last_update: true,
|
||||
merge_message: db_repository_entry.merge_message,
|
||||
stash_entries: Vec::new(),
|
||||
renamed_paths: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,6 +796,7 @@ impl Database {
|
||||
is_last_update: true,
|
||||
merge_message: db_repository.merge_message,
|
||||
stash_entries: Vec::new(),
|
||||
renamed_paths: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
#[gpui::test]
|
||||
async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
@@ -307,17 +307,35 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
..lsp::ServerCapabilities::default()
|
||||
};
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
||||
let mut fake_language_servers = [
|
||||
client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
),
|
||||
client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "fake-analyzer",
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
),
|
||||
];
|
||||
client_b.language_registry().add(rust_lang());
|
||||
client_b.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: capabilities.clone(),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
client_b.language_registry().add(rust_lang());
|
||||
client_b.language_registry().register_fake_lsp_adapter(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "fake-analyzer",
|
||||
capabilities,
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
@@ -352,7 +370,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)
|
||||
});
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
let fake_language_server = fake_language_servers[0].next().await.unwrap();
|
||||
let second_fake_language_server = fake_language_servers[1].next().await.unwrap();
|
||||
cx_a.background_executor.run_until_parked();
|
||||
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
@@ -414,6 +433,11 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
second_fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) })
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.executor().finish_waiting();
|
||||
|
||||
// Open the buffer on the host.
|
||||
@@ -522,6 +546,10 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
])))
|
||||
});
|
||||
|
||||
// Second language server also needs to handle the request (returns None)
|
||||
let mut second_completion_response = second_fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
|
||||
|
||||
// The completion now gets a new `text_edit.new_text` when resolving the completion item
|
||||
let mut resolve_completion_response = fake_language_server
|
||||
.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {
|
||||
@@ -545,6 +573,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
completion_response.next().await.unwrap();
|
||||
second_completion_response.next().await.unwrap();
|
||||
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
@@ -563,6 +592,77 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
|
||||
);
|
||||
});
|
||||
|
||||
// Ensure buffer is synced before proceeding with the next test
|
||||
cx_a.executor().run_until_parked();
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// Test completions from the second fake language server
|
||||
// Add another completion trigger to test the second language server
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([68..68])
|
||||
});
|
||||
editor.handle_input("; b", window, cx);
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.text(),
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b. }"
|
||||
);
|
||||
});
|
||||
|
||||
// Set up completion handlers for both language servers
|
||||
let mut first_lsp_completion = fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
|
||||
|
||||
let mut second_lsp_completion = second_fake_language_server
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
lsp::Position::new(1, 54),
|
||||
);
|
||||
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "analyzer_method(…)".into(),
|
||||
detail: Some("fn(&self) -> Result<T>".into()),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
new_text: "analyzer_method()".to_string(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(1, 54),
|
||||
lsp::Position::new(1, 54),
|
||||
),
|
||||
})),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// Await both language server responses
|
||||
first_lsp_completion.next().await.unwrap();
|
||||
second_lsp_completion.next().await.unwrap();
|
||||
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
// Confirm the completion from the second language server works
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
assert!(editor.context_menu_visible());
|
||||
editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b.analyzer_method() }"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -2169,16 +2269,28 @@ 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,
|
||||
}]))
|
||||
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,
|
||||
},
|
||||
]))
|
||||
}
|
||||
})
|
||||
.next()
|
||||
|
||||
@@ -672,20 +672,25 @@ impl CollabPanel {
|
||||
{
|
||||
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
|
||||
}
|
||||
|
||||
let should_respect_collapse = query.is_empty();
|
||||
let mut collapse_depth = None;
|
||||
|
||||
for (idx, channel) in channels.into_iter().enumerate() {
|
||||
let depth = channel.parent_path.len();
|
||||
|
||||
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
|
||||
collapse_depth = Some(depth);
|
||||
} else if let Some(collapsed_depth) = collapse_depth {
|
||||
if depth > collapsed_depth {
|
||||
continue;
|
||||
}
|
||||
if self.is_channel_collapsed(channel.id) {
|
||||
if should_respect_collapse {
|
||||
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
|
||||
collapse_depth = Some(depth);
|
||||
} else {
|
||||
collapse_depth = None;
|
||||
} else if let Some(collapsed_depth) = collapse_depth {
|
||||
if depth > collapsed_depth {
|
||||
continue;
|
||||
}
|
||||
if self.is_channel_collapsed(channel.id) {
|
||||
collapse_depth = Some(depth);
|
||||
} else {
|
||||
collapse_depth = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1029,13 +1029,11 @@ 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, collapse, window, cx)
|
||||
})
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.activate_match(index, matches, window, cx))
|
||||
}
|
||||
|
||||
fn select_matches(
|
||||
|
||||
@@ -491,7 +491,7 @@ impl ProjectDiagnosticsEditor {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let was_empty = self.multibuffer.read(cx).is_empty();
|
||||
let mut buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_id = buffer_snapshot.remote_id();
|
||||
|
||||
let max_severity = if self.include_warnings {
|
||||
@@ -602,7 +602,6 @@ 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 {
|
||||
@@ -1010,11 +1009,14 @@ 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() {
|
||||
|
||||
@@ -104,6 +104,7 @@ pub trait EditPredictionProvider: 'static + Sized {
|
||||
);
|
||||
fn accept(&mut self, cx: &mut Context<Self>);
|
||||
fn discard(&mut self, cx: &mut Context<Self>);
|
||||
fn did_show(&mut self, _cx: &mut Context<Self>) {}
|
||||
fn suggest(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
@@ -142,6 +143,7 @@ pub trait EditPredictionProviderHandle {
|
||||
direction: Direction,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn did_show(&self, cx: &mut App);
|
||||
fn accept(&self, cx: &mut App);
|
||||
fn discard(&self, cx: &mut App);
|
||||
fn suggest(
|
||||
@@ -233,6 +235,10 @@ where
|
||||
self.update(cx, |this, cx| this.discard(cx))
|
||||
}
|
||||
|
||||
fn did_show(&self, cx: &mut App) {
|
||||
self.update(cx, |this, cx| this.did_show(cx))
|
||||
}
|
||||
|
||||
fn suggest(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
|
||||
@@ -18,18 +18,19 @@ client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
codestral.workspace = true
|
||||
copilot.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
language.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
settings.workspace = true
|
||||
supermaven.workspace = true
|
||||
sweep_ai.workspace = true
|
||||
telemetry.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -18,12 +18,15 @@ use language::{
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use regex::Regex;
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use settings::{
|
||||
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME, Settings, SettingsStore, update_settings_file,
|
||||
};
|
||||
use std::{
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
use supermaven::{AccountStatus, Supermaven};
|
||||
use sweep_ai::SweepFeatureFlag;
|
||||
use ui::{
|
||||
Clickable, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton,
|
||||
IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
|
||||
@@ -78,7 +81,7 @@ impl Render for EditPredictionButton {
|
||||
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
|
||||
match all_language_settings.edit_predictions.provider {
|
||||
match &all_language_settings.edit_predictions.provider {
|
||||
EditPredictionProvider::None => div().hidden(),
|
||||
|
||||
EditPredictionProvider::Copilot => {
|
||||
@@ -297,6 +300,15 @@ impl Render for EditPredictionButton {
|
||||
.with_handle(self.popover_menu_handle.clone()),
|
||||
)
|
||||
}
|
||||
EditPredictionProvider::Experimental(provider_name) => {
|
||||
if *provider_name == EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME
|
||||
&& cx.has_flag::<SweepFeatureFlag>()
|
||||
{
|
||||
div().child(Icon::new(IconName::SweepAi))
|
||||
} else {
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
||||
EditPredictionProvider::Zed => {
|
||||
let enabled = self.editor_enabled.unwrap_or(true);
|
||||
@@ -525,7 +537,7 @@ impl EditPredictionButton {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
})
|
||||
}
|
||||
EditPredictionProvider::None => continue,
|
||||
EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => continue,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,10 +248,8 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
// Determine split index handling edge cases
|
||||
let split_index = if desired_bytes >= chunk.text.len() {
|
||||
chunk.text.len()
|
||||
} else if chunk.text.is_char_boundary(desired_bytes) {
|
||||
desired_bytes
|
||||
} else {
|
||||
find_next_utf8_boundary(chunk.text, desired_bytes)
|
||||
chunk.text.ceil_char_boundary(desired_bytes)
|
||||
};
|
||||
|
||||
let (prefix, suffix) = chunk.text.split_at(split_index);
|
||||
@@ -373,10 +371,8 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
.next()
|
||||
.map(|c| c.len_utf8())
|
||||
.unwrap_or(1)
|
||||
} else if inlay_chunk.is_char_boundary(next_inlay_highlight_endpoint) {
|
||||
next_inlay_highlight_endpoint
|
||||
} else {
|
||||
find_next_utf8_boundary(inlay_chunk, next_inlay_highlight_endpoint)
|
||||
inlay_chunk.ceil_char_boundary(next_inlay_highlight_endpoint)
|
||||
};
|
||||
|
||||
let (chunk, remainder) = inlay_chunk.split_at(split_index);
|
||||
@@ -1146,31 +1142,6 @@ fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a byte index that is NOT a UTF-8 boundary, find the next one.
|
||||
/// Assumes: 0 < byte_index < text.len() and !text.is_char_boundary(byte_index)
|
||||
#[inline(always)]
|
||||
fn find_next_utf8_boundary(text: &str, byte_index: usize) -> usize {
|
||||
let bytes = text.as_bytes();
|
||||
let mut idx = byte_index + 1;
|
||||
|
||||
// Scan forward until we find a boundary
|
||||
while idx < text.len() {
|
||||
if is_utf8_char_boundary(bytes[idx]) {
|
||||
return idx;
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
// Hit the end, return the full length
|
||||
text.len()
|
||||
}
|
||||
|
||||
// Private helper function taken from Rust's core::num module (which is both Apache2 and MIT licensed)
|
||||
const fn is_utf8_char_boundary(byte: u8) -> bool {
|
||||
// This is bit magic equivalent to: b < 128 || b >= 192
|
||||
(byte as i8) >= -0x40
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1099,6 +1099,7 @@ pub struct Editor {
|
||||
searchable: bool,
|
||||
cursor_shape: CursorShape,
|
||||
current_line_highlight: Option<CurrentLineHighlight>,
|
||||
collapse_matches: bool,
|
||||
autoindent_mode: Option<AutoindentMode>,
|
||||
workspace: Option<(WeakEntity<Workspace>, Option<WorkspaceId>)>,
|
||||
input_enabled: bool,
|
||||
@@ -1300,8 +1301,9 @@ struct SelectionHistoryEntry {
|
||||
add_selections_state: Option<AddSelectionsState>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
|
||||
enum SelectionHistoryMode {
|
||||
#[default]
|
||||
Normal,
|
||||
Undoing,
|
||||
Redoing,
|
||||
@@ -1314,12 +1316,6 @@ struct HoveredCursor {
|
||||
selection_id: usize,
|
||||
}
|
||||
|
||||
impl Default for SelectionHistoryMode {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// SelectionEffects controls the side-effects of updating the selection.
|
||||
///
|
||||
@@ -2216,7 +2212,7 @@ impl Editor {
|
||||
.unwrap_or_default(),
|
||||
current_line_highlight: None,
|
||||
autoindent_mode: Some(AutoindentMode::EachLine),
|
||||
|
||||
collapse_matches: false,
|
||||
workspace: None,
|
||||
input_enabled: !is_minimap,
|
||||
use_modal_editing: full_mode,
|
||||
@@ -2389,7 +2385,10 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
EditorEvent::Edited { .. } => {
|
||||
if vim_flavor(cx).is_none() {
|
||||
let vim_mode = vim_mode_setting::VimModeSetting::try_get(cx)
|
||||
.map(|vim_mode| vim_mode.0)
|
||||
.unwrap_or(false);
|
||||
if !vim_mode {
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let selections = editor.selections.all_adjusted_display(&display_map);
|
||||
let pop_state = editor
|
||||
@@ -2636,6 +2635,10 @@ impl Editor {
|
||||
key_context.add("end_of_input");
|
||||
}
|
||||
|
||||
if self.has_any_expanded_diff_hunks(cx) {
|
||||
key_context.add("diffs_expanded");
|
||||
}
|
||||
|
||||
key_context
|
||||
}
|
||||
|
||||
@@ -3014,17 +3017,21 @@ impl Editor {
|
||||
self.current_line_highlight = current_line_highlight;
|
||||
}
|
||||
|
||||
pub fn range_for_match<T: std::marker::Copy>(
|
||||
&self,
|
||||
range: &Range<T>,
|
||||
collapse: bool,
|
||||
) -> Range<T> {
|
||||
if collapse {
|
||||
pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
|
||||
self.collapse_matches = collapse_matches;
|
||||
}
|
||||
|
||||
pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
|
||||
if self.collapse_matches {
|
||||
return range.start..range.start;
|
||||
}
|
||||
range.clone()
|
||||
}
|
||||
|
||||
pub fn clip_at_line_ends(&mut self, cx: &mut Context<Self>) -> bool {
|
||||
self.display_map.read(cx).clip_at_line_ends
|
||||
}
|
||||
|
||||
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut Context<Self>) {
|
||||
if self.display_map.read(cx).clip_at_line_ends != clip {
|
||||
self.display_map
|
||||
@@ -3284,8 +3291,8 @@ impl Editor {
|
||||
self.refresh_document_highlights(cx);
|
||||
refresh_linked_ranges(self, window, cx);
|
||||
|
||||
self.refresh_selected_text_highlights(false, window, cx);
|
||||
self.refresh_matching_bracket_highlights(window, cx);
|
||||
// self.refresh_selected_text_highlights(false, window, cx);
|
||||
// self.refresh_matching_bracket_highlights(window, cx);
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
self.edit_prediction_requires_modifier_in_indent_conflict = true;
|
||||
self.inline_blame_popover.take();
|
||||
@@ -7870,6 +7877,10 @@ impl Editor {
|
||||
self.edit_prediction_preview,
|
||||
EditPredictionPreview::Inactive { .. }
|
||||
) {
|
||||
if let Some(provider) = self.edit_prediction_provider.as_ref() {
|
||||
provider.provider.did_show(cx)
|
||||
}
|
||||
|
||||
self.edit_prediction_preview = EditPredictionPreview::Active {
|
||||
previous_scroll_position: None,
|
||||
since: Instant::now(),
|
||||
@@ -8049,6 +8060,9 @@ impl Editor {
|
||||
&& !self.edit_predictions_hidden_for_vim_mode;
|
||||
|
||||
if show_completions_in_buffer {
|
||||
if let Some(provider) = &self.edit_prediction_provider {
|
||||
provider.provider.did_show(cx);
|
||||
}
|
||||
if edits
|
||||
.iter()
|
||||
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
|
||||
@@ -12583,6 +12597,7 @@ impl Editor {
|
||||
{
|
||||
let max_point = buffer.max_point();
|
||||
let mut is_first = true;
|
||||
let mut prev_selection_was_entire_line = false;
|
||||
for selection in &mut selections {
|
||||
let is_entire_line =
|
||||
(selection.is_empty() && cut_no_selection_line) || self.selections.line_mode();
|
||||
@@ -12597,9 +12612,10 @@ impl Editor {
|
||||
}
|
||||
if is_first {
|
||||
is_first = false;
|
||||
} else {
|
||||
} else if !prev_selection_was_entire_line {
|
||||
text += "\n";
|
||||
}
|
||||
prev_selection_was_entire_line = is_entire_line;
|
||||
let mut len = 0;
|
||||
for chunk in buffer.text_for_range(selection.start..selection.end) {
|
||||
text.push_str(chunk);
|
||||
@@ -12682,6 +12698,7 @@ impl Editor {
|
||||
{
|
||||
let max_point = buffer.max_point();
|
||||
let mut is_first = true;
|
||||
let mut prev_selection_was_entire_line = false;
|
||||
for selection in &selections {
|
||||
let mut start = selection.start;
|
||||
let mut end = selection.end;
|
||||
@@ -12740,9 +12757,10 @@ impl Editor {
|
||||
for trimmed_range in trimmed_selections {
|
||||
if is_first {
|
||||
is_first = false;
|
||||
} else {
|
||||
} else if !prev_selection_was_entire_line {
|
||||
text += "\n";
|
||||
}
|
||||
prev_selection_was_entire_line = is_entire_line;
|
||||
let mut len = 0;
|
||||
for chunk in buffer.text_for_range(trimmed_range.start..trimmed_range.end) {
|
||||
text.push_str(chunk);
|
||||
@@ -12816,7 +12834,11 @@ impl Editor {
|
||||
let end_offset = start_offset + clipboard_selection.len;
|
||||
to_insert = &clipboard_text[start_offset..end_offset];
|
||||
entire_line = clipboard_selection.is_entire_line;
|
||||
start_offset = end_offset + 1;
|
||||
start_offset = if entire_line {
|
||||
end_offset
|
||||
} else {
|
||||
end_offset + 1
|
||||
};
|
||||
original_indent_column = Some(clipboard_selection.first_line_indent);
|
||||
} else {
|
||||
to_insert = &*clipboard_text;
|
||||
@@ -16907,7 +16929,7 @@ impl Editor {
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
let range = target_range.to_point(target_buffer.read(cx));
|
||||
let range = editor.range_for_match(&range, false);
|
||||
let range = editor.range_for_match(&range);
|
||||
let range = collapse_multiline_range(range);
|
||||
|
||||
if !split
|
||||
@@ -19326,6 +19348,16 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn has_any_expanded_diff_hunks(&self, cx: &App) -> bool {
|
||||
if self.buffer.read(cx).all_diff_hunks_expanded() {
|
||||
return true;
|
||||
}
|
||||
let ranges = vec![Anchor::min()..Anchor::max()];
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.has_expanded_diff_hunks_in_ranges(&ranges, cx)
|
||||
}
|
||||
|
||||
fn toggle_diff_hunks_in_ranges(
|
||||
&mut self,
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
@@ -21216,9 +21248,9 @@ impl Editor {
|
||||
self.active_indent_guides_state.dirty = true;
|
||||
self.refresh_active_diagnostics(cx);
|
||||
self.refresh_code_actions(window, cx);
|
||||
self.refresh_selected_text_highlights(true, window, cx);
|
||||
// self.refresh_selected_text_highlights(true, window, cx);
|
||||
self.refresh_single_line_folds(window, cx);
|
||||
self.refresh_matching_bracket_highlights(window, cx);
|
||||
// self.refresh_matching_bracket_highlights(window, cx);
|
||||
if self.has_active_edit_prediction() {
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
}
|
||||
@@ -21313,6 +21345,7 @@ impl Editor {
|
||||
}
|
||||
multi_buffer::Event::Reparsed(buffer_id) => {
|
||||
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
|
||||
// self.refresh_selected_text_highlights(true, window, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
|
||||
cx.emit(EditorEvent::Reparsed(*buffer_id));
|
||||
@@ -21737,7 +21770,9 @@ impl Editor {
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|a| a.to_string()));
|
||||
|
||||
let vim_mode = vim_flavor(cx).is_some();
|
||||
let vim_mode = vim_mode_setting::VimModeSetting::try_get(cx)
|
||||
.map(|vim_mode| vim_mode.0)
|
||||
.unwrap_or(false);
|
||||
|
||||
let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider;
|
||||
let copilot_enabled = edit_predictions_provider
|
||||
@@ -22372,28 +22407,6 @@ fn edit_for_markdown_paste<'a>(
|
||||
(range, new_text)
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum VimFlavor {
|
||||
Vim,
|
||||
Helix,
|
||||
}
|
||||
|
||||
pub fn vim_flavor(cx: &App) -> Option<VimFlavor> {
|
||||
if vim_mode_setting::HelixModeSetting::try_get(cx)
|
||||
.map(|helix_mode| helix_mode.0)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(VimFlavor::Helix)
|
||||
} else if vim_mode_setting::VimModeSetting::try_get(cx)
|
||||
.map(|vim_mode| vim_mode.0)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Some(VimFlavor::Vim)
|
||||
} else {
|
||||
None // neither vim nor helix mode
|
||||
}
|
||||
}
|
||||
|
||||
fn process_completion_for_edit(
|
||||
completion: &Completion,
|
||||
intent: CompletionIntent,
|
||||
@@ -23895,6 +23908,10 @@ impl EditorSnapshot {
|
||||
self.scroll_anchor.scroll_position(&self.display_snapshot)
|
||||
}
|
||||
|
||||
pub fn scroll_near_end(&self) -> bool {
|
||||
self.scroll_anchor.near_end(&self.display_snapshot)
|
||||
}
|
||||
|
||||
fn gutter_dimensions(
|
||||
&self,
|
||||
font_id: FontId,
|
||||
|
||||
@@ -32,6 +32,7 @@ use language::{
|
||||
tree_sitter_python,
|
||||
};
|
||||
use language_settings::Formatter;
|
||||
use languages::markdown_lang;
|
||||
use languages::rust_lang;
|
||||
use lsp::CompletionParams;
|
||||
use multi_buffer::{IndentGuide, PathKey};
|
||||
@@ -17503,6 +17504,89 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
|
||||
language_registry.add(markdown_lang());
|
||||
language_registry.add(rust_lang());
|
||||
let buffer = cx.new(|cx| {
|
||||
let mut buffer = language::Buffer::local(
|
||||
indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}
|
||||
}
|
||||
```
|
||||
"},
|
||||
cx,
|
||||
);
|
||||
buffer.set_language_registry(language_registry.clone());
|
||||
buffer.set_language(Some(markdown_lang()), cx);
|
||||
buffer
|
||||
});
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
|
||||
cx.executor().run_until_parked();
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Case 1: Test outer enclosing brackets
|
||||
select_ranges(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}
|
||||
}ˇ
|
||||
```
|
||||
"},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree ˇ{
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}
|
||||
}
|
||||
```
|
||||
"},
|
||||
cx,
|
||||
);
|
||||
// Case 2: Test inner enclosing brackets
|
||||
select_ranges(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
|
||||
}ˇ
|
||||
}
|
||||
```
|
||||
"},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
|
||||
assert_text_with_selections(
|
||||
editor,
|
||||
&indoc! {"
|
||||
```rs
|
||||
impl Worktree {
|
||||
pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
|
||||
}
|
||||
}
|
||||
```
|
||||
"},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -27375,6 +27459,60 @@ async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
|
||||
cx.assert_editor_state("line1\nline2\nˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.set_state("ˇline1\nˇline2\nˇline3\n");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
|
||||
|
||||
let clipboard_text = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.text().as_deref().map(str::to_string));
|
||||
|
||||
assert_eq!(
|
||||
clipboard_text,
|
||||
Some("line1\nline2\nline3\n".to_string()),
|
||||
"Copying multiple lines should include a single newline between lines"
|
||||
);
|
||||
|
||||
cx.set_state("lineA\nˇ");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
|
||||
|
||||
cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.set_state("ˇline1\nˇline2\nˇline3\n");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
|
||||
|
||||
let clipboard_text = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.text().as_deref().map(str::to_string));
|
||||
|
||||
assert_eq!(
|
||||
clipboard_text,
|
||||
Some("line1\nline2\nline3\n".to_string()),
|
||||
"Copying multiple lines should include a single newline between lines"
|
||||
);
|
||||
|
||||
cx.set_state("lineA\nˇ");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
|
||||
|
||||
cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_end_of_editor_context(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -3664,6 +3664,7 @@ impl EditorElement {
|
||||
row_block_types: &mut HashMap<DisplayRow, bool>,
|
||||
selections: &[Selection<Point>],
|
||||
selected_buffer_ids: &Vec<BufferId>,
|
||||
latest_selection_anchors: &HashMap<BufferId, Anchor>,
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
sticky_header_excerpt_id: Option<ExcerptId>,
|
||||
window: &mut Window,
|
||||
@@ -3739,7 +3740,13 @@ impl EditorElement {
|
||||
let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
|
||||
let result = v_flex().id(block_id).w_full().pr(editor_margins.right);
|
||||
|
||||
let jump_data = header_jump_data(snapshot, block_row_start, *height, first_excerpt);
|
||||
let jump_data = header_jump_data(
|
||||
snapshot,
|
||||
block_row_start,
|
||||
*height,
|
||||
first_excerpt,
|
||||
latest_selection_anchors,
|
||||
);
|
||||
result
|
||||
.child(self.render_buffer_header(
|
||||
first_excerpt,
|
||||
@@ -3774,7 +3781,13 @@ impl EditorElement {
|
||||
Block::BufferHeader { excerpt, height } => {
|
||||
let mut result = v_flex().id(block_id).w_full();
|
||||
|
||||
let jump_data = header_jump_data(snapshot, block_row_start, *height, excerpt);
|
||||
let jump_data = header_jump_data(
|
||||
snapshot,
|
||||
block_row_start,
|
||||
*height,
|
||||
excerpt,
|
||||
latest_selection_anchors,
|
||||
);
|
||||
|
||||
if sticky_header_excerpt_id != Some(excerpt.id) {
|
||||
let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
|
||||
@@ -4236,6 +4249,7 @@ impl EditorElement {
|
||||
line_layouts: &mut [LineWithInvisibles],
|
||||
selections: &[Selection<Point>],
|
||||
selected_buffer_ids: &Vec<BufferId>,
|
||||
latest_selection_anchors: &HashMap<BufferId, Anchor>,
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
sticky_header_excerpt_id: Option<ExcerptId>,
|
||||
window: &mut Window,
|
||||
@@ -4279,6 +4293,7 @@ impl EditorElement {
|
||||
&mut row_block_types,
|
||||
selections,
|
||||
selected_buffer_ids,
|
||||
latest_selection_anchors,
|
||||
is_row_soft_wrapped,
|
||||
sticky_header_excerpt_id,
|
||||
window,
|
||||
@@ -4336,6 +4351,7 @@ impl EditorElement {
|
||||
&mut row_block_types,
|
||||
selections,
|
||||
selected_buffer_ids,
|
||||
latest_selection_anchors,
|
||||
is_row_soft_wrapped,
|
||||
sticky_header_excerpt_id,
|
||||
window,
|
||||
@@ -4391,6 +4407,7 @@ impl EditorElement {
|
||||
&mut row_block_types,
|
||||
selections,
|
||||
selected_buffer_ids,
|
||||
latest_selection_anchors,
|
||||
is_row_soft_wrapped,
|
||||
sticky_header_excerpt_id,
|
||||
window,
|
||||
@@ -4473,6 +4490,7 @@ impl EditorElement {
|
||||
hitbox: &Hitbox,
|
||||
selected_buffer_ids: &Vec<BufferId>,
|
||||
blocks: &[BlockLayout],
|
||||
latest_selection_anchors: &HashMap<BufferId, Anchor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
@@ -4481,6 +4499,7 @@ impl EditorElement {
|
||||
DisplayRow(scroll_position.y as u32),
|
||||
FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
excerpt,
|
||||
latest_selection_anchors,
|
||||
);
|
||||
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
@@ -7780,18 +7799,52 @@ fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
|
||||
}
|
||||
|
||||
fn header_jump_data(
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
block_row_start: DisplayRow,
|
||||
height: u32,
|
||||
first_excerpt: &ExcerptInfo,
|
||||
latest_selection_anchors: &HashMap<BufferId, Anchor>,
|
||||
) -> JumpData {
|
||||
let jump_target = if let Some(anchor) = latest_selection_anchors.get(&first_excerpt.buffer_id)
|
||||
&& let Some(range) = editor_snapshot.context_range_for_excerpt(anchor.excerpt_id)
|
||||
&& let Some(buffer) = editor_snapshot
|
||||
.buffer_snapshot()
|
||||
.buffer_for_excerpt(anchor.excerpt_id)
|
||||
{
|
||||
JumpTargetInExcerptInput {
|
||||
id: anchor.excerpt_id,
|
||||
buffer,
|
||||
excerpt_start_anchor: range.start,
|
||||
jump_anchor: anchor.text_anchor,
|
||||
}
|
||||
} else {
|
||||
JumpTargetInExcerptInput {
|
||||
id: first_excerpt.id,
|
||||
buffer: &first_excerpt.buffer,
|
||||
excerpt_start_anchor: first_excerpt.range.context.start,
|
||||
jump_anchor: first_excerpt.range.primary.start,
|
||||
}
|
||||
};
|
||||
header_jump_data_inner(editor_snapshot, block_row_start, height, &jump_target)
|
||||
}
|
||||
|
||||
struct JumpTargetInExcerptInput<'a> {
|
||||
id: ExcerptId,
|
||||
buffer: &'a language::BufferSnapshot,
|
||||
excerpt_start_anchor: text::Anchor,
|
||||
jump_anchor: text::Anchor,
|
||||
}
|
||||
|
||||
fn header_jump_data_inner(
|
||||
snapshot: &EditorSnapshot,
|
||||
block_row_start: DisplayRow,
|
||||
height: u32,
|
||||
for_excerpt: &ExcerptInfo,
|
||||
for_excerpt: &JumpTargetInExcerptInput,
|
||||
) -> JumpData {
|
||||
let range = &for_excerpt.range;
|
||||
let buffer = &for_excerpt.buffer;
|
||||
let jump_anchor = range.primary.start;
|
||||
|
||||
let excerpt_start = range.context.start;
|
||||
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
|
||||
let rows_from_excerpt_start = if jump_anchor == excerpt_start {
|
||||
let jump_position = language::ToPoint::to_point(&for_excerpt.jump_anchor, buffer);
|
||||
let excerpt_start = for_excerpt.excerpt_start_anchor;
|
||||
let rows_from_excerpt_start = if for_excerpt.jump_anchor == excerpt_start {
|
||||
0
|
||||
} else {
|
||||
let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, buffer);
|
||||
@@ -7808,7 +7861,7 @@ fn header_jump_data(
|
||||
|
||||
JumpData::MultiBufferPoint {
|
||||
excerpt_id: for_excerpt.id,
|
||||
anchor: jump_anchor,
|
||||
anchor: for_excerpt.jump_anchor,
|
||||
position: jump_position,
|
||||
line_offset_from_top,
|
||||
}
|
||||
@@ -9002,6 +9055,9 @@ impl Element for EditorElement {
|
||||
)
|
||||
});
|
||||
|
||||
if snapshot.scroll_near_end() {
|
||||
dbg!("near end!");
|
||||
}
|
||||
let mut scroll_position = snapshot.scroll_position();
|
||||
// The scroll position is a fractional point, the whole number of which represents
|
||||
// the top of the window in terms of display rows.
|
||||
@@ -9125,15 +9181,18 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
|
||||
let (local_selections, selected_buffer_ids): (
|
||||
let (local_selections, selected_buffer_ids, latest_selection_anchors): (
|
||||
Vec<Selection<Point>>,
|
||||
Vec<BufferId>,
|
||||
HashMap<BufferId, Anchor>,
|
||||
) = self
|
||||
.editor_with_selections(cx)
|
||||
.map(|editor| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let all_selections =
|
||||
editor.selections.all::<Point>(&snapshot.display_snapshot);
|
||||
let all_anchor_selections =
|
||||
editor.selections.all_anchors(&snapshot.display_snapshot);
|
||||
let selected_buffer_ids =
|
||||
if editor.buffer_kind(cx) == ItemBufferKind::Singleton {
|
||||
Vec::new()
|
||||
@@ -9162,10 +9221,31 @@ impl Element for EditorElement {
|
||||
selections
|
||||
.extend(editor.selections.pending(&snapshot.display_snapshot));
|
||||
|
||||
(selections, selected_buffer_ids)
|
||||
let mut anchors_by_buffer: HashMap<BufferId, (usize, Anchor)> =
|
||||
HashMap::default();
|
||||
for selection in all_anchor_selections.iter() {
|
||||
let head = selection.head();
|
||||
if let Some(buffer_id) = head.buffer_id {
|
||||
anchors_by_buffer
|
||||
.entry(buffer_id)
|
||||
.and_modify(|(latest_id, latest_anchor)| {
|
||||
if selection.id > *latest_id {
|
||||
*latest_id = selection.id;
|
||||
*latest_anchor = head;
|
||||
}
|
||||
})
|
||||
.or_insert((selection.id, head));
|
||||
}
|
||||
}
|
||||
let latest_selection_anchors = anchors_by_buffer
|
||||
.into_iter()
|
||||
.map(|(buffer_id, (_, anchor))| (buffer_id, anchor))
|
||||
.collect();
|
||||
|
||||
(selections, selected_buffer_ids, latest_selection_anchors)
|
||||
})
|
||||
})
|
||||
.unwrap_or_default();
|
||||
.unwrap_or_else(|| (Vec::new(), Vec::new(), HashMap::default()));
|
||||
|
||||
let (selections, mut active_rows, newest_selection_head) = self
|
||||
.layout_selections(
|
||||
@@ -9396,6 +9476,7 @@ impl Element for EditorElement {
|
||||
&mut line_layouts,
|
||||
&local_selections,
|
||||
&selected_buffer_ids,
|
||||
&latest_selection_anchors,
|
||||
is_row_soft_wrapped,
|
||||
sticky_header_excerpt_id,
|
||||
window,
|
||||
@@ -9429,6 +9510,7 @@ impl Element for EditorElement {
|
||||
&hitbox,
|
||||
&selected_buffer_ids,
|
||||
&blocks,
|
||||
&latest_selection_anchors,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
collections::hash_map,
|
||||
ops::{ControlFlow, Range},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -778,6 +779,7 @@ 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)| {
|
||||
@@ -804,8 +806,35 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
})
|
||||
.flat_map(|hints| hints.into_values())
|
||||
.flatten()
|
||||
.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
|
||||
})
|
||||
.filter_map(|(hint_id, lsp_hint)| {
|
||||
if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind)
|
||||
&& inlay_hints
|
||||
@@ -3732,6 +3761,7 @@ 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()
|
||||
@@ -3804,6 +3834,78 @@ 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)
|
||||
@@ -3847,6 +3949,7 @@ 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();
|
||||
|
||||
@@ -3855,11 +3958,16 @@ 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![
|
||||
@@ -3869,7 +3977,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"
|
||||
"lib.rs is added before main.rs , so its excerpts should be visible first; hints should be deduplicated per label"
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -3919,8 +4027,12 @@ 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),
|
||||
|
||||
@@ -1586,12 +1586,11 @@ 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], collapse);
|
||||
let range = self.range_for_match(&matches[index]);
|
||||
let autoscroll = if EditorSettings::get_global(cx).search.center_on_match {
|
||||
Autoscroll::center()
|
||||
} else {
|
||||
|
||||
@@ -81,7 +81,19 @@ impl MouseContextMenu {
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Self {
|
||||
let context_menu_focus = context_menu.focus_handle(cx);
|
||||
window.focus(&context_menu_focus);
|
||||
|
||||
// Since `ContextMenu` is rendered in a deferred fashion its focus
|
||||
// handle is not linked to the Editor's until after the deferred draw
|
||||
// callback runs.
|
||||
// We need to wait for that to happen before focusing it, so that
|
||||
// calling `contains_focused` on the editor's focus handle returns
|
||||
// `true` when the `ContextMenu` is focused.
|
||||
let focus_handle = context_menu_focus.clone();
|
||||
cx.on_next_frame(window, move |_, window, cx| {
|
||||
cx.on_next_frame(window, move |_, window, _cx| {
|
||||
window.focus(&focus_handle);
|
||||
});
|
||||
});
|
||||
|
||||
let _dismiss_subscription = cx.subscribe_in(&context_menu, window, {
|
||||
let context_menu_focus = context_menu_focus.clone();
|
||||
@@ -329,8 +341,18 @@ mod tests {
|
||||
}
|
||||
"});
|
||||
cx.editor(|editor, _window, _app| assert!(editor.mouse_context_menu.is_none()));
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
deploy_context_menu(editor, Some(Default::default()), point, window, cx)
|
||||
deploy_context_menu(editor, Some(Default::default()), point, window, cx);
|
||||
|
||||
// Assert that, even after deploying the editor's mouse context
|
||||
// menu, the editor's focus handle still contains the focused
|
||||
// element. The pane's tab bar relies on this to determine whether
|
||||
// to show the tab bar buttons and there was a small flicker when
|
||||
// deploying the mouse context menu that would cause this to not be
|
||||
// true, making it so that the buttons would disappear for a couple
|
||||
// of frames.
|
||||
assert!(editor.focus_handle.contains_focused(window, cx));
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
@@ -46,12 +46,20 @@ impl ScrollAnchor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn near_end(&self, snapshot: &DisplaySnapshot) -> bool {
|
||||
let editor_length = snapshot.max_point().row().as_f64();
|
||||
let scroll_top = self.anchor.to_display_point(snapshot).row().as_f64();
|
||||
(scroll_top - editor_length).abs() < 300.0
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<ScrollOffset> {
|
||||
self.offset.apply_along(Axis::Vertical, |offset| {
|
||||
if self.anchor == Anchor::min() {
|
||||
0.
|
||||
} else {
|
||||
dbg!(snapshot.max_point().row().as_f64());
|
||||
let scroll_top = self.anchor.to_display_point(snapshot).row().as_f64();
|
||||
dbg!(scroll_top, offset);
|
||||
(offset + scroll_top).max(0.)
|
||||
}
|
||||
})
|
||||
@@ -243,6 +251,11 @@ impl ScrollManager {
|
||||
}
|
||||
}
|
||||
};
|
||||
let near_end = self.anchor.near_end(map);
|
||||
// // TODO call load more here
|
||||
// if near_end {
|
||||
// cx.read();
|
||||
// }
|
||||
|
||||
let scroll_top_row = DisplayRow(scroll_top as u32);
|
||||
let scroll_top_buffer_point = map
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use language::rust_lang;
|
||||
use language::{markdown_lang, rust_lang};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Editor, ToPoint};
|
||||
@@ -313,6 +313,22 @@ impl EditorLspTestContext {
|
||||
Self::new(language, Default::default(), cx).await
|
||||
}
|
||||
|
||||
pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> Self {
|
||||
let context = Self::new(
|
||||
Arc::into_inner(markdown_lang()).unwrap(),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let language_registry = context.workspace.read_with(cx, |workspace, cx| {
|
||||
workspace.project().read(cx).languages().clone()
|
||||
});
|
||||
language_registry.add(rust_lang());
|
||||
|
||||
context
|
||||
}
|
||||
|
||||
/// Constructs lsp range using a marked string with '[', ']' range delimiters
|
||||
#[track_caller]
|
||||
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||
|
||||
@@ -25,6 +25,7 @@ language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
parking_lot.workspace = true
|
||||
proto.workspace = true
|
||||
semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -193,6 +193,36 @@ pub struct TargetConfig {
|
||||
/// If not provided and the URL is a GitHub release, we'll attempt to fetch it from GitHub.
|
||||
#[serde(default)]
|
||||
pub sha256: Option<String>,
|
||||
/// Environment variables to set when launching the agent server.
|
||||
/// These target-specific env vars will override any env vars set at the agent level.
|
||||
#[serde(default)]
|
||||
pub env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl TargetConfig {
|
||||
pub fn from_proto(proto: proto::ExternalExtensionAgentTarget) -> Self {
|
||||
Self {
|
||||
archive: proto.archive,
|
||||
cmd: proto.cmd,
|
||||
args: proto.args,
|
||||
sha256: proto.sha256,
|
||||
env: proto.env.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::ExternalExtensionAgentTarget {
|
||||
proto::ExternalExtensionAgentTarget {
|
||||
archive: self.archive.clone(),
|
||||
cmd: self.cmd.clone(),
|
||||
args: self.args.clone(),
|
||||
sha256: self.sha256.clone(),
|
||||
env: self
|
||||
.env
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
@@ -265,25 +295,26 @@ impl ExtensionManifest {
|
||||
.and_then(OsStr::to_str)
|
||||
.context("invalid extension name")?;
|
||||
|
||||
let mut extension_manifest_path = extension_dir.join("extension.json");
|
||||
let extension_manifest_path = extension_dir.join("extension.toml");
|
||||
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_json = serde_json::from_str::<OldExtensionManifest>(&manifest_content)
|
||||
.with_context(|| {
|
||||
format!("invalid extension.json for extension {extension_name}")
|
||||
})?;
|
||||
|
||||
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:?}")
|
||||
})?;
|
||||
toml::from_str(&manifest_content).map_err(|err| {
|
||||
anyhow!("Invalid extension.toml for extension {extension_name}:\n{err}")
|
||||
})
|
||||
} else if let extension_manifest_path = extension_manifest_path.with_extension("json")
|
||||
&& 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:?}")
|
||||
})?;
|
||||
|
||||
serde_json::from_str::<OldExtensionManifest>(&manifest_content)
|
||||
.with_context(|| format!("invalid extension.json for extension {extension_name}"))
|
||||
.map(|manifest_json| manifest_from_old_manifest(manifest_json, extension_name))
|
||||
} else {
|
||||
anyhow::bail!("No extension manifest found for extension {extension_name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use client::ExtensionProvides;
|
||||
use client::{Client, ExtensionMetadata, GetExtensionsResponse, proto, telemetry::Telemetry};
|
||||
use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map};
|
||||
use collections::{BTreeMap, BTreeSet, HashSet, btree_map};
|
||||
pub use extension::ExtensionManifest;
|
||||
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||
use extension::{
|
||||
@@ -43,7 +43,7 @@ use language::{
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::ContextProviderWithTasks;
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::{RemoteClient, RemoteConnectionOptions};
|
||||
use remote::RemoteClient;
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -123,7 +123,7 @@ pub struct ExtensionStore {
|
||||
pub wasm_host: Arc<WasmHost>,
|
||||
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
|
||||
pub tasks: Vec<Task<()>>,
|
||||
pub remote_clients: HashMap<RemoteConnectionOptions, WeakEntity<RemoteClient>>,
|
||||
pub remote_clients: Vec<WeakEntity<RemoteClient>>,
|
||||
pub ssh_registered_tx: UnboundedSender<()>,
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ impl ExtensionStore {
|
||||
reload_tx,
|
||||
tasks: Vec::new(),
|
||||
|
||||
remote_clients: HashMap::default(),
|
||||
remote_clients: Default::default(),
|
||||
ssh_registered_tx: connection_registered_tx,
|
||||
};
|
||||
|
||||
@@ -343,12 +343,12 @@ impl ExtensionStore {
|
||||
let index = this
|
||||
.update(cx, |this, cx| this.rebuild_extension_index(cx))?
|
||||
.await;
|
||||
this.update( cx, |this, cx| this.extensions_updated(index, cx))?
|
||||
this.update(cx, |this, cx| this.extensions_updated(index, cx))?
|
||||
.await;
|
||||
index_changed = false;
|
||||
}
|
||||
|
||||
Self::update_ssh_clients(&this, cx).await?;
|
||||
Self::update_remote_clients(&this, cx).await?;
|
||||
}
|
||||
_ = connection_registered_rx.next() => {
|
||||
debounce_timer = cx
|
||||
@@ -758,29 +758,28 @@ impl ExtensionStore {
|
||||
if let Some(content_length) = content_length {
|
||||
let actual_len = tar_gz_bytes.len();
|
||||
if content_length != actual_len {
|
||||
bail!("downloaded extension size {actual_len} does not match content length {content_length}");
|
||||
bail!(concat!(
|
||||
"downloaded extension size {actual_len} ",
|
||||
"does not match content length {content_length}"
|
||||
));
|
||||
}
|
||||
}
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(tar_gz_bytes.as_slice()));
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(extension_dir).await?;
|
||||
this.update( cx, |this, cx| {
|
||||
this.reload(Some(extension_id.clone()), cx)
|
||||
})?
|
||||
.await;
|
||||
this.update(cx, |this, cx| this.reload(Some(extension_id.clone()), cx))?
|
||||
.await;
|
||||
|
||||
if let ExtensionOperation::Install = operation {
|
||||
this.update( cx, |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
cx.emit(Event::ExtensionInstalled(extension_id.clone()));
|
||||
if let Some(events) = ExtensionEvents::try_global(cx)
|
||||
&& let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
|
||||
events.update(cx, |this, cx| {
|
||||
this.emit(
|
||||
extension::Event::ExtensionInstalled(manifest.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
&& let Some(manifest) = this.extension_manifest_for_id(&extension_id)
|
||||
{
|
||||
events.update(cx, |this, cx| {
|
||||
this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -1726,7 +1725,7 @@ impl ExtensionStore {
|
||||
})
|
||||
}
|
||||
|
||||
async fn sync_extensions_over_ssh(
|
||||
async fn sync_extensions_to_remotes(
|
||||
this: &WeakEntity<Self>,
|
||||
client: WeakEntity<RemoteClient>,
|
||||
cx: &mut AsyncApp,
|
||||
@@ -1779,7 +1778,11 @@ impl ExtensionStore {
|
||||
})?,
|
||||
path_style,
|
||||
);
|
||||
log::info!("Uploading extension {}", missing_extension.clone().id);
|
||||
log::info!(
|
||||
"Uploading extension {} to {:?}",
|
||||
missing_extension.clone().id,
|
||||
dest_dir
|
||||
);
|
||||
|
||||
client
|
||||
.update(cx, |client, cx| {
|
||||
@@ -1792,27 +1795,35 @@ impl ExtensionStore {
|
||||
missing_extension.clone().id
|
||||
);
|
||||
|
||||
client
|
||||
let result = client
|
||||
.update(cx, |client, _cx| {
|
||||
client.proto_client().request(proto::InstallExtension {
|
||||
tmp_dir: dest_dir.to_proto(),
|
||||
extension: Some(missing_extension),
|
||||
extension: Some(missing_extension.clone()),
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
log::error!(
|
||||
"Failed to install extension {}: {}",
|
||||
missing_extension.id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_ssh_clients(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
|
||||
pub async fn update_remote_clients(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
|
||||
let clients = this.update(cx, |this, _cx| {
|
||||
this.remote_clients.retain(|_k, v| v.upgrade().is_some());
|
||||
this.remote_clients.values().cloned().collect::<Vec<_>>()
|
||||
this.remote_clients.retain(|v| v.upgrade().is_some());
|
||||
this.remote_clients.clone()
|
||||
})?;
|
||||
|
||||
for client in clients {
|
||||
Self::sync_extensions_over_ssh(this, client, cx)
|
||||
Self::sync_extensions_to_remotes(this, client, cx)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
@@ -1820,16 +1831,12 @@ impl ExtensionStore {
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub fn register_remote_client(&mut self, client: Entity<RemoteClient>, cx: &mut Context<Self>) {
|
||||
let options = client.read(cx).connection_options();
|
||||
|
||||
if let Some(existing_client) = self.remote_clients.get(&options)
|
||||
&& existing_client.upgrade().is_some()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.remote_clients.insert(options, client.downgrade());
|
||||
pub fn register_remote_client(
|
||||
&mut self,
|
||||
client: Entity<RemoteClient>,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
self.remote_clients.push(client.downgrade());
|
||||
self.ssh_registered_tx.unbounded_send(()).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +279,8 @@ impl HeadlessExtensionStore {
|
||||
}
|
||||
|
||||
fs.rename(&tmp_path, &path, RenameOptions::default())
|
||||
.await?;
|
||||
.await
|
||||
.context("Failed to rename {tmp_path:?} to {path:?}")?;
|
||||
|
||||
Self::load_extension(this, extension, cx).await
|
||||
})
|
||||
|
||||
@@ -359,6 +359,7 @@ impl GitRepository for FakeGitRepository {
|
||||
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
anyhow::Ok(GitStatus {
|
||||
entries: entries.into(),
|
||||
renamed_paths: HashMap::default(),
|
||||
})
|
||||
});
|
||||
Task::ready(match result {
|
||||
|
||||
@@ -395,19 +395,19 @@ mod tests {
|
||||
thread::spawn(move || stream.run(move |events| tx.send(events.to_vec()).is_ok()));
|
||||
|
||||
fs::write(path.join("new-file"), "").unwrap();
|
||||
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
let events = rx.recv_timeout(timeout()).unwrap();
|
||||
let event = events.last().unwrap();
|
||||
assert_eq!(event.path, path.join("new-file"));
|
||||
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
|
||||
|
||||
fs::remove_file(path.join("existing-file-5")).unwrap();
|
||||
let mut events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
let mut events = rx.recv_timeout(timeout()).unwrap();
|
||||
let mut event = events.last().unwrap();
|
||||
// we see this duplicate about 1/100 test runs.
|
||||
if event.path == path.join("new-file")
|
||||
&& event.flags.contains(StreamFlags::ITEM_CREATED)
|
||||
{
|
||||
events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
events = rx.recv_timeout(timeout()).unwrap();
|
||||
event = events.last().unwrap();
|
||||
}
|
||||
assert_eq!(event.path, path.join("existing-file-5"));
|
||||
@@ -440,13 +440,13 @@ mod tests {
|
||||
});
|
||||
|
||||
fs::write(path.join("new-file"), "").unwrap();
|
||||
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
let events = rx.recv_timeout(timeout()).unwrap();
|
||||
let event = events.last().unwrap();
|
||||
assert_eq!(event.path, path.join("new-file"));
|
||||
assert!(event.flags.contains(StreamFlags::ITEM_CREATED));
|
||||
|
||||
fs::remove_file(path.join("existing-file-5")).unwrap();
|
||||
let events = rx.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
let events = rx.recv_timeout(timeout()).unwrap();
|
||||
let event = events.last().unwrap();
|
||||
assert_eq!(event.path, path.join("existing-file-5"));
|
||||
assert!(event.flags.contains(StreamFlags::ITEM_REMOVED));
|
||||
@@ -477,11 +477,11 @@ mod tests {
|
||||
});
|
||||
|
||||
fs::write(path.join("new-file"), "").unwrap();
|
||||
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "running");
|
||||
assert_eq!(rx.recv_timeout(timeout()).unwrap(), "running");
|
||||
|
||||
// Dropping the handle causes `EventStream::run` to return.
|
||||
drop(handle);
|
||||
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), "stopped");
|
||||
assert_eq!(rx.recv_timeout(timeout()).unwrap(), "stopped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -500,11 +500,14 @@ mod tests {
|
||||
}
|
||||
|
||||
fn flush_historical_events() {
|
||||
let duration = if std::env::var("CI").is_ok() {
|
||||
Duration::from_secs(2)
|
||||
thread::sleep(timeout());
|
||||
}
|
||||
|
||||
fn timeout() -> Duration {
|
||||
if std::env::var("CI").is_ok() {
|
||||
Duration::from_secs(4)
|
||||
} else {
|
||||
Duration::from_millis(500)
|
||||
};
|
||||
thread::sleep(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2045,7 +2045,7 @@ fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
|
||||
OsString::from("status"),
|
||||
OsString::from("--porcelain=v1"),
|
||||
OsString::from("--untracked-files=all"),
|
||||
OsString::from("--no-renames"),
|
||||
OsString::from("--find-renames"),
|
||||
OsString::from("-z"),
|
||||
];
|
||||
args.extend(
|
||||
@@ -2387,22 +2387,37 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
|
||||
continue;
|
||||
}
|
||||
let mut fields = line.split('\x00');
|
||||
let is_current_branch = fields.next().context("no HEAD")? == "*";
|
||||
let head_sha: SharedString = fields.next().context("no objectname")?.to_string().into();
|
||||
let parent_sha: SharedString = fields.next().context("no parent")?.to_string().into();
|
||||
let ref_name = fields.next().context("no refname")?.to_string().into();
|
||||
let upstream_name = fields.next().context("no upstream")?.to_string();
|
||||
let upstream_tracking = parse_upstream_track(fields.next().context("no upstream:track")?)?;
|
||||
let commiterdate = fields.next().context("no committerdate")?.parse::<i64>()?;
|
||||
let author_name = fields.next().context("no authorname")?.to_string().into();
|
||||
let subject: SharedString = fields
|
||||
.next()
|
||||
.context("no contents:subject")?
|
||||
.to_string()
|
||||
.into();
|
||||
let Some(head) = fields.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(head_sha) = fields.next().map(|f| f.to_string().into()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(parent_sha) = fields.next().map(|f| f.to_string()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(ref_name) = fields.next().map(|f| f.to_string().into()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(upstream_name) = fields.next().map(|f| f.to_string()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(upstream_tracking) = fields.next().and_then(|f| parse_upstream_track(f).ok())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(commiterdate) = fields.next().and_then(|f| f.parse::<i64>().ok()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(author_name) = fields.next().map(|f| f.to_string().into()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(subject) = fields.next().map(|f| f.to_string().into()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
branches.push(Branch {
|
||||
is_head: is_current_branch,
|
||||
is_head: head == "*",
|
||||
ref_name,
|
||||
most_recent_commit: Some(CommitSummary {
|
||||
sha: head_sha,
|
||||
@@ -2744,6 +2759,44 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_branches_parsing_containing_refs_with_missing_fields() {
|
||||
#[allow(clippy::octal_escapes)]
|
||||
let input = " \090012116c03db04344ab10d50348553aa94f1ea0\0refs/heads/broken\n \0eb0cae33272689bd11030822939dd2701c52f81e\0895951d681e5561478c0acdd6905e8aacdfd2249\0refs/heads/dev\0\0\01762948725\0Zed\0Add feature\n*\0895951d681e5561478c0acdd6905e8aacdfd2249\0\0refs/heads/main\0\0\01762948695\0Zed\0Initial commit\n";
|
||||
|
||||
let branches = parse_branch_input(input).unwrap();
|
||||
assert_eq!(branches.len(), 2);
|
||||
assert_eq!(
|
||||
branches,
|
||||
vec![
|
||||
Branch {
|
||||
is_head: false,
|
||||
ref_name: "refs/heads/dev".into(),
|
||||
upstream: None,
|
||||
most_recent_commit: Some(CommitSummary {
|
||||
sha: "eb0cae33272689bd11030822939dd2701c52f81e".into(),
|
||||
subject: "Add feature".into(),
|
||||
commit_timestamp: 1762948725,
|
||||
author_name: SharedString::new("Zed"),
|
||||
has_parent: true,
|
||||
})
|
||||
},
|
||||
Branch {
|
||||
is_head: true,
|
||||
ref_name: "refs/heads/main".into(),
|
||||
upstream: None,
|
||||
most_recent_commit: Some(CommitSummary {
|
||||
sha: "895951d681e5561478c0acdd6905e8aacdfd2249".into(),
|
||||
subject: "Initial commit".into(),
|
||||
commit_timestamp: 1762948695,
|
||||
author_name: SharedString::new("Zed"),
|
||||
has_parent: false,
|
||||
})
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
impl RealGitRepository {
|
||||
/// Force a Git garbage collection on the repository.
|
||||
fn gc(&self) -> BoxFuture<'_, Result<()>> {
|
||||
|
||||
@@ -203,6 +203,14 @@ impl FileStatus {
|
||||
matches!(self, FileStatus::Untracked)
|
||||
}
|
||||
|
||||
pub fn is_renamed(self) -> bool {
|
||||
let FileStatus::Tracked(tracked) = self else {
|
||||
return false;
|
||||
};
|
||||
tracked.index_status == StatusCode::Renamed
|
||||
|| tracked.worktree_status == StatusCode::Renamed
|
||||
}
|
||||
|
||||
pub fn summary(self) -> GitSummary {
|
||||
match self {
|
||||
FileStatus::Ignored => GitSummary::UNCHANGED,
|
||||
@@ -430,34 +438,79 @@ impl std::ops::Sub for GitSummary {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GitStatus {
|
||||
pub entries: Arc<[(RepoPath, FileStatus)]>,
|
||||
pub renamed_paths: HashMap<RepoPath, RepoPath>,
|
||||
}
|
||||
|
||||
impl FromStr for GitStatus {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut entries = s
|
||||
.split('\0')
|
||||
.filter_map(|entry| {
|
||||
let sep = entry.get(2..3)?;
|
||||
if sep != " " {
|
||||
return None;
|
||||
let mut parts = s.split('\0').peekable();
|
||||
let mut entries = Vec::new();
|
||||
let mut renamed_paths = HashMap::default();
|
||||
|
||||
while let Some(entry) = parts.next() {
|
||||
if entry.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !matches!(entry.get(2..3), Some(" ")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path_or_old_path = &entry[3..];
|
||||
|
||||
if path_or_old_path.ends_with('/') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let status = match entry.as_bytes()[0..2].try_into() {
|
||||
Ok(bytes) => match FileStatus::from_bytes(bytes).log_err() {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
},
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let is_rename = matches!(
|
||||
status,
|
||||
FileStatus::Tracked(TrackedStatus {
|
||||
index_status: StatusCode::Renamed | StatusCode::Copied,
|
||||
..
|
||||
}) | FileStatus::Tracked(TrackedStatus {
|
||||
worktree_status: StatusCode::Renamed | StatusCode::Copied,
|
||||
..
|
||||
})
|
||||
);
|
||||
|
||||
let (old_path_str, new_path_str) = if is_rename {
|
||||
let new_path = match parts.next() {
|
||||
Some(new_path) if !new_path.is_empty() => new_path,
|
||||
_ => continue,
|
||||
};
|
||||
let path = &entry[3..];
|
||||
// The git status output includes untracked directories as well as untracked files.
|
||||
// We do our own processing to compute the "summary" status of each directory,
|
||||
// so just skip any directories in the output, since they'll otherwise interfere
|
||||
// with our handling of nested repositories.
|
||||
if path.ends_with('/') {
|
||||
return None;
|
||||
(path_or_old_path, new_path)
|
||||
} else {
|
||||
(path_or_old_path, path_or_old_path)
|
||||
};
|
||||
|
||||
if new_path_str.ends_with('/') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_path = match RelPath::unix(new_path_str).log_err() {
|
||||
Some(p) => RepoPath::from_rel_path(p),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if is_rename {
|
||||
if let Some(old_path_rel) = RelPath::unix(old_path_str).log_err() {
|
||||
let old_path_repo = RepoPath::from_rel_path(old_path_rel);
|
||||
renamed_paths.insert(new_path.clone(), old_path_repo);
|
||||
}
|
||||
let status = entry.as_bytes()[0..2].try_into().unwrap();
|
||||
let status = FileStatus::from_bytes(status).log_err()?;
|
||||
// git-status outputs `/`-delimited repo paths, even on Windows.
|
||||
let path = RepoPath::from_rel_path(RelPath::unix(path).log_err()?);
|
||||
Some((path, status))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
entries.push((new_path, status));
|
||||
}
|
||||
entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
|
||||
// When a file exists in HEAD, is deleted in the index, and exists again in the working copy,
|
||||
// git produces two lines for it, one reading `D ` (deleted in index, unmodified in working copy)
|
||||
@@ -481,6 +534,7 @@ impl FromStr for GitStatus {
|
||||
});
|
||||
Ok(Self {
|
||||
entries: entries.into(),
|
||||
renamed_paths,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -489,6 +543,7 @@ impl Default for GitStatus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
entries: Arc::new([]),
|
||||
renamed_paths: HashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,27 +60,27 @@ pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mu
|
||||
buffer_added(editor, buffer, cx);
|
||||
}
|
||||
|
||||
cx.subscribe(&cx.entity(), |editor, _, event, cx| match event {
|
||||
EditorEvent::ExcerptsAdded { buffer, .. } => buffer_added(editor, buffer.clone(), cx),
|
||||
EditorEvent::ExcerptsExpanded { ids } => {
|
||||
let multibuffer = editor.buffer().read(cx).snapshot(cx);
|
||||
for excerpt_id in ids {
|
||||
let Some(buffer) = multibuffer.buffer_for_excerpt(*excerpt_id) else {
|
||||
continue;
|
||||
};
|
||||
let addon = editor.addon::<ConflictAddon>().unwrap();
|
||||
let Some(conflict_set) = addon.conflict_set(buffer.remote_id()).clone() else {
|
||||
return;
|
||||
};
|
||||
excerpt_for_buffer_updated(editor, conflict_set, cx);
|
||||
}
|
||||
}
|
||||
EditorEvent::ExcerptsRemoved {
|
||||
removed_buffer_ids, ..
|
||||
} => buffers_removed(editor, removed_buffer_ids, cx),
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
// cx.subscribe(&cx.entity(), |editor, _, event, cx| match event {
|
||||
// EditorEvent::ExcerptsAdded { buffer, .. } => buffer_added(editor, buffer.clone(), cx),
|
||||
// EditorEvent::ExcerptsExpanded { ids } => {
|
||||
// let multibuffer = editor.buffer().read(cx).snapshot(cx);
|
||||
// for excerpt_id in ids {
|
||||
// let Some(buffer) = multibuffer.buffer_for_excerpt(*excerpt_id) else {
|
||||
// continue;
|
||||
// };
|
||||
// let addon = editor.addon::<ConflictAddon>().unwrap();
|
||||
// let Some(conflict_set) = addon.conflict_set(buffer.remote_id()).clone() else {
|
||||
// return;
|
||||
// };
|
||||
// excerpt_for_buffer_updated(editor, conflict_set, cx);
|
||||
// }
|
||||
// }
|
||||
// EditorEvent::ExcerptsRemoved {
|
||||
// removed_buffer_ids, ..
|
||||
// } => buffers_removed(editor, removed_buffer_ids, cx),
|
||||
// _ => {}
|
||||
// })
|
||||
// .detach();
|
||||
}
|
||||
|
||||
fn excerpt_for_buffer_updated(
|
||||
|
||||
@@ -12,7 +12,9 @@ use agent_settings::AgentSettings;
|
||||
use anyhow::Context as _;
|
||||
use askpass::AskPassDelegate;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{Editor, EditorElement, EditorMode, MultiBuffer};
|
||||
use editor::{
|
||||
Direction, Editor, EditorElement, EditorMode, MultiBuffer, actions::ExpandAllDiffHunks,
|
||||
};
|
||||
use futures::StreamExt as _;
|
||||
use git::blame::ParsedCommitMessage;
|
||||
use git::repository::{
|
||||
@@ -28,10 +30,11 @@ use git::{
|
||||
TrashUntrackedFiles, UnstageAll,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AsyncApp, AsyncWindowContext, ClickEvent, Corner, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior,
|
||||
MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
|
||||
UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list,
|
||||
Action, AppContext, AsyncApp, AsyncWindowContext, ClickEvent, Corner, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
|
||||
ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy,
|
||||
Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred,
|
||||
uniform_list,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, File};
|
||||
@@ -69,7 +72,7 @@ use cloud_llm_client::CompletionIntent;
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId},
|
||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt},
|
||||
};
|
||||
|
||||
actions!(
|
||||
@@ -309,6 +312,9 @@ pub struct GitPanel {
|
||||
bulk_staging: Option<BulkStaging>,
|
||||
stash_entries: GitStash,
|
||||
_settings_subscription: Subscription,
|
||||
/// On clicking an entry in a the git_panel this will
|
||||
/// trigger loading it
|
||||
open_diff_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -409,17 +415,10 @@ impl GitPanel {
|
||||
}
|
||||
GitStoreEvent::RepositoryUpdated(
|
||||
_,
|
||||
RepositoryEvent::StatusesChanged { full_scan: true }
|
||||
RepositoryEvent::StatusesChanged
|
||||
| RepositoryEvent::BranchChanged
|
||||
| RepositoryEvent::MergeHeadsChanged,
|
||||
true,
|
||||
) => {
|
||||
this.schedule_update(window, cx);
|
||||
}
|
||||
GitStoreEvent::RepositoryUpdated(
|
||||
_,
|
||||
RepositoryEvent::StatusesChanged { full_scan: false },
|
||||
true,
|
||||
)
|
||||
| GitStoreEvent::RepositoryAdded
|
||||
| GitStoreEvent::RepositoryRemoved(_) => {
|
||||
@@ -476,6 +475,7 @@ impl GitPanel {
|
||||
bulk_staging: None,
|
||||
stash_entries: Default::default(),
|
||||
_settings_subscription,
|
||||
open_diff_task: None,
|
||||
};
|
||||
|
||||
this.schedule_update(window, cx);
|
||||
@@ -745,7 +745,7 @@ impl GitPanel {
|
||||
) {
|
||||
self.select_first_entry_if_none(cx);
|
||||
|
||||
cx.focus_self(window);
|
||||
self.focus_handle.focus(window);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -755,11 +755,23 @@ impl GitPanel {
|
||||
|
||||
fn open_diff(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
maybe!({
|
||||
let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
|
||||
let entry = self
|
||||
.entries
|
||||
.get(self.selected_entry?)?
|
||||
.status_entry()?
|
||||
.clone();
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let git_repo = self.active_repository.as_ref()?;
|
||||
let git_repo = self.active_repository.as_ref()?.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx)
|
||||
// let panel = panel.upgrade().unwrap(); // TODO FIXME
|
||||
// cx.read_entity(&panel, |panel, cx| {
|
||||
// panel
|
||||
// })
|
||||
// .unwrap(); // TODO FIXME
|
||||
|
||||
let project_diff = if let Some(project_diff) =
|
||||
workspace.read(cx).active_item_as::<ProjectDiff>(cx)
|
||||
&& let Some(project_path) = project_diff.read(cx).active_path(cx)
|
||||
&& Some(&entry.repo_path)
|
||||
== git_repo
|
||||
@@ -769,16 +781,21 @@ impl GitPanel {
|
||||
{
|
||||
project_diff.focus_handle(cx).focus(window);
|
||||
project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx));
|
||||
return None;
|
||||
};
|
||||
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
|
||||
project_diff
|
||||
} else {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx)
|
||||
})
|
||||
.ok();
|
||||
self.focus_handle.focus(window);
|
||||
};
|
||||
focus_handle.focus(window); // TODO: should we focus before the file is loaded or wait for that?
|
||||
|
||||
let project_diff = project_diff.downgrade();
|
||||
// TODO use the fancy new thing
|
||||
self.open_diff_task = Some(cx.spawn_in(window, async move |_, cx| {
|
||||
ProjectDiff::refresh_one(project_diff, entry.repo_path, entry.status, cx)
|
||||
.await
|
||||
.unwrap(); // TODO FIXME
|
||||
}));
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
@@ -799,15 +816,46 @@ impl GitPanel {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.workspace
|
||||
let open_task = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_path_preview(path, None, false, false, true, window, cx)
|
||||
.detach_and_prompt_err("Failed to open file", window, cx, |e, _, _| {
|
||||
Some(format!("{e}"))
|
||||
});
|
||||
workspace.open_path_preview(path, None, false, false, true, window, cx)
|
||||
})
|
||||
.ok()
|
||||
.ok()?;
|
||||
|
||||
cx.spawn_in(window, async move |_, mut cx| {
|
||||
let item = open_task
|
||||
.await
|
||||
.notify_async_err(&mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to open file"))?;
|
||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||
if let Some(diff_task) =
|
||||
active_editor.update(cx, |editor, _cx| editor.wait_for_diff_to_load())?
|
||||
{
|
||||
diff_task.await;
|
||||
}
|
||||
|
||||
cx.update(|window, cx| {
|
||||
active_editor.update(cx, |editor, cx| {
|
||||
editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
|
||||
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
language::Point::new(0, 0),
|
||||
Direction::Next,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1224,14 +1272,18 @@ impl GitPanel {
|
||||
let Some(active_repository) = self.active_repository.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let repo = active_repository.read(cx);
|
||||
let (stage, repo_paths) = match entry {
|
||||
GitListEntry::Status(status_entry) => {
|
||||
let repo_paths = vec![status_entry.clone()];
|
||||
let stage = if active_repository
|
||||
.read(cx)
|
||||
let stage = if repo
|
||||
.pending_ops_for_path(&status_entry.repo_path)
|
||||
.map(|ops| ops.staging() || ops.staged())
|
||||
.unwrap_or(status_entry.status.staging().has_staged())
|
||||
.or_else(|| {
|
||||
repo.status_for_path(&status_entry.repo_path)
|
||||
.map(|status| status.status.staging().has_staged())
|
||||
})
|
||||
.unwrap_or(status_entry.staging.has_staged())
|
||||
{
|
||||
if let Some(op) = self.bulk_staging.clone()
|
||||
&& op.anchor == status_entry.repo_path
|
||||
@@ -1247,13 +1299,12 @@ impl GitPanel {
|
||||
}
|
||||
GitListEntry::Header(section) => {
|
||||
let goal_staged_state = !self.header_state(section.header).selected();
|
||||
let repository = active_repository.read(cx);
|
||||
let entries = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| entry.status_entry())
|
||||
.filter(|status_entry| {
|
||||
section.contains(status_entry, repository)
|
||||
section.contains(status_entry, repo)
|
||||
&& status_entry.staging.as_bool() != Some(goal_staged_state)
|
||||
})
|
||||
.cloned()
|
||||
@@ -3224,18 +3275,12 @@ impl GitPanel {
|
||||
) -> Option<impl IntoElement> {
|
||||
self.active_repository.as_ref()?;
|
||||
|
||||
let text;
|
||||
let action;
|
||||
let tooltip;
|
||||
if self.total_staged_count() == self.entry_count && self.entry_count > 0 {
|
||||
text = "Unstage All";
|
||||
action = git::UnstageAll.boxed_clone();
|
||||
tooltip = "git reset";
|
||||
} else {
|
||||
text = "Stage All";
|
||||
action = git::StageAll.boxed_clone();
|
||||
tooltip = "git add --all ."
|
||||
}
|
||||
let (text, action, stage, tooltip) =
|
||||
if self.total_staged_count() == self.entry_count && self.entry_count > 0 {
|
||||
("Unstage All", UnstageAll.boxed_clone(), false, "git reset")
|
||||
} else {
|
||||
("Stage All", StageAll.boxed_clone(), true, "git add --all")
|
||||
};
|
||||
|
||||
let change_string = match self.entry_count {
|
||||
0 => "No Changes".to_string(),
|
||||
@@ -3273,11 +3318,15 @@ impl GitPanel {
|
||||
&self.focus_handle,
|
||||
))
|
||||
.disabled(self.entry_count == 0)
|
||||
.on_click(move |_, _, cx| {
|
||||
let action = action.boxed_clone();
|
||||
cx.defer(move |cx| {
|
||||
cx.dispatch_action(action.as_ref());
|
||||
})
|
||||
.on_click({
|
||||
let git_panel = cx.weak_entity();
|
||||
move |_, _, cx| {
|
||||
git_panel
|
||||
.update(cx, |git_panel, cx| {
|
||||
git_panel.change_all_files_stage(stage, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -3661,13 +3710,18 @@ impl GitPanel {
|
||||
let ix = self.entry_by_path(&repo_path, cx)?;
|
||||
let entry = self.entries.get(ix)?;
|
||||
|
||||
let is_staging_or_staged = if let Some(status_entry) = entry.status_entry() {
|
||||
repo.pending_ops_for_path(&repo_path)
|
||||
.map(|ops| ops.staging() || ops.staged())
|
||||
.unwrap_or(status_entry.staging.has_staged())
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let is_staging_or_staged = repo
|
||||
.pending_ops_for_path(&repo_path)
|
||||
.map(|ops| ops.staging() || ops.staged())
|
||||
.or_else(|| {
|
||||
repo.status_for_path(&repo_path)
|
||||
.and_then(|status| status.status.staging().as_bool())
|
||||
})
|
||||
.or_else(|| {
|
||||
entry
|
||||
.status_entry()
|
||||
.and_then(|entry| entry.staging.as_bool())
|
||||
});
|
||||
|
||||
let checkbox = Checkbox::new("stage-file", is_staging_or_staged.into())
|
||||
.disabled(!self.has_write_access(cx))
|
||||
@@ -3828,6 +3882,7 @@ impl GitPanel {
|
||||
})
|
||||
}
|
||||
|
||||
// context menu
|
||||
fn deploy_entry_context_menu(
|
||||
&mut self,
|
||||
position: Point<Pixels>,
|
||||
@@ -3925,6 +3980,20 @@ impl GitPanel {
|
||||
let path_style = self.project.read(cx).path_style(cx);
|
||||
let display_name = entry.display_name(path_style);
|
||||
|
||||
let active_repo = self
|
||||
.project
|
||||
.read(cx)
|
||||
.active_repository(cx)
|
||||
.expect("active repository must be set");
|
||||
let repo = active_repo.read(cx);
|
||||
let repo_snapshot = repo.snapshot();
|
||||
|
||||
let old_path = if entry.status.is_renamed() {
|
||||
repo_snapshot.renamed_paths.get(&entry.repo_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let selected = self.selected_entry == Some(ix);
|
||||
let marked = self.marked_entries.contains(&ix);
|
||||
let status_style = GitPanelSettings::get_global(cx).status_style;
|
||||
@@ -3933,15 +4002,16 @@ impl GitPanel {
|
||||
let has_conflict = status.is_conflicted();
|
||||
let is_modified = status.is_modified();
|
||||
let is_deleted = status.is_deleted();
|
||||
let is_renamed = status.is_renamed();
|
||||
|
||||
let label_color = if status_style == StatusStyle::LabelColor {
|
||||
if has_conflict {
|
||||
Color::VersionControlConflict
|
||||
} else if is_modified {
|
||||
Color::VersionControlModified
|
||||
} else if is_deleted {
|
||||
// We don't want a bunch of red labels in the list
|
||||
Color::Disabled
|
||||
} else if is_renamed || is_modified {
|
||||
Color::VersionControlModified
|
||||
} else {
|
||||
Color::VersionControlAdded
|
||||
}
|
||||
@@ -3961,12 +4031,6 @@ impl GitPanel {
|
||||
let checkbox_id: ElementId =
|
||||
ElementId::Name(format!("entry_{}_{}_checkbox", display_name, ix).into());
|
||||
|
||||
let active_repo = self
|
||||
.project
|
||||
.read(cx)
|
||||
.active_repository(cx)
|
||||
.expect("active repository must be set");
|
||||
let repo = active_repo.read(cx);
|
||||
// Checking for current staged/unstaged file status is a chained operation:
|
||||
// 1. first, we check for any pending operation recorded in repository
|
||||
// 2. if there are no pending ops either running or finished, we then ask the repository
|
||||
@@ -4044,6 +4108,7 @@ impl GitPanel {
|
||||
this.selected_entry = Some(ix);
|
||||
cx.notify();
|
||||
if event.modifiers().secondary() {
|
||||
// the click handler
|
||||
this.open_file(&Default::default(), window, cx)
|
||||
} else {
|
||||
this.open_diff(&Default::default(), window, cx);
|
||||
@@ -4121,23 +4186,32 @@ impl GitPanel {
|
||||
.items_center()
|
||||
.flex_1()
|
||||
// .overflow_hidden()
|
||||
.when_some(entry.parent_dir(path_style), |this, parent| {
|
||||
if !parent.is_empty() {
|
||||
this.child(
|
||||
self.entry_label(
|
||||
format!("{parent}{}", path_style.separator()),
|
||||
path_color,
|
||||
)
|
||||
.when(status.is_deleted(), |this| this.strikethrough()),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
.when_some(old_path.as_ref(), |this, old_path| {
|
||||
let new_display = old_path.display(path_style).to_string();
|
||||
let old_display = entry.repo_path.display(path_style).to_string();
|
||||
this.child(self.entry_label(old_display, Color::Muted).strikethrough())
|
||||
.child(self.entry_label(" → ", Color::Muted))
|
||||
.child(self.entry_label(new_display, label_color))
|
||||
})
|
||||
.child(
|
||||
self.entry_label(display_name, label_color)
|
||||
.when(status.is_deleted(), |this| this.strikethrough()),
|
||||
),
|
||||
.when(old_path.is_none(), |this| {
|
||||
this.when_some(entry.parent_dir(path_style), |this, parent| {
|
||||
if !parent.is_empty() {
|
||||
this.child(
|
||||
self.entry_label(
|
||||
format!("{parent}{}", path_style.separator()),
|
||||
path_color,
|
||||
)
|
||||
.when(status.is_deleted(), |this| this.strikethrough()),
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.child(
|
||||
self.entry_label(display_name, label_color)
|
||||
.when(status.is_deleted(), |this| this.strikethrough()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -708,6 +708,11 @@ impl RenderOnce for GitStatusIcon {
|
||||
IconName::SquareMinus,
|
||||
cx.theme().colors().version_control_deleted,
|
||||
)
|
||||
} else if status.is_renamed() {
|
||||
(
|
||||
IconName::ArrowRight,
|
||||
cx.theme().colors().version_control_modified,
|
||||
)
|
||||
} else if status.is_modified() {
|
||||
(
|
||||
IconName::SquareDot,
|
||||
|
||||
@@ -7,19 +7,21 @@ use crate::{
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
|
||||
use collections::{HashMap, HashSet};
|
||||
use db::smol::stream::StreamExt;
|
||||
use editor::{
|
||||
Addon, Editor, EditorEvent, SelectionEffects,
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
multibuffer_context_lines,
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use git::{
|
||||
Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
|
||||
repository::{Branch, RepoPath, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
status::FileStatus,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter,
|
||||
Action, AnyElement, AnyView, App, AppContext, AsyncWindowContext, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, actions,
|
||||
};
|
||||
use language::{Anchor, Buffer, Capability, OffsetRangeExt};
|
||||
@@ -27,17 +29,21 @@ use multi_buffer::{MultiBuffer, PathKey};
|
||||
use project::{
|
||||
Project, ProjectPath,
|
||||
git_store::{
|
||||
Repository,
|
||||
self, Repository, StatusEntry,
|
||||
branch_diff::{self, BranchDiffEvent, DiffBase},
|
||||
},
|
||||
};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::any::{Any, TypeId};
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
collections::VecDeque,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{ops::Range, time::Instant};
|
||||
|
||||
use theme::ActiveTheme;
|
||||
use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};
|
||||
use util::{ResultExt as _, rel_path::RelPath};
|
||||
use util::{ResultExt, rel_path::RelPath};
|
||||
use workspace::{
|
||||
CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,
|
||||
ToolbarItemView, Workspace,
|
||||
@@ -46,6 +52,8 @@ use workspace::{
|
||||
searchable::SearchableItemHandle,
|
||||
};
|
||||
|
||||
mod diff_loader;
|
||||
|
||||
actions!(
|
||||
git,
|
||||
[
|
||||
@@ -92,7 +100,7 @@ impl ProjectDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
Self::deploy_at(workspace, None, window, cx)
|
||||
Self::deploy_at(workspace, None, window, cx);
|
||||
}
|
||||
|
||||
fn deploy_branch_diff(
|
||||
@@ -134,7 +142,7 @@ impl ProjectDiff {
|
||||
entry: Option<GitStatusEntry>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
) -> Entity<ProjectDiff> {
|
||||
telemetry::event!(
|
||||
"Git Diff Opened",
|
||||
source = if entry.is_some() {
|
||||
@@ -166,7 +174,8 @@ impl ProjectDiff {
|
||||
project_diff.update(cx, |project_diff, cx| {
|
||||
project_diff.move_to_entry(entry, window, cx);
|
||||
})
|
||||
}
|
||||
};
|
||||
project_diff
|
||||
}
|
||||
|
||||
pub fn autoscroll(&self, cx: &mut Context<Self>) {
|
||||
@@ -267,15 +276,23 @@ impl ProjectDiff {
|
||||
cx.subscribe_in(&editor, window, Self::handle_editor_event)
|
||||
.detach();
|
||||
|
||||
let loader = diff_loader::start_loader(cx.entity(), window, cx);
|
||||
|
||||
let branch_diff_subscription = cx.subscribe_in(
|
||||
&branch_diff,
|
||||
window,
|
||||
move |this, _git_store, event, window, cx| match event {
|
||||
BranchDiffEvent::FileListChanged => {
|
||||
this._task = window.spawn(cx, {
|
||||
let this = cx.weak_entity();
|
||||
async |cx| Self::refresh(this, cx).await
|
||||
})
|
||||
// TODO this does not account for size of paths
|
||||
// maybe a quick fs metadata could get us info on that?
|
||||
// would make number of paths async but thats fine here
|
||||
// let entries = this.first_n_entries(cx, 100);
|
||||
loader.update_file_list();
|
||||
// let
|
||||
// this._task = window.spawn(cx, {
|
||||
// let this = cx.weak_entity();
|
||||
// async |cx| Self::refresh(this, entries, cx).await
|
||||
// })
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -290,22 +307,32 @@ impl ProjectDiff {
|
||||
if is_sort_by_path != was_sort_by_path
|
||||
|| is_collapse_untracked_diff != was_collapse_untracked_diff
|
||||
{
|
||||
this._task = {
|
||||
window.spawn(cx, {
|
||||
let this = cx.weak_entity();
|
||||
async |cx| Self::refresh(this, cx).await
|
||||
})
|
||||
}
|
||||
// no idea why we need to do anything here
|
||||
// probably should sort the multibuffer instead of reparsing
|
||||
// everything though!!!
|
||||
todo!("resort multibuffer entries");
|
||||
todo!("assert the entries in the list did not change")
|
||||
// this._task = {
|
||||
// window.spawn(cx, {
|
||||
// let this = cx.weak_entity();
|
||||
// async |cx| Self::refresh(this, cx).await
|
||||
// })
|
||||
// }
|
||||
}
|
||||
was_sort_by_path = is_sort_by_path;
|
||||
was_collapse_untracked_diff = is_collapse_untracked_diff;
|
||||
})
|
||||
.detach();
|
||||
|
||||
let task = window.spawn(cx, {
|
||||
let this = cx.weak_entity();
|
||||
async |cx| Self::refresh(this, cx).await
|
||||
});
|
||||
// let task = window.spawn(cx, {
|
||||
// let this = cx.weak_entity();
|
||||
// async |cx| {
|
||||
// let entries = this
|
||||
// .read_with(cx, |project_diff, cx| project_diff.first_n_entries(cx, 100))
|
||||
// .unwrap();
|
||||
// Self::refresh(this, entries, cx).await
|
||||
// }
|
||||
// });
|
||||
|
||||
Self {
|
||||
project,
|
||||
@@ -471,10 +498,11 @@ impl ProjectDiff {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let subscription = cx.subscribe_in(&diff, window, move |this, _, _, window, cx| {
|
||||
this._task = window.spawn(cx, {
|
||||
let this = cx.weak_entity();
|
||||
async |cx| Self::refresh(this, cx).await
|
||||
})
|
||||
// TODO fix this
|
||||
// this._task = window.spawn(cx, {
|
||||
// let this = cx.weak_entity();
|
||||
// async |cx| Self::refresh(this, cx).await
|
||||
// })
|
||||
});
|
||||
self.buffer_diff_subscriptions
|
||||
.insert(path_key.path.clone(), (diff.clone(), subscription));
|
||||
@@ -550,51 +578,221 @@ impl ProjectDiff {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn refresh(this: WeakEntity<Self>, cx: &mut AsyncWindowContext) -> Result<()> {
|
||||
let mut path_keys = Vec::new();
|
||||
let buffers_to_load = this.update(cx, |this, cx| {
|
||||
let (repo, buffers_to_load) = this.branch_diff.update(cx, |branch_diff, cx| {
|
||||
let load_buffers = branch_diff.load_buffers(cx);
|
||||
(branch_diff.repo().cloned(), load_buffers)
|
||||
pub fn all_entries(&self, cx: &App) -> Vec<StatusEntry> {
|
||||
let Some(ref repo) = self.branch_diff.read(cx).repo else {
|
||||
return Vec::new();
|
||||
};
|
||||
repo.read(cx).cached_status().collect()
|
||||
}
|
||||
|
||||
pub fn entries(&self, cx: &App) -> Option<impl Iterator<Item = StatusEntry>> {
|
||||
Some(
|
||||
self.branch_diff
|
||||
.read(cx)
|
||||
.repo
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.cached_status(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn first_n_entries(&self, cx: &App, n: usize) -> VecDeque<StatusEntry> {
|
||||
let Some(ref repo) = self.branch_diff.read(cx).repo else {
|
||||
return VecDeque::new();
|
||||
};
|
||||
repo.read(cx).cached_status().take(n).collect()
|
||||
}
|
||||
|
||||
pub async fn refresh_one(
|
||||
this: WeakEntity<Self>,
|
||||
repo_path: RepoPath,
|
||||
status: FileStatus,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
use git_store::branch_diff::BranchDiff;
|
||||
|
||||
let Some(this) = this.upgrade() else {
|
||||
return Ok(());
|
||||
};
|
||||
let multibuffer = cx.read_entity(&this, |this, _| this.multibuffer.clone())?;
|
||||
let branch_diff = cx.read_entity(&this, |pd, _| pd.branch_diff.clone())?;
|
||||
|
||||
let Some(repo) = cx.read_entity(&branch_diff, |bd, _| bd.repo.clone())? else {
|
||||
return Ok(());
|
||||
};
|
||||
let project = cx.read_entity(&branch_diff, |bd, _| bd.project.clone())?;
|
||||
|
||||
let mut previous_paths =
|
||||
cx.read_entity(&multibuffer, |mb, _| mb.paths().collect::<HashSet<_>>())?;
|
||||
|
||||
let tree_diff_status = cx.read_entity(&branch_diff, |branch_diff, _| {
|
||||
branch_diff
|
||||
.tree_diff
|
||||
.as_ref()
|
||||
.and_then(|t| t.entries.get(&repo_path))
|
||||
.cloned()
|
||||
})?;
|
||||
|
||||
let Some(status) = cx.read_entity(&branch_diff, |bd, _| {
|
||||
bd.merge_statuses(Some(status), tree_diff_status.as_ref())
|
||||
})?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
if !status.has_changes() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(project_path) = cx.read_entity(&repo, |repo, cx| {
|
||||
repo.repo_path_to_project_path(&repo_path, cx)
|
||||
})?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let sort_prefix =
|
||||
cx.read_entity(&repo, |repo, cx| sort_prefix(repo, &repo_path, status, cx))?;
|
||||
|
||||
let path_key = PathKey::with_sort_prefix(sort_prefix, repo_path.into_arc());
|
||||
previous_paths.remove(&path_key);
|
||||
|
||||
let repo = repo.clone();
|
||||
let Some((buffer, diff)) = BranchDiff::load_buffer(
|
||||
tree_diff_status,
|
||||
project_path,
|
||||
repo,
|
||||
project.downgrade(),
|
||||
&mut cx.to_app(),
|
||||
)
|
||||
.await
|
||||
.log_err() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.register_buffer(path_key, status, buffer, diff, window, cx)
|
||||
});
|
||||
let mut previous_paths = this.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||
})?;
|
||||
|
||||
if let Some(repo) = repo {
|
||||
let repo = repo.read(cx);
|
||||
// TODO LL clear multibuff on open?
|
||||
// // remove anything not part of the diff in the multibuffer
|
||||
// this.update(cx, |this, cx| {
|
||||
// multibuffer.update(cx, |multibuffer, cx| {
|
||||
// for path in previous_paths {
|
||||
// this.buffer_diff_subscriptions.remove(&path.path);
|
||||
// multibuffer.remove_excerpts_for_path(path, cx);
|
||||
// }
|
||||
// });
|
||||
// })?;
|
||||
|
||||
path_keys = Vec::with_capacity(buffers_to_load.len());
|
||||
for entry in buffers_to_load.iter() {
|
||||
let sort_prefix = sort_prefix(&repo, &entry.repo_path, entry.file_status, cx);
|
||||
let path_key =
|
||||
PathKey::with_sort_prefix(sort_prefix, entry.repo_path.as_ref().clone());
|
||||
previous_paths.remove(&path_key);
|
||||
path_keys.push(path_key)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn refresh(
|
||||
this: WeakEntity<Self>,
|
||||
cached_status: Vec<StatusEntry>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
dbg!("refreshing all");
|
||||
use git_store::branch_diff::BranchDiff;
|
||||
let Some(this) = this.upgrade() else {
|
||||
return Ok(());
|
||||
};
|
||||
let multibuffer = cx.read_entity(&this, |this, _| this.multibuffer.clone())?;
|
||||
let branch_diff = cx.read_entity(&this, |pd, _| pd.branch_diff.clone())?;
|
||||
|
||||
let Some(repo) = cx.read_entity(&branch_diff, |bd, _| bd.repo.clone())? else {
|
||||
return Ok(());
|
||||
};
|
||||
let project = cx.read_entity(&branch_diff, |bd, _| bd.project.clone())?;
|
||||
|
||||
let mut previous_paths =
|
||||
cx.read_entity(&multibuffer, |mb, _| mb.paths().collect::<HashSet<_>>())?;
|
||||
|
||||
// Idea: on click in git panel prioritize task for that file in some way ...
|
||||
// could have a hashmap of futures here
|
||||
// - needs to prioritize *some* background tasks over others
|
||||
// -
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
let mut seen = HashSet::default();
|
||||
for entry in cached_status {
|
||||
seen.insert(entry.repo_path.clone());
|
||||
let tree_diff_status = cx.read_entity(&branch_diff, |branch_diff, _| {
|
||||
branch_diff
|
||||
.tree_diff
|
||||
.as_ref()
|
||||
.and_then(|t| t.entries.get(&entry.repo_path))
|
||||
.cloned()
|
||||
})?;
|
||||
|
||||
let Some(status) = cx.read_entity(&branch_diff, |bd, _| {
|
||||
bd.merge_statuses(Some(entry.status), tree_diff_status.as_ref())
|
||||
})?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if !status.has_changes() {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.multibuffer.update(cx, |multibuffer, cx| {
|
||||
let Some(project_path) = cx.read_entity(&repo, |repo, cx| {
|
||||
repo.repo_path_to_project_path(&entry.repo_path, cx)
|
||||
})?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let sort_prefix = cx.read_entity(&repo, |repo, cx| {
|
||||
sort_prefix(repo, &entry.repo_path, entry.status, cx)
|
||||
})?;
|
||||
|
||||
let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.into_arc());
|
||||
previous_paths.remove(&path_key);
|
||||
|
||||
let repo = repo.clone();
|
||||
let project = project.downgrade();
|
||||
let task = cx.spawn(async move |cx| {
|
||||
let res = BranchDiff::load_buffer(
|
||||
tree_diff_status,
|
||||
project_path,
|
||||
repo,
|
||||
project,
|
||||
&mut cx.to_app(),
|
||||
)
|
||||
.await;
|
||||
(res, path_key, entry.status)
|
||||
});
|
||||
|
||||
tasks.push(task)
|
||||
}
|
||||
|
||||
// remove anything not part of the diff in the multibuffer
|
||||
this.update(cx, |this, cx| {
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
for path in previous_paths {
|
||||
this.buffer_diff_subscriptions.remove(&path.path);
|
||||
multibuffer.remove_excerpts_for_path(path, cx);
|
||||
}
|
||||
});
|
||||
buffers_to_load
|
||||
})?;
|
||||
|
||||
for (entry, path_key) in buffers_to_load.into_iter().zip(path_keys.into_iter()) {
|
||||
if let Some((buffer, diff)) = entry.load.await.log_err() {
|
||||
// add the new buffers as they are parsed
|
||||
let mut last_notify = Instant::now();
|
||||
while let Some((res, path_key, file_status)) = tasks.next().await {
|
||||
if let Some((buffer, diff)) = res.log_err() {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.register_buffer(path_key, entry.file_status, buffer, diff, window, cx)
|
||||
})
|
||||
.ok();
|
||||
this.register_buffer(path_key, file_status, buffer, diff, window, cx)
|
||||
});
|
||||
})?;
|
||||
}
|
||||
|
||||
if last_notify.elapsed().as_millis() > 100 {
|
||||
cx.update_entity(&this, |_, cx| cx.notify())?;
|
||||
last_notify = Instant::now();
|
||||
}
|
||||
}
|
||||
this.update(cx, |this, cx| {
|
||||
this.pending_scroll.take();
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
249
crates/git_ui/src/project_diff/diff_loader.rs
Normal file
249
crates/git_ui/src/project_diff/diff_loader.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
//! Task which updates the project diff multibuffer without putting too much
|
||||
//! pressure on the frontend executor. It prioritizes loading the area around the user
|
||||
|
||||
use collections::HashSet;
|
||||
use db::smol::stream::StreamExt;
|
||||
use futures::channel::mpsc;
|
||||
use gpui::{AppContext, AsyncWindowContext, Entity, Task, WeakEntity};
|
||||
use project::git_store::StatusEntry;
|
||||
use ui::{App, Window};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{git_panel::GitStatusEntry, project_diff::ProjectDiff};
|
||||
|
||||
enum Update {
|
||||
Position(usize),
|
||||
NewFile(StatusEntry),
|
||||
ListChanged,
|
||||
// should not need to handle re-ordering (sorting) here.
|
||||
// something to handle scroll? or should that live in the project diff?
|
||||
}
|
||||
|
||||
struct LoaderHandle {
|
||||
task: Task<Option<()>>,
|
||||
sender: mpsc::UnboundedSender<Update>,
|
||||
}
|
||||
|
||||
impl LoaderHandle {
|
||||
pub fn update_file_list(&self) {
|
||||
let _ = self
|
||||
.sender
|
||||
.unbounded_send(Update::ListChanged)
|
||||
.log_err();
|
||||
|
||||
}
|
||||
pub fn update_pos(&self, pos: usize) {
|
||||
let _ = self
|
||||
.sender
|
||||
.unbounded_send(Update::Position((pos)))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_loader(project_diff: Entity<ProjectDiff>, window: &Window, cx: &App) -> LoaderHandle {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
|
||||
let task = window.spawn(cx, async move |cx| {
|
||||
load(rx, project_diff.downgrade(), cx).await
|
||||
});
|
||||
LoaderHandle { task, sender: tx }
|
||||
}
|
||||
|
||||
enum DiffEntry {
|
||||
Loading(GitStatusEntry),
|
||||
Loaded(GitStatusEntry),
|
||||
Queued(GitStatusEntry),
|
||||
}
|
||||
|
||||
impl DiffEntry {
|
||||
fn queued(&self) -> bool {
|
||||
matches!(self, DiffEntry::Queued(_))
|
||||
}
|
||||
}
|
||||
|
||||
async fn load(
|
||||
rx: mpsc::UnboundedReceiver<Update>,
|
||||
project_diff: WeakEntity<ProjectDiff>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Option<()> {
|
||||
// let initial_entries = cx.read_entity(&cx.entity(), |project_diff, cx| project_diff.first_n_entries(cx, 100));
|
||||
// let loading = to_load.drain(..100).map(|| refresh_one)
|
||||
let mut existing = Vec::new();
|
||||
|
||||
loop {
|
||||
let update = rx.next().await?;
|
||||
match update {
|
||||
Update::Position(pos) => {
|
||||
if existing.get(pos).is_some_and(|diff| diff.queued()) {
|
||||
todo!("append to future unordered, also load in the bit
|
||||
around (maybe with a short sleep ahead so we get some sense
|
||||
of 'priority'")
|
||||
}
|
||||
// drop whatever is loading so we get to the new bit earlier
|
||||
}
|
||||
Update::NewFile(status_entry) => todo!(),
|
||||
Update::ListChanged => {
|
||||
let (added, removed) = project_diff
|
||||
.upgrade()?
|
||||
.read_with(cx, |diff, cx| diff_current_list(&existing, diff, cx))
|
||||
.ok()?;
|
||||
}
|
||||
}
|
||||
|
||||
// wait for Update OR Load done
|
||||
// -> Immediately spawn update
|
||||
// OR
|
||||
// -> spawn next group
|
||||
}
|
||||
}
|
||||
|
||||
// could be new list
|
||||
fn diff_current_list(
|
||||
existing_entries: &[GitStatusEntry],
|
||||
project_diff: &ProjectDiff,
|
||||
cx: &App,
|
||||
) -> (Vec<(usize, GitStatusEntry)>, Vec<usize>) {
|
||||
let Some(new_entries) = project_diff.entries(cx) else {
|
||||
return (Vec::new(), Vec::new());
|
||||
};
|
||||
|
||||
let existing_entries = existing_entries.iter().enumerate();
|
||||
for entry in new_entries {
|
||||
let Some((idx, existing)) = existing_entries.next() else {
|
||||
todo!();
|
||||
};
|
||||
|
||||
if existing == entry {
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// let initial_entries = cx.read_entity(&cx.entity(), |project_diff, cx| project_diff.first_n_entries(cx, 100));
|
||||
// let loading = to_load.drain(..100).map(|| refresh_one)
|
||||
}
|
||||
|
||||
// // remove anything not part of the diff in the multibuffer
|
||||
// fn remove_anything_not_being_loaded() {
|
||||
// this.update(cx, |this, cx| {
|
||||
// multibuffer.update(cx, |multibuffer, cx| {
|
||||
// for path in previous_paths {
|
||||
// this.buffer_diff_subscriptions.remove(&path.path);
|
||||
// multibuffer.remove_excerpts_for_path(path, cx);
|
||||
// }
|
||||
// });
|
||||
// })?;
|
||||
// }
|
||||
|
||||
pub async fn refresh_group(
|
||||
this: WeakEntity<ProjectDiff>,
|
||||
cached_status: Vec<StatusEntry>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> anyhow::Result<()> {
|
||||
dbg!("refreshing all");
|
||||
use project::git_store::branch_diff::BranchDiff;
|
||||
let Some(this) = this.upgrade() else {
|
||||
return Ok(());
|
||||
};
|
||||
let multibuffer = cx.read_entity(&this, |this, _| this.multibuffer.clone())?;
|
||||
let branch_diff = cx.read_entity(&this, |pd, _| pd.branch_diff.clone())?;
|
||||
|
||||
let Some(repo) = cx.read_entity(&branch_diff, |bd, _| bd.repo.clone())? else {
|
||||
return Ok(());
|
||||
};
|
||||
let project = cx.read_entity(&branch_diff, |bd, _| bd.project.clone())?;
|
||||
|
||||
let mut previous_paths =
|
||||
cx.read_entity(&multibuffer, |mb, _| mb.paths().collect::<HashSet<_>>())?;
|
||||
|
||||
// Idea: on click in git panel prioritize task for that file in some way ...
|
||||
// could have a hashmap of futures here
|
||||
// - needs to prioritize *some* background tasks over others
|
||||
// -
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
let mut seen = HashSet::default();
|
||||
for entry in cached_status {
|
||||
seen.insert(entry.repo_path.clone());
|
||||
let tree_diff_status = cx.read_entity(&branch_diff, |branch_diff, _| {
|
||||
branch_diff
|
||||
.tree_diff
|
||||
.as_ref()
|
||||
.and_then(|t| t.entries.get(&entry.repo_path))
|
||||
.cloned()
|
||||
})?;
|
||||
|
||||
let Some(status) = cx.read_entity(&branch_diff, |bd, _| {
|
||||
bd.merge_statuses(Some(entry.status), tree_diff_status.as_ref())
|
||||
})?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if !status.has_changes() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(project_path) = cx.read_entity(&repo, |repo, cx| {
|
||||
repo.repo_path_to_project_path(&entry.repo_path, cx)
|
||||
})?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let sort_prefix = cx.read_entity(&repo, |repo, cx| {
|
||||
sort_prefix(repo, &entry.repo_path, entry.status, cx)
|
||||
})?;
|
||||
|
||||
let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.into_arc());
|
||||
previous_paths.remove(&path_key);
|
||||
|
||||
let repo = repo.clone();
|
||||
let project = project.downgrade();
|
||||
let task = cx.spawn(async move |cx| {
|
||||
let res = BranchDiff::load_buffer(
|
||||
tree_diff_status,
|
||||
project_path,
|
||||
repo,
|
||||
project,
|
||||
&mut cx.to_app(),
|
||||
)
|
||||
.await;
|
||||
(res, path_key, entry.status)
|
||||
});
|
||||
|
||||
tasks.push(task)
|
||||
}
|
||||
|
||||
// remove anything not part of the diff in the multibuffer
|
||||
this.update(cx, |this, cx| {
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
for path in previous_paths {
|
||||
this.buffer_diff_subscriptions.remove(&path.path);
|
||||
multibuffer.remove_excerpts_for_path(path, cx);
|
||||
}
|
||||
});
|
||||
})?;
|
||||
|
||||
// add the new buffers as they are parsed
|
||||
let mut last_notify = Instant::now();
|
||||
while let Some((res, path_key, file_status)) = tasks.next().await {
|
||||
if let Some((buffer, diff)) = res.log_err() {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.register_buffer(path_key, file_status, buffer, diff, window, cx)
|
||||
});
|
||||
})?;
|
||||
}
|
||||
|
||||
if last_notify.elapsed().as_millis() > 100 {
|
||||
cx.update_entity(&this, |_, cx| cx.notify())?;
|
||||
last_notify = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn sort_or_collapse_changed() {
|
||||
todo!()
|
||||
}
|
||||
@@ -187,12 +187,13 @@ font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "11052312
|
||||
"source-fontconfig-dlopen",
|
||||
], optional = true }
|
||||
|
||||
calloop = { version = "0.13.0" }
|
||||
calloop = { version = "0.14.3" }
|
||||
filedescriptor = { version = "0.8.2", optional = true }
|
||||
open = { version = "5.2.0", optional = true }
|
||||
|
||||
|
||||
# Wayland
|
||||
calloop-wayland-source = { version = "0.3.0", optional = true }
|
||||
calloop-wayland-source = { version = "0.4.1", optional = true }
|
||||
wayland-backend = { version = "0.3.3", features = [
|
||||
"client_system",
|
||||
"dlopen",
|
||||
@@ -265,7 +266,6 @@ naga.workspace = true
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies]
|
||||
naga.workspace = true
|
||||
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
path = "examples/hello_world.rs"
|
||||
|
||||
@@ -63,4 +63,4 @@ In addition to the systems above, GPUI provides a range of smaller services that
|
||||
|
||||
- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details.
|
||||
|
||||
Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
|
||||
Currently, the best way to learn about these APIs is to read the Zed source code or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
|
||||
|
||||
@@ -2400,10 +2400,6 @@ impl HttpClient for NullHttpClient {
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
/// A mutable reference to an entity owned by GPUI
|
||||
|
||||
@@ -310,6 +310,11 @@ impl AsyncWindowContext {
|
||||
.update(self, |_, window, cx| read(cx.global(), window, cx))
|
||||
}
|
||||
|
||||
/// Returns an `AsyncApp` by cloning the one used by Self
|
||||
pub fn to_app(&self) -> AsyncApp {
|
||||
self.app.clone()
|
||||
}
|
||||
|
||||
/// A convenience method for [`App::update_global`](BorrowAppContext::update_global).
|
||||
/// for updating the global state of the specified type.
|
||||
pub fn update_global<G, R>(
|
||||
|
||||
@@ -233,6 +233,9 @@ impl<'a, T: 'static> Context<'a, T> {
|
||||
/// Spawn the future returned by the given function.
|
||||
/// The function is provided a weak handle to the entity owned by this context and a context that can be held across await points.
|
||||
/// The returned task must be held or detached.
|
||||
///
|
||||
/// # Example
|
||||
/// `cx.spawn(async move |some_weak_entity, cx| ...)`
|
||||
#[track_caller]
|
||||
pub fn spawn<AsyncFn, R>(&self, f: AsyncFn) -> Task<R>
|
||||
where
|
||||
|
||||
@@ -305,9 +305,10 @@ pub enum KeyboardButton {
|
||||
}
|
||||
|
||||
/// An enum representing the mouse button that was pressed.
|
||||
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[derive(Hash, Default, PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub enum MouseButton {
|
||||
/// The left mouse button.
|
||||
#[default]
|
||||
Left,
|
||||
|
||||
/// The right mouse button.
|
||||
@@ -333,28 +334,17 @@ impl MouseButton {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MouseButton {
|
||||
fn default() -> Self {
|
||||
Self::Left
|
||||
}
|
||||
}
|
||||
|
||||
/// A navigation direction, such as back or forward.
|
||||
#[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[derive(Hash, Default, PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub enum NavigationDirection {
|
||||
/// The back button.
|
||||
#[default]
|
||||
Back,
|
||||
|
||||
/// The forward button.
|
||||
Forward,
|
||||
}
|
||||
|
||||
impl Default for NavigationDirection {
|
||||
fn default() -> Self {
|
||||
Self::Back
|
||||
}
|
||||
}
|
||||
|
||||
/// A mouse move event from the platform.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MouseMoveEvent {
|
||||
@@ -519,7 +509,7 @@ impl Deref for MouseExitEvent {
|
||||
}
|
||||
|
||||
/// A collection of paths from the platform, such as from a file drop.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
|
||||
|
||||
impl ExternalPaths {
|
||||
|
||||
@@ -1346,11 +1346,12 @@ pub enum WindowKind {
|
||||
///
|
||||
/// On macOS, this corresponds to named [`NSAppearance`](https://developer.apple.com/documentation/appkit/nsappearance)
|
||||
/// values.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum WindowAppearance {
|
||||
/// A light appearance.
|
||||
///
|
||||
/// On macOS, this corresponds to the `aqua` appearance.
|
||||
#[default]
|
||||
Light,
|
||||
|
||||
/// A light appearance with vibrant colors.
|
||||
@@ -1369,12 +1370,6 @@ pub enum WindowAppearance {
|
||||
VibrantDark,
|
||||
}
|
||||
|
||||
impl Default for WindowAppearance {
|
||||
fn default() -> Self {
|
||||
Self::Light
|
||||
}
|
||||
}
|
||||
|
||||
/// The appearance of the background of the window itself, when there is
|
||||
/// no content or the content is transparent.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
@@ -1475,9 +1470,10 @@ impl From<&str> for PromptButton {
|
||||
}
|
||||
|
||||
/// The style of the cursor (pointer)
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum CursorStyle {
|
||||
/// The default cursor
|
||||
#[default]
|
||||
Arrow,
|
||||
|
||||
/// A text input cursor
|
||||
@@ -1564,12 +1560,6 @@ pub enum CursorStyle {
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for CursorStyle {
|
||||
fn default() -> Self {
|
||||
Self::Arrow
|
||||
}
|
||||
}
|
||||
|
||||
/// A clipboard item that should be copied to the clipboard
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ClipboardItem {
|
||||
@@ -1583,6 +1573,8 @@ pub enum ClipboardEntry {
|
||||
String(ClipboardString),
|
||||
/// An image entry
|
||||
Image(Image),
|
||||
/// A file entry
|
||||
ExternalPaths(crate::ExternalPaths),
|
||||
}
|
||||
|
||||
impl ClipboardItem {
|
||||
@@ -1623,16 +1615,29 @@ impl ClipboardItem {
|
||||
/// Returns None if there were no ClipboardString entries.
|
||||
pub fn text(&self) -> Option<String> {
|
||||
let mut answer = String::new();
|
||||
let mut any_entries = false;
|
||||
|
||||
for entry in self.entries.iter() {
|
||||
if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
|
||||
answer.push_str(text);
|
||||
any_entries = true;
|
||||
}
|
||||
}
|
||||
|
||||
if any_entries { Some(answer) } else { None }
|
||||
if answer.is_empty() {
|
||||
for entry in self.entries.iter() {
|
||||
if let ClipboardEntry::ExternalPaths(paths) = entry {
|
||||
for path in &paths.0 {
|
||||
use std::fmt::Write as _;
|
||||
_ = write!(answer, "{}", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !answer.is_empty() {
|
||||
Some(answer)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// If this item is one ClipboardEntry::String, returns its metadata.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -18,6 +17,7 @@ use anyhow::{Context as _, anyhow};
|
||||
use calloop::{LoopSignal, channel::Channel};
|
||||
use futures::channel::oneshot;
|
||||
use util::ResultExt as _;
|
||||
use util::command::{new_smol_command, new_std_command};
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||
|
||||
@@ -215,7 +215,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
clippy::disallowed_methods,
|
||||
reason = "We are restarting ourselves, using std command thus is fine"
|
||||
)]
|
||||
let restart_process = Command::new("/usr/bin/env")
|
||||
let restart_process = new_std_command("/usr/bin/env")
|
||||
.arg("bash")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
@@ -422,7 +422,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
let path = path.to_owned();
|
||||
self.background_executor()
|
||||
.spawn(async move {
|
||||
let _ = smol::process::Command::new("xdg-open")
|
||||
let _ = new_smol_command("xdg-open")
|
||||
.arg(path)
|
||||
.spawn()
|
||||
.context("invoking xdg-open")
|
||||
|
||||
@@ -487,12 +487,15 @@ impl WaylandClient {
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let handle = event_loop.handle();
|
||||
let handle = event_loop.handle(); // CHECK that wayland sources get higher prio
|
||||
handle
|
||||
// these are all tasks spawned on the foreground executor.
|
||||
// There is no concept of priority, they are all equal.
|
||||
.insert_source(main_receiver, {
|
||||
let handle = handle.clone();
|
||||
move |event, _, _: &mut WaylandClientStatePtr| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
// will only be called when the event loop has finished processing all pending events from the sources
|
||||
handle.insert_idle(|_| {
|
||||
let start = Instant::now();
|
||||
let mut timing = match runnable {
|
||||
@@ -650,6 +653,7 @@ impl WaylandClient {
|
||||
event_loop: Some(event_loop),
|
||||
}));
|
||||
|
||||
// MAGIC HERE IT IS
|
||||
WaylandSource::new(conn, event_queue)
|
||||
.insert(handle)
|
||||
.unwrap();
|
||||
@@ -1574,6 +1578,7 @@ fn linux_button_to_gpui(button: u32) -> Option<MouseButton> {
|
||||
})
|
||||
}
|
||||
|
||||
// how is this being called inside calloop
|
||||
impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
@@ -1664,7 +1669,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
modifiers: state.modifiers,
|
||||
});
|
||||
drop(state);
|
||||
window.handle_input(input);
|
||||
window.handle_input(input); // How does this get into the event loop?
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Button {
|
||||
|
||||
@@ -53,14 +53,16 @@ use std::{
|
||||
ffi::{CStr, OsStr, c_void},
|
||||
os::{raw::c_char, unix::ffi::OsStrExt},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
ptr,
|
||||
rc::Rc,
|
||||
slice, str,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use strum::IntoEnumIterator;
|
||||
use util::ResultExt;
|
||||
use util::{
|
||||
ResultExt,
|
||||
command::{new_smol_command, new_std_command},
|
||||
};
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
const NSUTF8StringEncoding: NSUInteger = 4;
|
||||
@@ -552,7 +554,7 @@ impl Platform for MacPlatform {
|
||||
clippy::disallowed_methods,
|
||||
reason = "We are restarting ourselves, using std command thus is fine"
|
||||
)]
|
||||
let restart_process = Command::new("/bin/bash")
|
||||
let restart_process = new_std_command("/bin/bash")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
.arg(app_pid)
|
||||
@@ -867,7 +869,7 @@ impl Platform for MacPlatform {
|
||||
.lock()
|
||||
.background_executor
|
||||
.spawn(async move {
|
||||
if let Some(mut child) = smol::process::Command::new("open")
|
||||
if let Some(mut child) = new_smol_command("open")
|
||||
.arg(path)
|
||||
.spawn()
|
||||
.context("invoking open command")
|
||||
@@ -1046,6 +1048,7 @@ impl Platform for MacPlatform {
|
||||
ClipboardEntry::Image(image) => {
|
||||
self.write_image_to_clipboard(image);
|
||||
}
|
||||
ClipboardEntry::ExternalPaths(_) => {}
|
||||
},
|
||||
None => {
|
||||
// Writing an empty list of entries just clears the clipboard.
|
||||
|
||||
@@ -1543,6 +1543,17 @@ impl PlatformWindow for MacWindow {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn start_window_move(&self) {
|
||||
let this = self.0.lock();
|
||||
let window = this.native_window;
|
||||
|
||||
unsafe {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
let mut event: id = msg_send![app, currentEvent];
|
||||
let _: () = msg_send![window, performWindowDragWithEvent: event];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for MacWindow {
|
||||
@@ -1967,10 +1978,36 @@ extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the window scale factor and drawable size, and call the resize callback if any.
|
||||
fn update_window_scale_factor(window_state: &Arc<Mutex<MacWindowState>>) {
|
||||
let mut lock = window_state.as_ref().lock();
|
||||
let scale_factor = lock.scale_factor();
|
||||
let size = lock.content_size();
|
||||
let drawable_size = size.to_device_pixels(scale_factor);
|
||||
unsafe {
|
||||
let _: () = msg_send![
|
||||
lock.renderer.layer(),
|
||||
setContentsScale: scale_factor as f64
|
||||
];
|
||||
}
|
||||
|
||||
lock.renderer.update_drawable_size(drawable_size);
|
||||
|
||||
if let Some(mut callback) = lock.resize_callback.take() {
|
||||
let content_size = lock.content_size();
|
||||
let scale_factor = lock.scale_factor();
|
||||
drop(lock);
|
||||
callback(content_size, scale_factor);
|
||||
window_state.as_ref().lock().resize_callback = Some(callback);
|
||||
};
|
||||
}
|
||||
|
||||
extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let mut lock = window_state.as_ref().lock();
|
||||
lock.start_display_link();
|
||||
drop(lock);
|
||||
update_window_scale_factor(&window_state);
|
||||
}
|
||||
|
||||
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
|
||||
@@ -2079,27 +2116,7 @@ extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id {
|
||||
|
||||
extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) {
|
||||
let window_state = unsafe { get_window_state(this) };
|
||||
let mut lock = window_state.as_ref().lock();
|
||||
|
||||
let scale_factor = lock.scale_factor();
|
||||
let size = lock.content_size();
|
||||
let drawable_size = size.to_device_pixels(scale_factor);
|
||||
unsafe {
|
||||
let _: () = msg_send![
|
||||
lock.renderer.layer(),
|
||||
setContentsScale: scale_factor as f64
|
||||
];
|
||||
}
|
||||
|
||||
lock.renderer.update_drawable_size(drawable_size);
|
||||
|
||||
if let Some(mut callback) = lock.resize_callback.take() {
|
||||
let content_size = lock.content_size();
|
||||
let scale_factor = lock.scale_factor();
|
||||
drop(lock);
|
||||
callback(content_size, scale_factor);
|
||||
window_state.as_ref().lock().resize_callback = Some(callback);
|
||||
};
|
||||
update_window_scale_factor(&window_state);
|
||||
}
|
||||
|
||||
extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{FxHashMap, FxHashSet};
|
||||
use collections::FxHashMap;
|
||||
use itertools::Itertools;
|
||||
use windows::Win32::{
|
||||
Foundation::{HANDLE, HGLOBAL},
|
||||
@@ -18,7 +18,9 @@ use windows::Win32::{
|
||||
};
|
||||
use windows_core::PCWSTR;
|
||||
|
||||
use crate::{ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, hash};
|
||||
use crate::{
|
||||
ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash,
|
||||
};
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
||||
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
|
||||
@@ -48,16 +50,6 @@ static FORMATS_MAP: LazyLock<FxHashMap<u32, ClipboardFormatType>> = LazyLock::ne
|
||||
formats_map.insert(CF_HDROP.0 as u32, ClipboardFormatType::Files);
|
||||
formats_map
|
||||
});
|
||||
static FORMATS_SET: LazyLock<FxHashSet<u32>> = LazyLock::new(|| {
|
||||
let mut formats_map = FxHashSet::default();
|
||||
formats_map.insert(CF_UNICODETEXT.0 as u32);
|
||||
formats_map.insert(*CLIPBOARD_PNG_FORMAT);
|
||||
formats_map.insert(*CLIPBOARD_GIF_FORMAT);
|
||||
formats_map.insert(*CLIPBOARD_JPG_FORMAT);
|
||||
formats_map.insert(*CLIPBOARD_SVG_FORMAT);
|
||||
formats_map.insert(CF_HDROP.0 as u32);
|
||||
formats_map
|
||||
});
|
||||
static IMAGE_FORMATS_MAP: LazyLock<FxHashMap<u32, ImageFormat>> = LazyLock::new(|| {
|
||||
let mut formats_map = FxHashMap::default();
|
||||
formats_map.insert(*CLIPBOARD_PNG_FORMAT, ImageFormat::Png);
|
||||
@@ -138,6 +130,11 @@ fn register_clipboard_format(format: PCWSTR) -> u32 {
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
log::debug!(
|
||||
"Registered clipboard format {} as {}",
|
||||
unsafe { format.display() },
|
||||
ret
|
||||
);
|
||||
ret
|
||||
}
|
||||
|
||||
@@ -159,6 +156,7 @@ fn write_to_clipboard_inner(item: ClipboardItem) -> Result<()> {
|
||||
ClipboardEntry::Image(image) => {
|
||||
write_image_to_clipboard(image)?;
|
||||
}
|
||||
ClipboardEntry::ExternalPaths(_) => {}
|
||||
},
|
||||
None => {
|
||||
// Writing an empty list of entries just clears the clipboard.
|
||||
@@ -249,19 +247,33 @@ fn with_best_match_format<F>(f: F) -> Option<ClipboardItem>
|
||||
where
|
||||
F: Fn(u32) -> Option<ClipboardEntry>,
|
||||
{
|
||||
let mut text = None;
|
||||
let mut image = None;
|
||||
let mut files = None;
|
||||
let count = unsafe { CountClipboardFormats() };
|
||||
let mut clipboard_format = 0;
|
||||
for _ in 0..count {
|
||||
clipboard_format = unsafe { EnumClipboardFormats(clipboard_format) };
|
||||
let Some(item_format) = FORMATS_SET.get(&clipboard_format) else {
|
||||
let Some(item_format) = FORMATS_MAP.get(&clipboard_format) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(entry) = f(*item_format) {
|
||||
return Some(ClipboardItem {
|
||||
entries: vec![entry],
|
||||
});
|
||||
let bucket = match item_format {
|
||||
ClipboardFormatType::Text if text.is_none() => &mut text,
|
||||
ClipboardFormatType::Image if image.is_none() => &mut image,
|
||||
ClipboardFormatType::Files if files.is_none() => &mut files,
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(entry) = f(clipboard_format) {
|
||||
*bucket = Some(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(entry) = [image, files, text].into_iter().flatten().next() {
|
||||
return Some(ClipboardItem {
|
||||
entries: vec![entry],
|
||||
});
|
||||
}
|
||||
|
||||
// log the formats that we don't support yet.
|
||||
{
|
||||
clipboard_format = 0;
|
||||
@@ -346,18 +358,17 @@ fn read_image_for_type(format_number: u32, format: ImageFormat) -> Option<Clipbo
|
||||
}
|
||||
|
||||
fn read_files_from_clipboard() -> Option<ClipboardEntry> {
|
||||
let text = with_clipboard_data(CF_HDROP.0 as u32, |data_ptr, _size| {
|
||||
let filenames = with_clipboard_data(CF_HDROP.0 as u32, |data_ptr, _size| {
|
||||
let hdrop = HDROP(data_ptr);
|
||||
let mut filenames = String::new();
|
||||
let mut filenames = Vec::new();
|
||||
with_file_names(hdrop, |file_name| {
|
||||
filenames.push_str(&file_name);
|
||||
filenames.push(std::path::PathBuf::from(file_name));
|
||||
});
|
||||
filenames
|
||||
})?;
|
||||
Some(ClipboardEntry::String(ClipboardString {
|
||||
text,
|
||||
metadata: None,
|
||||
}))
|
||||
Some(ClipboardEntry::ExternalPaths(ExternalPaths(
|
||||
filenames.into(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn with_clipboard_data<F, R>(format: u32, f: F) -> Option<R>
|
||||
|
||||
@@ -234,11 +234,14 @@ impl DirectXAtlasState {
|
||||
}
|
||||
|
||||
fn texture(&self, id: AtlasTextureId) -> &DirectXAtlasTexture {
|
||||
let textures = match id.kind {
|
||||
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
|
||||
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
|
||||
};
|
||||
textures[id.index as usize].as_ref().unwrap()
|
||||
match id.kind {
|
||||
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures[id.index as usize]
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures[id.index as usize]
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ pub(crate) struct DirectXRenderer {
|
||||
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
/// Whether we want to skip drwaing due to device lost events.
|
||||
///
|
||||
/// In that case we want to discard the first frame that we draw as we got reset in the middle of a frame
|
||||
/// meaning we lost all the allocated gpu textures and scene resources.
|
||||
skip_draws: bool,
|
||||
}
|
||||
|
||||
/// Direct3D objects
|
||||
@@ -167,6 +173,7 @@ impl DirectXRenderer {
|
||||
font_info: Self::get_font_info(),
|
||||
width: 1,
|
||||
height: 1,
|
||||
skip_draws: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,8 +199,13 @@ impl DirectXRenderer {
|
||||
}],
|
||||
)?;
|
||||
unsafe {
|
||||
device_context
|
||||
.ClearRenderTargetView(resources.render_target_view.as_ref().unwrap(), &[0.0; 4]);
|
||||
device_context.ClearRenderTargetView(
|
||||
resources
|
||||
.render_target_view
|
||||
.as_ref()
|
||||
.context("missing render target view")?,
|
||||
&[0.0; 4],
|
||||
);
|
||||
device_context
|
||||
.OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None);
|
||||
device_context.RSSetViewports(Some(slice::from_ref(&resources.viewport)));
|
||||
@@ -283,10 +295,16 @@ impl DirectXRenderer {
|
||||
self.globals = globals;
|
||||
self.pipelines = pipelines;
|
||||
self.direct_composition = direct_composition;
|
||||
self.skip_draws = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn draw(&mut self, scene: &Scene) -> Result<()> {
|
||||
if self.skip_draws {
|
||||
// skip drawing this frame, we just recovered from a device lost event
|
||||
// and so likely do not have the textures anymore that are required for drawing
|
||||
return Ok(());
|
||||
}
|
||||
self.pre_draw()?;
|
||||
for batch in scene.batches() {
|
||||
match batch {
|
||||
@@ -306,14 +324,18 @@ impl DirectXRenderer {
|
||||
sprites,
|
||||
} => self.draw_polychrome_sprites(texture_id, sprites),
|
||||
PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(surfaces),
|
||||
}.context(format!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
|
||||
scene.paths.len(),
|
||||
scene.shadows.len(),
|
||||
scene.quads.len(),
|
||||
scene.underlines.len(),
|
||||
scene.monochrome_sprites.len(),
|
||||
scene.polychrome_sprites.len(),
|
||||
scene.surfaces.len(),))?;
|
||||
}
|
||||
.context(format!(
|
||||
"scene too large:\
|
||||
{} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
|
||||
scene.paths.len(),
|
||||
scene.shadows.len(),
|
||||
scene.quads.len(),
|
||||
scene.underlines.len(),
|
||||
scene.monochrome_sprites.len(),
|
||||
scene.polychrome_sprites.len(),
|
||||
scene.surfaces.len(),
|
||||
))?;
|
||||
}
|
||||
self.present()
|
||||
}
|
||||
@@ -352,6 +374,7 @@ impl DirectXRenderer {
|
||||
}
|
||||
|
||||
resources.recreate_resources(devices, width, height)?;
|
||||
|
||||
unsafe {
|
||||
devices
|
||||
.device_context
|
||||
@@ -647,6 +670,10 @@ impl DirectXRenderer {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn mark_drawable(&mut self) {
|
||||
self.skip_draws = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectXResources {
|
||||
|
||||
@@ -201,8 +201,10 @@ impl WindowsWindowInner {
|
||||
let new_logical_size = device_size.to_pixels(scale_factor);
|
||||
let mut lock = self.state.borrow_mut();
|
||||
lock.logical_size = new_logical_size;
|
||||
if should_resize_renderer {
|
||||
lock.renderer.resize(device_size).log_err();
|
||||
if should_resize_renderer && let Err(e) = lock.renderer.resize(device_size) {
|
||||
log::error!("Failed to resize renderer, invalidating devices: {}", e);
|
||||
lock.invalidate_devices
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
}
|
||||
if let Some(mut callback) = lock.callbacks.resize.take() {
|
||||
drop(lock);
|
||||
@@ -1138,6 +1140,11 @@ impl WindowsWindowInner {
|
||||
#[inline]
|
||||
fn draw_window(&self, handle: HWND, force_render: bool) -> Option<isize> {
|
||||
let mut request_frame = self.state.borrow_mut().callbacks.request_frame.take()?;
|
||||
|
||||
// we are instructing gpui to force render a frame, this will
|
||||
// re-populate all the gpu textures for us so we can resume drawing in
|
||||
// case we disabled drawing earlier due to a device loss
|
||||
self.state.borrow_mut().renderer.mark_drawable();
|
||||
request_frame(RequestFrameOptions {
|
||||
require_presentation: false,
|
||||
force_render,
|
||||
|
||||
@@ -3,7 +3,10 @@ use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
rc::{Rc, Weak},
|
||||
sync::{Arc, atomic::Ordering},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use ::util::{ResultExt, paths::SanitizedPath};
|
||||
@@ -36,6 +39,9 @@ pub(crate) struct WindowsPlatform {
|
||||
text_system: Arc<DirectWriteTextSystem>,
|
||||
windows_version: WindowsVersion,
|
||||
drop_target_helper: IDropTargetHelper,
|
||||
/// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices
|
||||
/// as resizing them has failed, causing us to have lost at least the render target.
|
||||
invalidate_devices: Arc<AtomicBool>,
|
||||
handle: HWND,
|
||||
disable_direct_composition: bool,
|
||||
}
|
||||
@@ -162,6 +168,7 @@ impl WindowsPlatform {
|
||||
disable_direct_composition,
|
||||
windows_version,
|
||||
drop_target_helper,
|
||||
invalidate_devices: Arc::new(AtomicBool::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,6 +202,7 @@ impl WindowsPlatform {
|
||||
platform_window_handle: self.handle,
|
||||
disable_direct_composition: self.disable_direct_composition,
|
||||
directx_devices: self.inner.state.borrow().directx_devices.clone().unwrap(),
|
||||
invalidate_devices: self.invalidate_devices.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,13 +255,17 @@ impl WindowsPlatform {
|
||||
let validation_number = self.inner.validation_number;
|
||||
let all_windows = Arc::downgrade(&self.raw_window_handles);
|
||||
let text_system = Arc::downgrade(&self.text_system);
|
||||
let invalidate_devices = self.invalidate_devices.clone();
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("VSyncProvider".to_owned())
|
||||
.spawn(move || {
|
||||
let vsync_provider = VSyncProvider::new();
|
||||
loop {
|
||||
vsync_provider.wait_for_vsync();
|
||||
if check_device_lost(&directx_device.device) {
|
||||
if check_device_lost(&directx_device.device)
|
||||
|| invalidate_devices.fetch_and(false, Ordering::Acquire)
|
||||
{
|
||||
if let Err(err) = handle_gpu_device_lost(
|
||||
&mut directx_device,
|
||||
platform_window.as_raw(),
|
||||
@@ -877,6 +889,9 @@ pub(crate) struct WindowCreationInfo {
|
||||
pub(crate) platform_window_handle: HWND,
|
||||
pub(crate) disable_direct_composition: bool,
|
||||
pub(crate) directx_devices: DirectXDevices,
|
||||
/// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices
|
||||
/// as resizing them has failed, causing us to have lost at least the render target.
|
||||
pub(crate) invalidate_devices: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
struct PlatformWindowCreateContext {
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
rc::{Rc, Weak},
|
||||
str::FromStr,
|
||||
sync::{Arc, Once},
|
||||
sync::{Arc, Once, atomic::AtomicBool},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
@@ -53,6 +53,9 @@ pub struct WindowsWindowState {
|
||||
pub nc_button_pressed: Option<u32>,
|
||||
|
||||
pub display: WindowsDisplay,
|
||||
/// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices
|
||||
/// as resizing them has failed, causing us to have lost at least the render target.
|
||||
pub invalidate_devices: Arc<AtomicBool>,
|
||||
fullscreen: Option<StyleAndBounds>,
|
||||
initial_placement: Option<WindowOpenStatus>,
|
||||
hwnd: HWND,
|
||||
@@ -83,6 +86,7 @@ impl WindowsWindowState {
|
||||
min_size: Option<Size<Pixels>>,
|
||||
appearance: WindowAppearance,
|
||||
disable_direct_composition: bool,
|
||||
invalidate_devices: Arc<AtomicBool>,
|
||||
) -> Result<Self> {
|
||||
let scale_factor = {
|
||||
let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
|
||||
@@ -138,6 +142,7 @@ impl WindowsWindowState {
|
||||
fullscreen,
|
||||
initial_placement,
|
||||
hwnd,
|
||||
invalidate_devices,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -211,6 +216,7 @@ impl WindowsWindowInner {
|
||||
context.min_size,
|
||||
context.appearance,
|
||||
context.disable_direct_composition,
|
||||
context.invalidate_devices.clone(),
|
||||
)?);
|
||||
|
||||
Ok(Rc::new(Self {
|
||||
@@ -361,6 +367,7 @@ struct WindowCreateContext {
|
||||
appearance: WindowAppearance,
|
||||
disable_direct_composition: bool,
|
||||
directx_devices: DirectXDevices,
|
||||
invalidate_devices: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl WindowsWindow {
|
||||
@@ -380,6 +387,7 @@ impl WindowsWindow {
|
||||
platform_window_handle,
|
||||
disable_direct_composition,
|
||||
directx_devices,
|
||||
invalidate_devices,
|
||||
} = creation_info;
|
||||
register_window_class(icon);
|
||||
let hide_title_bar = params
|
||||
@@ -440,6 +448,7 @@ impl WindowsWindow {
|
||||
appearance,
|
||||
disable_direct_composition,
|
||||
directx_devices,
|
||||
invalidate_devices,
|
||||
};
|
||||
let creation_result = unsafe {
|
||||
CreateWindowExW(
|
||||
|
||||
@@ -13,8 +13,9 @@ const ELLIPSIS: SharedString = SharedString::new_static("…");
|
||||
|
||||
/// A trait for elements that can be styled.
|
||||
/// Use this to opt-in to a utility CSS-like styling API.
|
||||
// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv
|
||||
#[cfg_attr(
|
||||
any(feature = "inspector", debug_assertions),
|
||||
all(any(feature = "inspector", debug_assertions), not(rust_analyzer)),
|
||||
gpui_macros::derive_inspector_reflection
|
||||
)]
|
||||
pub trait Styled: Sized {
|
||||
|
||||
@@ -320,7 +320,7 @@ mod tests {
|
||||
let focus_map = Arc::new(FocusMap::default());
|
||||
let mut tab_index_map = TabStopMap::default();
|
||||
|
||||
let focus_handles = vec![
|
||||
let focus_handles = [
|
||||
FocusHandle::new(&focus_map).tab_stop(true).tab_index(0),
|
||||
FocusHandle::new(&focus_map).tab_stop(true).tab_index(1),
|
||||
FocusHandle::new(&focus_map).tab_stop(true).tab_index(1),
|
||||
|
||||
@@ -1819,6 +1819,7 @@ impl Window {
|
||||
self.platform_window.show_window_menu(position)
|
||||
}
|
||||
|
||||
/// Handle window movement for Linux and macOS.
|
||||
/// Tells the compositor to take control of window movement (Wayland and X11)
|
||||
///
|
||||
/// Events may not be received during a move operation.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! This code was generated using Zed Agent with Claude Opus 4.
|
||||
|
||||
use gpui_macros::derive_inspector_reflection;
|
||||
|
||||
#[derive_inspector_reflection]
|
||||
// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv
|
||||
#[cfg_attr(not(rust_analyzer), gpui_macros::derive_inspector_reflection)]
|
||||
trait Transform: Clone {
|
||||
/// Doubles the value
|
||||
fn double(self) -> Self;
|
||||
|
||||
@@ -14,9 +14,9 @@ use futures::{
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "test-support")]
|
||||
use std::fmt;
|
||||
use std::{any::type_name, sync::Arc};
|
||||
use std::{any::type_name, fmt};
|
||||
pub use url::{Host, Url};
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -59,10 +59,10 @@ impl HttpRequestExt for http::request::Builder {
|
||||
}
|
||||
|
||||
pub trait HttpClient: 'static + Send + Sync {
|
||||
fn type_name(&self) -> &'static str;
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue>;
|
||||
|
||||
fn proxy(&self) -> Option<&Url>;
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<AsyncBody>,
|
||||
@@ -106,8 +106,6 @@ pub trait HttpClient: 'static + Send + Sync {
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
panic!("called as_fake on {}", type_name::<Self>())
|
||||
@@ -163,10 +161,6 @@ impl HttpClient for HttpClientWithProxy {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self.client.as_fake()
|
||||
@@ -182,19 +176,13 @@ impl HttpClient for HttpClientWithProxy {
|
||||
}
|
||||
|
||||
/// An [`HttpClient`] that has a base URL.
|
||||
#[derive(Deref)]
|
||||
pub struct HttpClientWithUrl {
|
||||
base_url: Mutex<String>,
|
||||
#[deref]
|
||||
client: HttpClientWithProxy,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for HttpClientWithUrl {
|
||||
type Target = HttpClientWithProxy;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClientWithUrl {
|
||||
/// Returns a new [`HttpClientWithUrl`] with the given base URL.
|
||||
pub fn new(
|
||||
@@ -314,10 +302,6 @@ impl HttpClient for HttpClientWithUrl {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
self.client.type_name()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self.client.as_fake()
|
||||
@@ -384,10 +368,6 @@ impl HttpClient for BlockedHttpClient {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
panic!("called as_fake on {}", type_name::<Self>())
|
||||
@@ -482,10 +462,6 @@ impl HttpClient for FakeHttpClient {
|
||||
None
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
type_name::<Self>()
|
||||
}
|
||||
|
||||
fn as_fake(&self) -> &FakeHttpClient {
|
||||
self
|
||||
}
|
||||
|
||||
@@ -217,6 +217,7 @@ pub enum IconName {
|
||||
SupermavenError,
|
||||
SupermavenInit,
|
||||
SwatchBook,
|
||||
SweepAi,
|
||||
Tab,
|
||||
Terminal,
|
||||
TerminalAlt,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user