Compare commits

..

4 Commits

Author SHA1 Message Date
dino
eebc03e0b5 test(vim): add test for minibrackets in multibuffer 2025-11-03 20:23:13 +00:00
dino
0a48bad901 refactor(vim): filter ranges before cover_or_next 2025-11-03 16:26:18 +00:00
dino
4b4dd36b4b fix(vim): fix issue with cover_or_next
The `vim::object::cover_or_next` function assumes that `range_filter`
also expects the ranges for a `MultiBufferSnapshot` instead of a single
buffer. This commit fixes the function so as to convert the ranges to
buffer ranges before calling range_filter.
2025-11-03 15:55:49 +00:00
dino
9cd297621a chore(vim): update find_mini_delimiters to use buffer range
This doesn't yet fix the user's issue, as it appears to not correctly be
copying the content inside the brackets, but it does appear to prevent
it from crashing as we're now converting the multibuffer's ranges to
buffer ranges.
2025-10-31 18:13:28 +00:00
299 changed files with 6879 additions and 13134 deletions

View File

@@ -1,39 +0,0 @@
# Generated from xtask::workflows::cherry_pick
# Rebuild with `cargo xtask workflows`.
name: cherry_pick
on:
workflow_dispatch:
inputs:
commit:
description: commit
required: true
type: string
branch:
description: branch
required: true
type: string
channel:
description: channel
required: true
type: string
jobs:
run_cherry_pick:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- id: get-app-token
name: cherry_pick::run_cherry_pick::authenticate_as_zippy
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
- name: cherry_pick::run_cherry_pick::cherry_pick
run: ./script/cherry-pick ${{ inputs.branch }} ${{ inputs.commit }} ${{ inputs.channel }}
shell: bash -euxo pipefail {0}
env:
GIT_COMMITTER_NAME: Zed Zippy
GIT_COMMITTER_EMAIL: hi@zed.dev
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}

841
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,841 @@
name: CI
on:
push:
tags:
- "v*"
concurrency:
# Allow only one workflow per any non-`main` branch.
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
jobs:
job_spec:
name: Decide which jobs to run
if: github.repository_owner == 'zed-industries'
outputs:
run_tests: ${{ steps.filter.outputs.run_tests }}
run_license: ${{ steps.filter.outputs.run_license }}
run_docs: ${{ steps.filter.outputs.run_docs }}
run_nix: ${{ steps.filter.outputs.run_nix }}
run_actionlint: ${{ steps.filter.outputs.run_actionlint }}
runs-on:
- namespace-profile-2x4-ubuntu-2404
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
# 350 is arbitrary; ~10days of history on main (5secs); full history is ~25secs
fetch-depth: ${{ github.ref == 'refs/heads/main' && 2 || 350 }}
- name: Fetch git history and generate output filters
id: filter
run: |
if [ -z "$GITHUB_BASE_REF" ]; then
echo "Not in a PR context (i.e., push to main/stable/preview)"
COMPARE_REV="$(git rev-parse HEAD~1)"
else
echo "In a PR context comparing to pull_request.base.ref"
git fetch origin "$GITHUB_BASE_REF" --depth=350
COMPARE_REV="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)"
fi
CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" ${{ github.sha }})"
# Specify anything which should potentially skip full test suite in this regex:
# - docs/
# - script/update_top_ranking_issues/
# - .github/ISSUE_TEMPLATE/
# - .github/workflows/ (except .github/workflows/ci.yml)
SKIP_REGEX='^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!ci)))'
echo "$CHANGED_FILES" | grep -qvP "$SKIP_REGEX" && \
echo "run_tests=true" >> "$GITHUB_OUTPUT" || \
echo "run_tests=false" >> "$GITHUB_OUTPUT"
echo "$CHANGED_FILES" | grep -qP '^docs/' && \
echo "run_docs=true" >> "$GITHUB_OUTPUT" || \
echo "run_docs=false" >> "$GITHUB_OUTPUT"
echo "$CHANGED_FILES" | grep -qP '^\.github/(workflows/|actions/|actionlint.yml)' && \
echo "run_actionlint=true" >> "$GITHUB_OUTPUT" || \
echo "run_actionlint=false" >> "$GITHUB_OUTPUT"
echo "$CHANGED_FILES" | grep -qP '^(Cargo.lock|script/.*licenses)' && \
echo "run_license=true" >> "$GITHUB_OUTPUT" || \
echo "run_license=false" >> "$GITHUB_OUTPUT"
echo "$CHANGED_FILES" | grep -qP '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' && \
echo "$GITHUB_REF_NAME" | grep -qvP '^v[0-9]+\.[0-9]+\.[0-9x](-pre)?$' && \
echo "run_nix=true" >> "$GITHUB_OUTPUT" || \
echo "run_nix=false" >> "$GITHUB_OUTPUT"
migration_checks:
name: Check Postgres and Protobuf migrations, mergability
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
timeout-minutes: 60
runs-on:
- self-mini-macos
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
fetch-depth: 0 # fetch full history
- name: Remove untracked files
run: git clean -df
- name: Find modified migrations
shell: bash -euxo pipefail {0}
run: |
export SQUAWK_GITHUB_TOKEN=${{ github.token }}
. ./script/squawk
- name: Ensure fresh merge
shell: bash -euxo pipefail {0}
run: |
if [ -z "$GITHUB_BASE_REF" ];
then
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV"
else
git checkout -B temp
git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp"
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV"
fi
- uses: bufbuild/buf-setup-action@v1
with:
version: v1.29.0
- uses: bufbuild/buf-breaking-action@v1
with:
input: "crates/proto/proto/"
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
style:
timeout-minutes: 60
name: Check formatting and spelling
needs: [job_spec]
if: github.repository_owner == 'zed-industries'
runs-on:
- namespace-profile-4x8-ubuntu-2204
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0
with:
version: 9
- name: Prettier Check on /docs
working-directory: ./docs
run: |
pnpm dlx "prettier@${PRETTIER_VERSION}" . --check || {
echo "To fix, run from the root of the Zed repo:"
echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .."
false
}
env:
PRETTIER_VERSION: 3.5.0
- name: Prettier Check on default.json
run: |
pnpm dlx "prettier@${PRETTIER_VERSION}" assets/settings/default.json --check || {
echo "To fix, run from the root of the Zed repo:"
echo " pnpm dlx prettier@${PRETTIER_VERSION} assets/settings/default.json --write"
false
}
env:
PRETTIER_VERSION: 3.5.0
# To support writing comments that they will certainly be revisited.
- name: Check for todo! and FIXME comments
run: script/check-todos
- name: Check modifier use in keymaps
run: script/check-keymaps
- name: Run style checks
uses: ./.github/actions/check_style
- name: Check for typos
uses: crate-ci/typos@80c8a4945eec0f6d464eaf9e65ed98ef085283d1 # v1.38.1
with:
config: ./typos.toml
check_docs:
timeout-minutes: 60
name: Check docs
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
(needs.job_spec.outputs.run_tests == 'true' || needs.job_spec.outputs.run_docs == 'true')
runs-on:
- namespace-profile-8x16-ubuntu-2204
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: Build docs
uses: ./.github/actions/build_docs
actionlint:
runs-on: namespace-profile-2x4-ubuntu-2404
if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true'
needs: [job_spec]
steps:
- uses: actions/checkout@v4
- name: Download actionlint
id: get_actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
shell: bash
- name: Check workflow files
run: ${{ steps.get_actionlint.outputs.executable }} -color
shell: bash
macos_tests:
timeout-minutes: 60
name: (macOS) Run Clippy and tests
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- self-mini-macos
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: Check that Cargo.lock is up to date
run: |
cargo update --locked --workspace
- name: cargo clippy
run: ./script/clippy
- name: Install cargo-machete
uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 # v2
with:
command: install
args: cargo-machete@0.7.0
- name: Check unused dependencies
uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 # v2
with:
command: machete
- name: Check licenses
run: |
script/check-licenses
if [[ "${{ needs.job_spec.outputs.run_license }}" == "true" ]]; then
script/generate-licenses /tmp/zed_licenses_output
fi
- name: Check for new vulnerable dependencies
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@67d4f4bd7a9b17a0db54d2a7519187c65e339de8 # v4
with:
license-check: false
- name: Run tests
uses: ./.github/actions/run_tests
- name: Build collab
# we should do this on a linux x86 machinge
run: cargo build -p collab
- name: Build other binaries and features
run: |
cargo build --workspace --bins --examples
# Since the macOS runners are stateful, so we need to remove the config file to prevent potential bug.
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo
linux_tests:
timeout-minutes: 60
name: (Linux) Run Clippy and tests
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- namespace-profile-16x32-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# cache-provider: "buildjet"
- name: Install Linux dependencies
run: ./script/linux
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: cargo clippy
run: ./script/clippy
- name: Run tests
uses: ./.github/actions/run_tests
- name: Build other binaries and features
run: |
cargo build -p zed
cargo check -p workspace
cargo check -p gpui --examples
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
# to clean up the config file, Ive included the cleanup code here as a precaution.
# While its not strictly necessary at this moment, I believe its better to err on the side of caution.
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo
doctests:
# Nextest currently doesn't support doctests, so run them separately and in parallel.
timeout-minutes: 60
name: (Linux) Run doctests
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- namespace-profile-16x32-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# cache-provider: "buildjet"
- name: Install Linux dependencies
run: ./script/linux
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: Run doctests
run: cargo test --workspace --doc --no-fail-fast
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo
build_remote_server:
timeout-minutes: 60
name: (Linux) Build Remote Server
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on:
- namespace-profile-16x32-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# cache-provider: "buildjet"
- name: Install Clang & Mold
run: ./script/remote-server && ./script/install-mold 2.34.0
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: Build Remote Server
run: cargo build -p remote_server
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo
windows_tests:
timeout-minutes: 60
name: (Windows) Run Clippy and tests
needs: [job_spec]
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on: [self-32vcpu-windows-2022]
steps:
- name: Environment Setup
run: |
$RunnerDir = Split-Path -Parent $env:RUNNER_WORKSPACE
Write-Output `
"RUSTUP_HOME=$RunnerDir\.rustup" `
"CARGO_HOME=$RunnerDir\.cargo" `
"PATH=$RunnerDir\.cargo\bin;$env:PATH" `
>> $env:GITHUB_ENV
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Configure CI
run: |
New-Item -ItemType Directory -Path "./../.cargo" -Force
Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml"
- name: cargo clippy
run: |
.\script\clippy.ps1
- name: Run tests
uses: ./.github/actions/run_tests_windows
- name: Build Zed
run: cargo build
- name: Limit target directory size
run: ./script/clear-target-dir-if-larger-than.ps1 250
- name: Clean CI config file
if: always()
run: Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue
tests_pass:
name: Tests Pass
runs-on: namespace-profile-2x4-ubuntu-2404
needs:
- job_spec
- style
- check_docs
- actionlint
- migration_checks
# run_tests: If adding required tests, add them here and to script below.
- linux_tests
- build_remote_server
- macos_tests
- windows_tests
if: |
github.repository_owner == 'zed-industries' &&
always()
steps:
- name: Check all tests passed
run: |
# Check dependent jobs...
RET_CODE=0
# Always check style
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
if [[ "${{ needs.job_spec.outputs.run_docs }}" == "true" ]]; then
[[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
fi
if [[ "${{ needs.job_spec.outputs.run_actionlint }}" == "true" ]]; then
[[ "${{ needs.actionlint.result }}" != 'success' ]] && { RET_CODE=1; echo "actionlint checks failed"; }
fi
# Only check test jobs if they were supposed to run
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
[[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; }
[[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; }
[[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; }
[[ "${{ needs.build_remote_server.result }}" != 'success' ]] && { RET_CODE=1; echo "Remote server build failed"; }
# This check is intentionally disabled. See: https://github.com/zed-industries/zed/pull/28431
# [[ "${{ needs.migration_checks.result }}" != 'success' ]] && { RET_CODE=1; echo "Migration Checks failed"; }
fi
if [[ "$RET_CODE" -eq 0 ]]; then
echo "All tests passed successfully!"
fi
exit $RET_CODE
bundle-mac:
timeout-minutes: 120
name: Create a macOS bundle
runs-on:
- self-mini-macos
if: startsWith(github.ref, 'refs/tags/v')
needs: [macos_tests]
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "18"
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
# We need to fetch more than one commit so that `script/draft-release-notes`
# is able to diff between the current and previous tag.
#
# 25 was chosen arbitrarily.
fetch-depth: 25
clean: false
ref: ${{ github.ref }}
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 300
- name: Determine version and release channel
run: |
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel
- name: Draft release notes
run: |
mkdir -p target/
# Ignore any errors that occur while drafting release notes to not fail the build.
script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md || true
script/create-draft-release target/release-notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create macOS app bundle (aarch64)
run: script/bundle-mac aarch64-apple-darwin
- name: Create macOS app bundle (x64)
run: script/bundle-mac x86_64-apple-darwin
- name: Rename binaries
run: |
mv target/aarch64-apple-darwin/release/Zed.dmg target/aarch64-apple-darwin/release/Zed-aarch64.dmg
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
- uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
name: Upload app bundle to release
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: |
target/zed-remote-server-macos-x86_64.gz
target/zed-remote-server-macos-aarch64.gz
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux-x86_x64:
timeout-minutes: 60
name: Linux x86_x64 release bundle
runs-on:
- namespace-profile-16x32-ubuntu-2004 # ubuntu 20.04 for minimal glibc
if: |
( startsWith(github.ref, 'refs/tags/v') )
needs: [linux_tests]
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Install Linux dependencies
run: ./script/linux && ./script/install-mold 2.34.0
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Determine version and release channel
run: |
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel
- name: Create Linux .tar.gz bundle
run: script/bundle-linux
- name: Upload Artifacts to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: |
target/zed-remote-server-linux-x86_64.gz
target/release/zed-linux-x86_64.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-linux-aarch64: # this runs on ubuntu22.04
timeout-minutes: 60
name: Linux arm64 release bundle
runs-on:
- namespace-profile-8x32-ubuntu-2004-arm-m4 # ubuntu 20.04 for minimal glibc
if: |
startsWith(github.ref, 'refs/tags/v')
needs: [linux_tests]
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Install Linux dependencies
run: ./script/linux
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Determine version and release channel
run: |
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel
- name: Create and upload Linux .tar.gz bundles
run: script/bundle-linux
- name: Upload Artifacts to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: |
target/zed-remote-server-linux-aarch64.gz
target/release/zed-linux-aarch64.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
freebsd:
timeout-minutes: 60
runs-on: github-8vcpu-ubuntu-2404
if: |
false && ( startsWith(github.ref, 'refs/tags/v') )
needs: [linux_tests]
name: Build Zed on FreeBSD
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
id: freebsd-build
uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0
with:
usesh: true
release: 13.5
copyback: true
prepare: |
pkg install -y \
bash curl jq git \
rustup-init cmake-core llvm-devel-lite pkgconf protobuf # ibx11 alsa-lib rust-bindgen-cli
run: |
freebsd-version
sysctl hw.model
sysctl hw.ncpu
sysctl hw.physmem
sysctl hw.usermem
git config --global --add safe.directory /home/runner/work/zed/zed
rustup-init --profile minimal --default-toolchain none -y
. "$HOME/.cargo/env"
./script/bundle-freebsd
mkdir -p out/
mv "target/zed-remote-server-freebsd-x86_64.gz" out/
rm -rf target/
cargo clean
- name: Upload Artifact to Workflow - zed-remote-server (run-bundling)
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
with:
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-freebsd.gz
path: out/zed-remote-server-freebsd-x86_64.gz
- name: Upload Artifacts to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }}
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: |
out/zed-remote-server-freebsd-x86_64.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
nix-build:
name: Build with Nix
uses: ./.github/workflows/nix_build.yml
needs: [job_spec]
if: github.repository_owner == 'zed-industries' &&
(contains(github.event.pull_request.labels.*.name, 'run-nix') ||
needs.job_spec.outputs.run_nix == 'true')
secrets: inherit
bundle-windows-x64:
timeout-minutes: 120
name: Create a Windows installer for x86_64
runs-on: [self-32vcpu-windows-2022]
if: |
( startsWith(github.ref, 'refs/tags/v') )
needs: [windows_tests]
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com"
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Determine version and release channel
working-directory: ${{ env.ZED_WORKSPACE }}
run: |
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel.ps1
- name: Build Zed installer
working-directory: ${{ env.ZED_WORKSPACE }}
run: script/bundle-windows.ps1
- name: Upload Artifacts to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: ${{ env.SETUP_PATH }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
bundle-windows-aarch64:
timeout-minutes: 120
name: Create a Windows installer for aarch64
runs-on: [self-32vcpu-windows-2022]
if: |
( startsWith(github.ref, 'refs/tags/v') )
needs: [windows_tests]
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com"
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Determine version and release channel
working-directory: ${{ env.ZED_WORKSPACE }}
run: |
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
script/determine-release-channel.ps1
- name: Build Zed installer
working-directory: ${{ env.ZED_WORKSPACE }}
run: script/bundle-windows.ps1 -Architecture aarch64
- name: Upload Artifacts to release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
draft: true
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
files: ${{ env.SETUP_PATH }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
auto-release-preview:
name: Auto release preview
if: |
false
&& startsWith(github.ref, 'refs/tags/v')
&& endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64, bundle-windows-aarch64]
runs-on:
- self-mini-macos
steps:
- name: gh release
run: gh release edit "$GITHUB_REF_NAME" --draft=false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Sentry release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3
env:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
environment: production

View File

@@ -2,77 +2,12 @@
# Rebuild with `cargo xtask workflows`.
name: compare_perf
on:
workflow_dispatch:
inputs:
head:
description: head
required: true
type: string
base:
description: base
required: true
type: string
crate_name:
description: crate_name
type: string
default: ''
workflow_dispatch: {}
jobs:
run_perf:
runs-on: namespace-profile-16x32-ubuntu-2204
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::install_hyperfine
run: cargo install hyperfine
shell: bash -euxo pipefail {0}
- name: steps::git_checkout
run: git fetch origin ${{ inputs.base }} && git checkout ${{ inputs.base }}
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::cargo_perf_test
run: |2-
if [ -n "${{ inputs.crate_name }}" ]; then
cargo perf-test -p ${{ inputs.crate_name }} -- --json=${{ inputs.base }};
else
cargo perf-test -p vim -- --json=${{ inputs.base }};
fi
shell: bash -euxo pipefail {0}
- name: steps::git_checkout
run: git fetch origin ${{ inputs.head }} && git checkout ${{ inputs.head }}
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::cargo_perf_test
run: |2-
if [ -n "${{ inputs.crate_name }}" ]; then
cargo perf-test -p ${{ inputs.crate_name }} -- --json=${{ inputs.head }};
else
cargo perf-test -p vim -- --json=${{ inputs.head }};
fi
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::compare_runs
run: cargo perf-compare --save=results.md ${{ inputs.base }} ${{ inputs.head }}
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact results.md'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: results.md
path: results.md
if-no-files-found: error
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}

View File

@@ -29,10 +29,10 @@ jobs:
node-version: '20'
cache: pnpm
cache-dependency-path: script/danger/pnpm-lock.yaml
- name: danger::danger_job::install_deps
- name: danger::install_deps
run: pnpm install --dir script/danger
shell: bash -euxo pipefail {0}
- name: danger::danger_job::run
- name: danger::run
run: pnpm run --dir script/danger danger ci
shell: bash -euxo pipefail {0}
env:

71
.github/workflows/eval.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: Run Agent Eval
on:
schedule:
- cron: "0 0 * * *"
pull_request:
branches:
- "**"
types: [synchronize, reopened, labeled]
workflow_dispatch:
concurrency:
# Allow only one workflow per any non-`main` branch.
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_EVAL_TELEMETRY: 1
jobs:
run_eval:
timeout-minutes: 60
name: Run Agent Eval
if: >
github.repository_owner == 'zed-industries' &&
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
runs-on:
- namespace-profile-16x32-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# cache-provider: "buildjet"
- name: Install Linux dependencies
run: ./script/linux
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: Compile eval
run: cargo build --package=eval
- name: Run eval
run: cargo run --package=eval -- --repetitions=8 --concurrency=1
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
# to clean up the config file, Ive included the cleanup code here as a precaution.
# While its not strictly necessary at this moment, I believe its better to err on the side of caution.
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo

97
.github/workflows/nix_build.yml vendored Normal file
View File

@@ -0,0 +1,97 @@
# Generated from xtask::workflows::nix_build
# Rebuild with `cargo xtask workflows`.
name: nix_build
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: '1'
CARGO_INCREMENTAL: '0'
on:
pull_request:
branches:
- '**'
paths:
- nix/**
- flake.*
- Cargo.*
- rust-toolchain.toml
- .cargo/config.toml
push:
branches:
- main
- v[0-9]+.[0-9]+.x
paths:
- nix/**
- flake.*
- Cargo.*
- rust-toolchain.toml
- .cargo/config.toml
workflow_call: {}
jobs:
build_nix_linux_x86_64:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-32x64-ubuntu-2004
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
GIT_LFS_SKIP_SMUDGE: '1'
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: nix_build::install_nix
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- name: nix_build::cachix_action
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
with:
name: zed
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
cachixArgs: -v
pushFilter: -zed-editor-[0-9.]*-nightly
- name: nix_build::build
run: nix build .#debug -L --accept-flake-config
shell: bash -euxo pipefail {0}
timeout-minutes: 60
continue-on-error: true
build_nix_mac_aarch64:
if: github.repository_owner == 'zed-industries'
runs-on: self-mini-macos
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON: ${{ secrets.ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON }}
GIT_LFS_SKIP_SMUDGE: '1'
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: nix_build::set_path
run: |
echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH"
echo "/Users/administrator/.nix-profile/bin" >> "$GITHUB_PATH"
shell: bash -euxo pipefail {0}
- name: nix_build::cachix_action
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
with:
name: zed
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
cachixArgs: -v
pushFilter: -zed-editor-[0-9.]*-nightly
- name: nix_build::build
run: nix build .#debug -L --accept-flake-config
shell: bash -euxo pipefail {0}
- name: nix_build::limit_store
run: |-
if [ "$(du -sm /nix/store | cut -f1)" -gt 50000 ]; then
nix-collect-garbage -d || true
fi
shell: bash -euxo pipefail {0}
timeout-minutes: 60
continue-on-error: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -1,488 +0,0 @@
# Generated from xtask::workflows::release
# Rebuild with `cargo xtask workflows`.
name: release
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: '1'
on:
push:
tags:
- v*
jobs:
run_tests_mac:
if: github.repository_owner == 'zed-industries'
runs-on: self-mini-macos
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- 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}
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
timeout-minutes: 60
run_tests_linux:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- 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 250
shell: bash -euxo pipefail {0}
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
timeout-minutes: 60
run_tests_windows:
if: github.repository_owner == 'zed-industries'
runs-on: self-32vcpu-windows-2022
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_cargo_config
run: |
New-Item -ItemType Directory -Path "./../.cargo" -Force
Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml"
shell: pwsh
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- 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
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: pwsh
- name: steps::cleanup_cargo_config
if: always()
run: |
Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue
shell: pwsh
timeout-minutes: 60
check_scripts:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: run_tests::check_scripts::run_shellcheck
run: ./script/shellcheck-scripts error
shell: bash -euxo pipefail {0}
- id: get_actionlint
name: run_tests::check_scripts::download_actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
shell: bash -euxo pipefail {0}
- name: run_tests::check_scripts::run_actionlint
run: |
${{ steps.get_actionlint.outputs.executable }} -color
shell: bash -euxo pipefail {0}
- name: run_tests::check_scripts::check_xtask_workflows
run: |
cargo xtask workflows
if ! git diff --exit-code .github; then
echo "Error: .github directory has uncommitted changes after running 'cargo xtask workflows'"
echo "Please run 'cargo xtask workflows' locally and commit the changes"
exit 1
fi
shell: bash -euxo pipefail {0}
timeout-minutes: 60
create_draft_release:
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
fetch-depth: 25
ref: ${{ github.ref }}
- name: script/determine-release-channel
run: script/determine-release-channel
shell: bash -euxo pipefail {0}
- name: mkdir -p target/
run: mkdir -p target/
shell: bash -euxo pipefail {0}
- name: release::create_draft_release::generate_release_notes
run: node --redirect-warnings=/dev/null ./script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md
shell: bash -euxo pipefail {0}
- name: release::create_draft_release::create_release
run: script/create-draft-release target/release-notes.md
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
timeout-minutes: 60
bundle_linux_aarch64:
needs:
- run_tests_linux
- check_scripts
runs-on: namespace-profile-8x32-ubuntu-2004-arm-m4
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact zed-linux-aarch64.tar.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-linux-aarch64.tar.gz
path: target/release/zed-linux-aarch64.tar.gz
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-linux-aarch64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-linux-aarch64.gz
path: target/zed-remote-server-linux-aarch64.gz
if-no-files-found: error
timeout-minutes: 60
bundle_linux_x86_64:
needs:
- run_tests_linux
- check_scripts
runs-on: namespace-profile-32x64-ubuntu-2004
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact zed-linux-x86_64.tar.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-linux-x86_64.tar.gz
path: target/release/zed-linux-x86_64.tar.gz
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-linux-x86_64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-linux-x86_64.gz
path: target/zed-remote-server-linux-x86_64.gz
if-no-files-found: error
timeout-minutes: 60
bundle_mac_aarch64:
needs:
- run_tests_mac
- check_scripts
runs-on: self-mini-macos
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
- name: run_bundling::bundle_mac::bundle_mac
run: ./script/bundle-mac aarch64-apple-darwin
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact Zed-aarch64.dmg'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed-aarch64.dmg
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-macos-aarch64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-macos-aarch64.gz
path: target/zed-remote-server-macos-aarch64.gz
if-no-files-found: error
timeout-minutes: 60
bundle_mac_x86_64:
needs:
- run_tests_mac
- check_scripts
runs-on: self-mini-macos
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
- name: run_bundling::bundle_mac::bundle_mac
run: ./script/bundle-mac x86_64-apple-darwin
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact Zed-x86_64.dmg'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed-x86_64.dmg
path: target/x86_64-apple-darwin/release/Zed-x86_64.dmg
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-macos-x86_64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-macos-x86_64.gz
path: target/zed-remote-server-macos-x86_64.gz
if-no-files-found: error
timeout-minutes: 60
bundle_windows_aarch64:
needs:
- run_tests_windows
- check_scripts
runs-on: self-32vcpu-windows-2022
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: run_bundling::bundle_windows::bundle_windows
run: script/bundle-windows.ps1 -Architecture aarch64
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
- name: '@actions/upload-artifact Zed-aarch64.exe'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed-aarch64.exe
path: target/Zed-aarch64.exe
if-no-files-found: error
timeout-minutes: 60
bundle_windows_x86_64:
needs:
- run_tests_windows
- check_scripts
runs-on: self-32vcpu-windows-2022
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: run_bundling::bundle_windows::bundle_windows
run: script/bundle-windows.ps1 -Architecture x86_64
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
- name: '@actions/upload-artifact Zed-x86_64.exe'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed-x86_64.exe
path: target/Zed-x86_64.exe
if-no-files-found: error
timeout-minutes: 60
upload_release_assets:
needs:
- create_draft_release
- bundle_linux_aarch64
- bundle_linux_x86_64
- bundle_mac_aarch64
- bundle_mac_x86_64
- bundle_windows_aarch64
- bundle_windows_x86_64
runs-on: namespace-profile-4x8-ubuntu-2204
steps:
- name: release::download_workflow_artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
with:
path: ./artifacts/
- name: ls -lR ./artifacts
run: ls -lR ./artifacts
shell: bash -euxo pipefail {0}
- name: release::prep_release_artifacts
run: |-
mkdir -p release-artifacts/
mv ./artifacts/Zed-aarch64.dmg/Zed-aarch64.dmg release-artifacts/Zed-aarch64.dmg
mv ./artifacts/Zed-x86_64.dmg/Zed-x86_64.dmg release-artifacts/Zed-x86_64.dmg
mv ./artifacts/zed-linux-aarch64.tar.gz/zed-linux-aarch64.tar.gz release-artifacts/zed-linux-aarch64.tar.gz
mv ./artifacts/zed-linux-x86_64.tar.gz/zed-linux-x86_64.tar.gz release-artifacts/zed-linux-x86_64.tar.gz
mv ./artifacts/Zed-x86_64.exe/Zed-x86_64.exe release-artifacts/Zed-x86_64.exe
mv ./artifacts/Zed-aarch64.exe/Zed-aarch64.exe release-artifacts/Zed-aarch64.exe
mv ./artifacts/zed-remote-server-macos-aarch64.gz/zed-remote-server-macos-aarch64.gz release-artifacts/zed-remote-server-macos-aarch64.gz
mv ./artifacts/zed-remote-server-macos-x86_64.gz/zed-remote-server-macos-x86_64.gz release-artifacts/zed-remote-server-macos-x86_64.gz
mv ./artifacts/zed-remote-server-linux-aarch64.gz/zed-remote-server-linux-aarch64.gz release-artifacts/zed-remote-server-linux-aarch64.gz
mv ./artifacts/zed-remote-server-linux-x86_64.gz/zed-remote-server-linux-x86_64.gz release-artifacts/zed-remote-server-linux-x86_64.gz
shell: bash -euxo pipefail {0}
- name: gh release upload "$GITHUB_REF_NAME" --repo=zed-industries/zed release-artifacts/*
run: gh release upload "$GITHUB_REF_NAME" --repo=zed-industries/zed release-artifacts/*
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
auto_release_preview:
needs:
- upload_release_assets
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: release::create_sentry_release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c
with:
environment: production
env:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -3,7 +3,12 @@
name: release_nightly
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1'
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
on:
push:
tags:
@@ -27,6 +32,41 @@ jobs:
run: ./script/clippy
shell: bash -euxo pipefail {0}
timeout-minutes: 60
run_tests_mac:
if: github.repository_owner == 'zed-industries'
runs-on: self-mini-macos
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- 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}
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
timeout-minutes: 60
run_tests_windows:
if: github.repository_owner == 'zed-industries'
runs-on: self-32vcpu-windows-2022
@@ -62,109 +102,13 @@ jobs:
Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue
shell: pwsh
timeout-minutes: 60
bundle_linux_aarch64:
bundle_mac_nightly_x86_64:
needs:
- check_style
- run_tests_windows
runs-on: namespace-profile-8x32-ubuntu-2004-arm-m4
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
run: |
set -eu
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
shell: bash -euxo pipefail {0}
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact zed-linux-aarch64.tar.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-linux-aarch64.tar.gz
path: target/release/zed-linux-aarch64.tar.gz
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-linux-aarch64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-linux-aarch64.gz
path: target/zed-remote-server-linux-aarch64.gz
if-no-files-found: error
timeout-minutes: 60
bundle_linux_x86_64:
needs:
- check_style
- run_tests_windows
runs-on: namespace-profile-32x64-ubuntu-2004
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
run: |
set -eu
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
shell: bash -euxo pipefail {0}
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact zed-linux-x86_64.tar.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-linux-x86_64.tar.gz
path: target/release/zed-linux-x86_64.tar.gz
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-linux-x86_64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-linux-x86_64.gz
path: target/zed-remote-server-linux-x86_64.gz
if-no-files-found: error
timeout-minutes: 60
bundle_mac_aarch64:
needs:
- check_style
- run_tests_windows
- run_tests_mac
if: github.repository_owner == 'zed-industries'
runs-on: self-mini-macos
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
@@ -175,13 +119,6 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
run: |
set -eu
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
shell: bash -euxo pipefail {0}
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
@@ -193,84 +130,148 @@ jobs:
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
- name: run_bundling::bundle_mac::bundle_mac
run: ./script/bundle-mac aarch64-apple-darwin
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact Zed-aarch64.dmg'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed-aarch64.dmg
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-macos-aarch64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-macos-aarch64.gz
path: target/zed-remote-server-macos-aarch64.gz
if-no-files-found: error
timeout-minutes: 60
bundle_mac_x86_64:
needs:
- check_style
- run_tests_windows
runs-on: self-mini-macos
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
- name: release_nightly::set_release_channel_to_nightly
run: |
set -eu
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
shell: bash -euxo pipefail {0}
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
- name: run_bundling::bundle_mac::bundle_mac
- name: run_bundling::bundle_mac
run: ./script/bundle-mac x86_64-apple-darwin
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact Zed-x86_64.dmg'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed-x86_64.dmg
path: target/x86_64-apple-darwin/release/Zed-x86_64.dmg
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-macos-x86_64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-macos-x86_64.gz
path: target/zed-remote-server-macos-x86_64.gz
if-no-files-found: error
- name: release_nightly::upload_zed_nightly
run: script/upload-nightly macos x86_64
shell: bash -euxo pipefail {0}
timeout-minutes: 60
bundle_windows_aarch64:
bundle_mac_nightly_aarch64:
needs:
- check_style
- run_tests_mac
if: github.repository_owner == 'zed-industries'
runs-on: self-mini-macos
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
- name: release_nightly::set_release_channel_to_nightly
run: |
set -eu
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
shell: bash -euxo pipefail {0}
- name: run_bundling::bundle_mac
run: ./script/bundle-mac aarch64-apple-darwin
shell: bash -euxo pipefail {0}
- name: release_nightly::upload_zed_nightly
run: script/upload-nightly macos aarch64
shell: bash -euxo pipefail {0}
timeout-minutes: 60
bundle_linux_nightly_x86_64:
needs:
- check_style
- run_tests_mac
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-32x64-ubuntu-2004
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: release_nightly::add_rust_to_path
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
shell: bash -euxo pipefail {0}
- name: ./script/linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: ./script/install-mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 100
shell: bash -euxo pipefail {0}
- name: release_nightly::set_release_channel_to_nightly
run: |
set -eu
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
- name: release_nightly::upload_zed_nightly
run: script/upload-nightly linux-targz x86_64
shell: bash -euxo pipefail {0}
timeout-minutes: 60
bundle_linux_nightly_aarch64:
needs:
- check_style
- run_tests_mac
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-8x32-ubuntu-2004-arm-m4
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: release_nightly::add_rust_to_path
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
shell: bash -euxo pipefail {0}
- name: ./script/linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 100
shell: bash -euxo pipefail {0}
- name: release_nightly::set_release_channel_to_nightly
run: |
set -eu
version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
- name: release_nightly::upload_zed_nightly
run: script/upload-nightly linux-targz aarch64
shell: bash -euxo pipefail {0}
timeout-minutes: 60
bundle_windows_nightly_x86_64:
needs:
- check_style
- run_tests_windows
if: github.repository_owner == 'zed-industries'
runs-on: self-32vcpu-windows-2022
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
@@ -285,7 +286,11 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: release_nightly::set_release_channel_to_nightly
run: |
$ErrorActionPreference = "Stop"
$version = git rev-parse --short HEAD
@@ -293,71 +298,61 @@ jobs:
"nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL"
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: run_bundling::bundle_windows::bundle_windows
run: script/bundle-windows.ps1 -Architecture aarch64
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
- name: '@actions/upload-artifact Zed-aarch64.exe'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed-aarch64.exe
path: target/Zed-aarch64.exe
if-no-files-found: error
timeout-minutes: 60
bundle_windows_x86_64:
needs:
- check_style
- run_tests_windows
runs-on: self-32vcpu-windows-2022
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: run_bundling::set_release_channel_to_nightly
run: |
$ErrorActionPreference = "Stop"
$version = git rev-parse --short HEAD
Write-Host "Publishing version: $version on release channel nightly"
"nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL"
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: run_bundling::bundle_windows::bundle_windows
- name: release_nightly::build_zed_installer
run: script/bundle-windows.ps1 -Architecture x86_64
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
- name: '@actions/upload-artifact Zed-x86_64.exe'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
- name: release_nightly::upload_zed_nightly_windows
run: script/upload-nightly.ps1 -Architecture x86_64
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
timeout-minutes: 60
bundle_windows_nightly_aarch64:
needs:
- check_style
- run_tests_windows
if: github.repository_owner == 'zed-industries'
runs-on: self-32vcpu-windows-2022
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
name: Zed-x86_64.exe
path: target/Zed-x86_64.exe
if-no-files-found: error
clean: false
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: release_nightly::set_release_channel_to_nightly
run: |
$ErrorActionPreference = "Stop"
$version = git rev-parse --short HEAD
Write-Host "Publishing version: $version on release channel nightly"
"nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL"
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
- name: release_nightly::build_zed_installer
run: script/bundle-windows.ps1 -Architecture aarch64
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
- name: release_nightly::upload_zed_nightly_windows
run: script/upload-nightly.ps1 -Architecture aarch64
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
timeout-minutes: 60
build_nix_linux_x86_64:
needs:
- check_style
- run_tests_windows
- run_tests_mac
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-32x64-ubuntu-2004
env:
@@ -370,17 +365,17 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: nix_build::build_nix::install_nix
- name: nix_build::install_nix
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- name: nix_build::build_nix::cachix_action
- name: nix_build::cachix_action
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
with:
name: zed
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
cachixArgs: -v
- name: nix_build::build_nix::build
- name: nix_build::build
run: nix build .#default -L --accept-flake-config
shell: bash -euxo pipefail {0}
timeout-minutes: 60
@@ -388,7 +383,7 @@ jobs:
build_nix_mac_aarch64:
needs:
- check_style
- run_tests_windows
- run_tests_mac
if: github.repository_owner == 'zed-industries'
runs-on: self-mini-macos
env:
@@ -401,21 +396,21 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: nix_build::build_nix::set_path
- name: nix_build::set_path
run: |
echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH"
echo "/Users/administrator/.nix-profile/bin" >> "$GITHUB_PATH"
shell: bash -euxo pipefail {0}
- name: nix_build::build_nix::cachix_action
- name: nix_build::cachix_action
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
with:
name: zed
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
cachixArgs: -v
- name: nix_build::build_nix::build
- name: nix_build::build
run: nix build .#default -L --accept-flake-config
shell: bash -euxo pipefail {0}
- name: nix_build::build_nix::limit_store
- name: nix_build::limit_store
run: |-
if [ "$(du -sm /nix/store | cut -f1)" -gt 50000 ]; then
nix-collect-garbage -d || true
@@ -425,49 +420,21 @@ jobs:
continue-on-error: true
update_nightly_tag:
needs:
- bundle_linux_aarch64
- bundle_linux_x86_64
- bundle_mac_aarch64
- bundle_mac_x86_64
- bundle_windows_aarch64
- bundle_windows_x86_64
- bundle_mac_nightly_x86_64
- bundle_mac_nightly_aarch64
- bundle_linux_nightly_x86_64
- bundle_linux_nightly_aarch64
- bundle_windows_nightly_x86_64
- bundle_windows_nightly_aarch64
if: github.repository_owner == 'zed-industries'
runs-on: namespace-profile-4x8-ubuntu-2204
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
fetch-depth: 0
- name: release::download_workflow_artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
with:
path: ./artifacts/
- name: ls -lR ./artifacts
run: ls -lR ./artifacts
shell: bash -euxo pipefail {0}
- name: release::prep_release_artifacts
run: |-
mkdir -p release-artifacts/
mv ./artifacts/Zed-aarch64.dmg/Zed-aarch64.dmg release-artifacts/Zed-aarch64.dmg
mv ./artifacts/Zed-x86_64.dmg/Zed-x86_64.dmg release-artifacts/Zed-x86_64.dmg
mv ./artifacts/zed-linux-aarch64.tar.gz/zed-linux-aarch64.tar.gz release-artifacts/zed-linux-aarch64.tar.gz
mv ./artifacts/zed-linux-x86_64.tar.gz/zed-linux-x86_64.tar.gz release-artifacts/zed-linux-x86_64.tar.gz
mv ./artifacts/Zed-x86_64.exe/Zed-x86_64.exe release-artifacts/Zed-x86_64.exe
mv ./artifacts/Zed-aarch64.exe/Zed-aarch64.exe release-artifacts/Zed-aarch64.exe
mv ./artifacts/zed-remote-server-macos-aarch64.gz/zed-remote-server-macos-aarch64.gz release-artifacts/zed-remote-server-macos-aarch64.gz
mv ./artifacts/zed-remote-server-macos-x86_64.gz/zed-remote-server-macos-x86_64.gz release-artifacts/zed-remote-server-macos-x86_64.gz
mv ./artifacts/zed-remote-server-linux-aarch64.gz/zed-remote-server-linux-aarch64.gz release-artifacts/zed-remote-server-linux-aarch64.gz
mv ./artifacts/zed-remote-server-linux-x86_64.gz/zed-remote-server-linux-x86_64.gz release-artifacts/zed-remote-server-linux-x86_64.gz
shell: bash -euxo pipefail {0}
- name: ./script/upload-nightly
run: ./script/upload-nightly
shell: bash -euxo pipefail {0}
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
- name: release_nightly::update_nightly_tag_job::update_nightly_tag
- name: release_nightly::update_nightly_tag
run: |
if [ "$(git rev-parse nightly)" = "$(git rev-parse HEAD)" ]; then
echo "Nightly tag already points to current commit. Skipping tagging."
@@ -478,7 +445,7 @@ jobs:
git tag -f nightly
git push origin nightly --force
shell: bash -euxo pipefail {0}
- name: release::create_sentry_release
- name: release_nightly::create_sentry_release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c
with:
environment: production

View File

@@ -1,62 +0,0 @@
# Generated from xtask::workflows::run_agent_evals
# Rebuild with `cargo xtask workflows`.
name: run_agent_evals
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1'
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_EVAL_TELEMETRY: '1'
on:
pull_request:
types:
- synchronize
- reopened
- labeled
branches:
- '**'
schedule:
- cron: 0 0 * * *
workflow_dispatch: {}
jobs:
agent_evals:
if: |
github.repository_owner == 'zed-industries' &&
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: cargo build --package=eval
run: cargo build --package=eval
shell: bash -euxo pipefail {0}
- name: run_agent_evals::agent_evals::run_eval
run: cargo run --package=eval -- --repetitions=8 --concurrency=1
shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
timeout-minutes: 60
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -3,148 +3,22 @@
name: run_bundling
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1'
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
on:
pull_request:
types:
- labeled
- synchronize
jobs:
bundle_linux_aarch64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: namespace-profile-8x32-ubuntu-2004-arm-m4
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact zed-linux-aarch64.tar.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-linux-aarch64.tar.gz
path: target/release/zed-linux-aarch64.tar.gz
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-linux-aarch64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-linux-aarch64.gz
path: target/zed-remote-server-linux-aarch64.gz
if-no-files-found: error
timeout-minutes: 60
bundle_linux_x86_64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: namespace-profile-32x64-ubuntu-2004
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact zed-linux-x86_64.tar.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-linux-x86_64.tar.gz
path: target/release/zed-linux-x86_64.tar.gz
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-linux-x86_64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-linux-x86_64.gz
path: target/zed-remote-server-linux-x86_64.gz
if-no-files-found: error
timeout-minutes: 60
bundle_mac_aarch64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: self-mini-macos
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
- name: run_bundling::bundle_mac::bundle_mac
run: ./script/bundle-mac aarch64-apple-darwin
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact Zed-aarch64.dmg'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed-aarch64.dmg
path: target/aarch64-apple-darwin/release/Zed-aarch64.dmg
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-macos-aarch64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-macos-aarch64.gz
path: target/zed-remote-server-macos-aarch64.gz
if-no-files-found: error
timeout-minutes: 60
bundle_mac_x86_64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: self-mini-macos
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
@@ -166,40 +40,66 @@ jobs:
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
- name: run_bundling::bundle_mac::bundle_mac
- name: run_bundling::bundle_mac
run: ./script/bundle-mac x86_64-apple-darwin
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact Zed-x86_64.dmg'
- name: '@actions/upload-artifact Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed-x86_64.dmg
path: target/x86_64-apple-darwin/release/Zed-x86_64.dmg
if-no-files-found: error
- name: '@actions/upload-artifact zed-remote-server-macos-x86_64.gz'
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.dmg
path: target/x86_64-apple-darwin/release/Zed.dmg
- name: '@actions/upload-artifact zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-macos-x86_64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-macos-x86_64.gz
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-macos-x86_64.gz
path: target/zed-remote-server-macos-x86_64.gz
if-no-files-found: error
timeout-minutes: 60
bundle_windows_aarch64:
bundle_mac_arm64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: self-32vcpu-windows-2022
runs-on: self-mini-macos
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
- name: run_bundling::bundle_mac
run: ./script/bundle-mac aarch64-apple-darwin
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.dmg
path: target/aarch64-apple-darwin/release/Zed.dmg
- name: '@actions/upload-artifact zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-macos-aarch64.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-macos-aarch64.gz
path: target/zed-remote-server-macos-aarch64.gz
timeout-minutes: 60
bundle_linux_x86_64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: namespace-profile-32x64-ubuntu-2004
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -209,16 +109,59 @@ jobs:
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: run_bundling::bundle_windows::bundle_windows
run: script/bundle-windows.ps1 -Architecture aarch64
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
- name: '@actions/upload-artifact Zed-aarch64.exe'
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed-aarch64.exe
path: target/Zed-aarch64.exe
if-no-files-found: error
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
- name: '@actions/upload-artifact zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-x86_64-unknown-linux-gnu.tar.gz
path: target/release/zed-remote-server-*.tar.gz
timeout-minutes: 60
bundle_linux_arm64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: namespace-profile-8x32-ubuntu-2004-arm-m4
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
- name: '@actions/upload-artifact zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
path: target/release/zed-*.tar.gz
- name: '@actions/upload-artifact zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: zed-remote-server-${{ github.event.pull_request.head.sha || github.sha }}-aarch64-unknown-linux-gnu.tar.gz
path: target/release/zed-remote-server-*.tar.gz
timeout-minutes: 60
bundle_windows_x86_64:
if: |-
@@ -226,9 +169,6 @@ jobs:
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: self-32vcpu-windows-2022
env:
CARGO_INCREMENTAL: 0
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_MINIDUMP_ENDPOINT: ${{ secrets.ZED_SENTRY_MINIDUMP_ENDPOINT }}
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
@@ -247,16 +187,49 @@ jobs:
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: run_bundling::bundle_windows::bundle_windows
- name: run_bundling::bundle_windows
run: script/bundle-windows.ps1 -Architecture x86_64
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
- name: '@actions/upload-artifact Zed-x86_64.exe'
- name: '@actions/upload-artifact Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.exe'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed-x86_64.exe
path: target/Zed-x86_64.exe
if-no-files-found: error
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-x86_64.exe
path: ${{ env.SETUP_PATH }}
timeout-minutes: 60
bundle_windows_arm64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: self-32vcpu-windows-2022
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }}
ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }}
ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }}
FILE_DIGEST: SHA256
TIMESTAMP_DIGEST: SHA256
TIMESTAMP_SERVER: http://timestamp.acs.microsoft.com
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_sentry
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b
with:
token: ${{ secrets.SENTRY_AUTH_TOKEN }}
- name: run_bundling::bundle_windows
run: script/bundle-windows.ps1 -Architecture aarch64
shell: pwsh
working-directory: ${{ env.ZED_WORKSPACE }}
- name: '@actions/upload-artifact Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.exe'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.exe
path: ${{ env.SETUP_PATH }}
timeout-minutes: 60
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}

View File

@@ -66,10 +66,6 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_pnpm
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
@@ -149,10 +145,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
@@ -164,7 +156,7 @@ jobs:
run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
run: ./script/clear-target-dir-if-larger-than 100
shell: bash -euxo pipefail {0}
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
@@ -222,10 +214,10 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
- name: steps::cache_rust_dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6
with:
cache: rust
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
@@ -269,10 +261,6 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: cargo build -p collab
run: cargo build -p collab
shell: bash -euxo pipefail {0}
@@ -329,10 +317,6 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: run_tests::check_dependencies::install_cargo_machete
uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386
with:
@@ -366,10 +350,10 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
- name: steps::cache_rust_dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6
with:
cache: rust
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: run_tests::check_docs::lychee_link_check
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332
with:
@@ -408,10 +392,6 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: ./script/check-licenses
run: ./script/check-licenses
shell: bash -euxo pipefail {0}
@@ -464,18 +444,18 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: nix_build::build_nix::install_nix
- name: nix_build::install_nix
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f
with:
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- name: nix_build::build_nix::cachix_action
- name: nix_build::cachix_action
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
with:
name: zed
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
cachixArgs: -v
pushFilter: -zed-editor-[0-9.]*-nightly
- name: nix_build::build_nix::build
- name: nix_build::build
run: nix build .#debug -L --accept-flake-config
shell: bash -euxo pipefail {0}
timeout-minutes: 60
@@ -495,22 +475,22 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: nix_build::build_nix::set_path
- name: nix_build::set_path
run: |
echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH"
echo "/Users/administrator/.nix-profile/bin" >> "$GITHUB_PATH"
shell: bash -euxo pipefail {0}
- name: nix_build::build_nix::cachix_action
- name: nix_build::cachix_action
uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad
with:
name: zed
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
cachixArgs: -v
pushFilter: -zed-editor-[0-9.]*-nightly
- name: nix_build::build_nix::build
- name: nix_build::build
run: nix build .#debug -L --accept-flake-config
shell: bash -euxo pipefail {0}
- name: nix_build::build_nix::limit_store
- name: nix_build::limit_store
run: |-
if [ "$(du -sm /nix/store | cut -f1)" -gt 50000 ]; then
nix-collect-garbage -d || true

View File

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

86
.github/workflows/unit_evals.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
name: Run Unit Evals
on:
schedule:
# GitHub might drop jobs at busy times, so we choose a random time in the middle of the night.
- cron: "47 1 * * 2"
workflow_dispatch:
concurrency:
# Allow only one workflow per any non-`main` branch.
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
jobs:
unit_evals:
if: github.repository_owner == 'zed-industries'
timeout-minutes: 60
name: Run unit evals
runs-on:
- namespace-profile-16x32-ubuntu-2204
steps:
- name: Add Rust to the PATH
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
clean: false
- name: Cache dependencies
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
# cache-provider: "buildjet"
- name: Install Linux dependencies
run: ./script/linux
- name: Configure CI
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: Install Rust
shell: bash -euxo pipefail {0}
run: |
cargo install cargo-nextest --locked
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "18"
- name: Limit target directory size
shell: bash -euxo pipefail {0}
run: script/clear-target-dir-if-larger-than 100
- name: Run unit evals
shell: bash -euxo pipefail {0}
run: cargo nextest run --workspace --no-fail-fast --features unit-eval --no-capture -E 'test(::eval_)'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Send failure message to Slack channel if needed
if: ${{ failure() }}
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
with:
method: chat.postMessage
token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}
payload: |
channel: C04UDRNNJFQ
text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
# to clean up the config file, Ive included the cleanup code here as a precaution.
# While its not strictly necessary at this moment, I believe its better to err on the side of caution.
- name: Clean CI config file
if: always()
run: rm -rf ./../.cargo

45
Cargo.lock generated
View File

@@ -211,6 +211,8 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525705e39c11cd73f7bc784e3681a9386aa30c8d0630808d3dc2237eb4f9cb1b"
dependencies = [
"agent-client-protocol-schema",
"anyhow",
@@ -226,7 +228,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol-schema"
version = "0.6.3"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecf16c18fea41282d6bbadd1549a06be6836bddb1893f44a6235f340fa24e2af"
dependencies = [
"anyhow",
"derive_more 2.0.1",
@@ -1335,7 +1339,6 @@ dependencies = [
"settings",
"smol",
"tempfile",
"util",
"which 6.0.3",
"workspace",
]
@@ -1347,7 +1350,6 @@ dependencies = [
"anyhow",
"log",
"simplelog",
"tempfile",
"windows 0.61.3",
"winresource",
]
@@ -4526,15 +4528,12 @@ dependencies = [
"fs",
"futures 0.3.31",
"gpui",
"http_client",
"json_dotpath",
"language",
"log",
"node_runtime",
"paths",
"serde",
"serde_json",
"settings",
"smol",
"task",
"util",
@@ -4933,7 +4932,6 @@ dependencies = [
"editor",
"gpui",
"indoc",
"itertools 0.14.0",
"language",
"log",
"lsp",
@@ -5834,6 +5832,8 @@ name = "extension"
version = "0.1.0"
dependencies = [
"anyhow",
"async-compression",
"async-tar",
"async-trait",
"collections",
"dap",
@@ -6957,7 +6957,7 @@ dependencies = [
[[package]]
name = "gh-workflow"
version = "0.8.0"
source = "git+https://github.com/zed-industries/gh-workflow?rev=3eaa84abca0778eb54272f45a312cb24f9a0b435#3eaa84abca0778eb54272f45a312cb24f9a0b435"
source = "git+https://github.com/zed-industries/gh-workflow?rev=0090c6b6ef82fff02bc8616645953e778d1acc08#0090c6b6ef82fff02bc8616645953e778d1acc08"
dependencies = [
"async-trait",
"derive_more 2.0.1",
@@ -6974,7 +6974,7 @@ dependencies = [
[[package]]
name = "gh-workflow-macros"
version = "0.8.0"
source = "git+https://github.com/zed-industries/gh-workflow?rev=3eaa84abca0778eb54272f45a312cb24f9a0b435#3eaa84abca0778eb54272f45a312cb24f9a0b435"
source = "git+https://github.com/zed-industries/gh-workflow?rev=0090c6b6ef82fff02bc8616645953e778d1acc08#0090c6b6ef82fff02bc8616645953e778d1acc08"
dependencies = [
"heck 0.5.0",
"quote",
@@ -7074,7 +7074,6 @@ dependencies = [
"serde_json",
"settings",
"url",
"urlencoding",
"util",
]
@@ -7113,8 +7112,6 @@ dependencies = [
"picker",
"pretty_assertions",
"project",
"recent_projects",
"remote",
"schemars 1.0.4",
"serde",
"serde_json",
@@ -12714,6 +12711,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
[[package]]
name = "pollster"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "portable-atomic"
version = "1.11.1"
@@ -12762,7 +12765,7 @@ dependencies = [
"log",
"parking_lot",
"pin-project",
"pollster",
"pollster 0.2.5",
"static_assertions",
"thiserror 1.0.69",
]
@@ -14314,7 +14317,6 @@ dependencies = [
"gpui",
"log",
"rand 0.9.2",
"rayon",
"sum_tree",
"unicode-segmentation",
"util",
@@ -16240,6 +16242,7 @@ checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
name = "streaming_diff"
version = "0.1.0"
dependencies = [
"gpui",
"ordered-float 2.10.1",
"rand 0.9.2",
"rope",
@@ -16358,9 +16361,11 @@ version = "0.1.0"
dependencies = [
"arrayvec",
"ctor",
"futures 0.3.31",
"itertools 0.14.0",
"log",
"pollster 0.4.0",
"rand 0.9.2",
"rayon",
"zlog",
]
@@ -18045,7 +18050,7 @@ dependencies = [
[[package]]
name = "tree-sitter-gomod"
version = "1.1.1"
source = "git+https://github.com/camdencheek/tree-sitter-go-mod?rev=2e886870578eeba1927a2dc4bd2e2b3f598c5f9a#2e886870578eeba1927a2dc4bd2e2b3f598c5f9a"
source = "git+https://github.com/camdencheek/tree-sitter-go-mod?rev=6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c#6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c"
dependencies = [
"cc",
"tree-sitter-language",
@@ -18614,7 +18619,6 @@ dependencies = [
"itertools 0.14.0",
"libc",
"log",
"mach2 0.5.0",
"nix 0.29.0",
"pretty_assertions",
"rand 0.9.2",
@@ -21132,7 +21136,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.213.0"
version = "0.212.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -21224,7 +21228,6 @@ dependencies = [
"project_symbols",
"prompt_store",
"proto",
"rayon",
"recent_projects",
"release_channel",
"remote",
@@ -21739,7 +21742,6 @@ dependencies = [
"futures 0.3.31",
"gpui",
"gpui_tokio",
"indoc",
"language",
"language_extension",
"language_model",
@@ -21750,10 +21752,8 @@ dependencies = [
"ordered-float 2.10.1",
"paths",
"polars",
"pretty_assertions",
"project",
"prompt_store",
"pulldown-cmark 0.12.2",
"release_channel",
"reqwest_client",
"serde",
@@ -21763,7 +21763,6 @@ dependencies = [
"smol",
"soa-rs",
"terminal_view",
"toml 0.8.23",
"util",
"watch",
"zeta",

View File

@@ -440,7 +440,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { path = "../agent-client-protocol", features = ["unstable"] }
agent-client-protocol = { version = "0.7.0", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = "0.25.1-rc1"
any_vec = "0.14"
@@ -508,7 +508,7 @@ fork = "0.2.0"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "3eaa84abca0778eb54272f45a312cb24f9a0b435" }
gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "0090c6b6ef82fff02bc8616645953e778d1acc08" }
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
handlebars = "4.3"
@@ -680,7 +680,7 @@ tree-sitter-elixir = "0.3"
tree-sitter-embedded-template = "0.23.0"
tree-sitter-gitcommit = { git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9" }
tree-sitter-go = "0.23"
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "2e886870578eeba1927a2dc4bd2e2b3f598c5f9a", package = "tree-sitter-gomod" }
tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "6efb59652d30e0e9cd5f3b3a669afd6f1a926d3c", package = "tree-sitter-gomod" }
tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" }
tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex", rev = "1dd45142fbb05562e35b2040c6129c9bca346592" }
tree-sitter-html = "0.23"

View File

@@ -8,110 +8,107 @@
; to other areas too.
<all>
= @cole-miller
= @ConradIrwin
= @danilo-leal
= @dinocosta
= @HactarCE
= @kubkon
= @maxdeviant
= @p1n3appl3
= @probably-neb
= @smitbarmase
= @SomeoneToIgnore
= @Veykril
ai
= @benbrandt
= @bennetbo
= @danilo-leal
= @rtfeldman
audio
= @dvdsk
crashes
= @p1n3appl3
= @Veykril
debugger
= @Anthony-Eid
= @kubkon
= @osiewicz
design
= @danilo-leal
docs
= @probably-neb
extension
= @danilo-leal
= @Veykril
= @kubkon
= @p1n3appl3
= @dinocosta
= @smitbarmase
= @cole-miller
= @HactarCE
vim
= @ConradIrwin
= @probably-neb
= @p1n3appl3
= @dinocosta
gpui
= @mikayla-maki
git
= @cole-miller
= @danilo-leal
gpui
= @Anthony-Eid
= @cameron1024
= @mikayla-maki
linux
= @dvdsk
= @smitbarmase
= @p1n3appl3
= @cole-miller
= @probably-neb
windows
= @reflectronic
= @localcc
pickers
= @p1n3appl3
= @dvdsk
= @SomeoneToIgnore
audio
= @dvdsk
helix
= @kubkon
languages
= @osiewicz
= @probably-neb
= @smitbarmase
= @SomeoneToIgnore
= @Veykril
linux
= @cole-miller
= @dvdsk
= @p1n3appl3
= @probably-neb
= @smitbarmase
lsp
= @osiewicz
= @smitbarmase
= @SomeoneToIgnore
= @Veykril
multi_buffer
= @Veykril
= @SomeoneToIgnore
pickers
= @dvdsk
= @p1n3appl3
= @SomeoneToIgnore
project_panel
= @smitbarmase
settings_ui
= @Anthony-Eid
= @danilo-leal
= @probably-neb
tasks
= @SomeoneToIgnore
= @Veykril
terminal
= @kubkon
= @Veykril
vim
= @ConradIrwin
= @dinocosta
debugger
= @kubkon
= @osiewicz
= @Anthony-Eid
extension
= @kubkon
settings_ui
= @probably-neb
= @danilo-leal
= @Anthony-Eid
crashes
= @p1n3appl3
= @Veykril
ai
= @rtfeldman
= @danilo-leal
= @benbrandt
= @bennetbo
design
= @danilo-leal
multi_buffer
= @Veykril
= @SomeoneToIgnore
lsp
= @osiewicz
= @Veykril
= @smitbarmase
= @SomeoneToIgnore
languages
= @osiewicz
= @Veykril
= @smitbarmase
= @SomeoneToIgnore
= @probably-neb
windows
= @localcc
= @reflectronic
project_panel
= @smitbarmase
tasks
= @SomeoneToIgnore
= @Veykril
docs
= @probably-neb

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3335 13.3333L8.00017 10L4.66685 13.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3335 2.66669L8.00017 6.00002L4.66685 2.66669" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 382 B

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -43,8 +43,7 @@
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu",
"ctrl-alt-.": "project_panel::ToggleHideHidden"
"ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -236,13 +235,12 @@
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenSettings",
"ctrl-alt-p": "agent::ManageProfiles",
"ctrl-alt-l": "agent::OpenRulesLibrary",
"ctrl-alt-p": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "agent::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl->": "agent::AddSelectionToThread",
@@ -409,7 +407,6 @@
"bindings": {
"escape": "project_search::ToggleFocus",
"shift-find": "search::FocusSearch",
"shift-enter": "project_search::ToggleAllSearchResults",
"ctrl-shift-f": "search::FocusSearch",
"ctrl-shift-h": "search::ToggleReplace",
"alt-ctrl-g": "search::ToggleRegex",
@@ -482,7 +479,6 @@
"alt-w": "search::ToggleWholeWord",
"alt-find": "project_search::ToggleFilters",
"alt-ctrl-f": "project_search::ToggleFilters",
"shift-enter": "project_search::ToggleAllSearchResults",
"ctrl-alt-shift-r": "search::ToggleRegex",
"ctrl-alt-shift-x": "search::ToggleRegex",
"alt-r": "search::ToggleRegex",
@@ -735,6 +731,14 @@
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && in_snippet",
"use_key_equivalents": true,
"bindings": {
"alt-right": "editor::NextSnippetTabstop",
"alt-left": "editor::PreviousSnippetTabstop"
}
},
// Bindings for accepting edit predictions
//
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
@@ -937,7 +941,6 @@
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-down": "git::PullRebase",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
@@ -1249,14 +1252,6 @@
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
},
{
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-space": "git::WorktreeFromDefaultOnWindow",
"ctrl-space": "git::WorktreeFromDefault"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,

View File

@@ -49,8 +49,7 @@
"ctrl-cmd-f": "zed::ToggleFullScreen",
"ctrl-cmd-z": "edit_prediction::RateCompletions",
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
"ctrl-cmd-l": "lsp_tool::ToggleMenu",
"cmd-alt-.": "project_panel::ToggleHideHidden"
"ctrl-cmd-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -275,13 +274,12 @@
"cmd-alt-n": "agent::NewTextThread",
"cmd-shift-h": "agent::OpenHistory",
"cmd-alt-c": "agent::OpenSettings",
"cmd-alt-l": "agent::OpenRulesLibrary",
"cmd-alt-p": "agent::ManageProfiles",
"cmd-alt-p": "agent::OpenRulesLibrary",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "agent::ToggleModelSelector",
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-shift-j": "agent::ToggleNavigationMenu",
"cmd-alt-m": "agent::ToggleOptionsMenu",
"cmd-shift-i": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"cmd->": "agent::AddSelectionToThread",
@@ -470,7 +468,6 @@
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
"shift-enter": "project_search::ToggleAllSearchResults",
"cmd-shift-f": "search::FocusSearch",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ToggleRegex",
@@ -499,7 +496,6 @@
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
"shift-enter": "project_search::ToggleAllSearchResults",
"cmd-shift-h": "search::ToggleReplace",
"alt-cmd-g": "search::ToggleRegex",
"alt-cmd-x": "search::ToggleRegex"
@@ -805,6 +801,14 @@
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && in_snippet",
"use_key_equivalents": true,
"bindings": {
"alt-right": "editor::NextSnippetTabstop",
"alt-left": "editor::PreviousSnippetTabstop"
}
},
{
"context": "Editor && edit_prediction",
"bindings": {
@@ -1030,7 +1034,6 @@
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-down": "git::PullRebase",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
@@ -1354,14 +1357,6 @@
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
},
{
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-space": "git::WorktreeFromDefaultOnWindow",
"ctrl-space": "git::WorktreeFromDefault"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,

View File

@@ -41,8 +41,7 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-shift-i": "edit_prediction::ToggleMenu",
"shift-alt-l": "lsp_tool::ToggleMenu",
"ctrl-alt-.": "project_panel::ToggleHideHidden"
"shift-alt-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -237,13 +236,12 @@
"shift-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"shift-alt-c": "agent::OpenSettings",
"shift-alt-l": "agent::OpenRulesLibrary",
"shift-alt-p": "agent::ManageProfiles",
"shift-alt-p": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-alt-/": "agent::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu",
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl-shift-.": "agent::AddSelectionToThread",
@@ -490,7 +488,6 @@
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
"alt-f": "project_search::ToggleFilters",
"shift-enter": "project_search::ToggleAllSearchResults",
"alt-r": "search::ToggleRegex",
// "ctrl-shift-alt-x": "search::ToggleRegex",
"ctrl-k shift-enter": "pane::TogglePinTab"
@@ -739,6 +736,14 @@
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && in_snippet",
"use_key_equivalents": true,
"bindings": {
"alt-right": "editor::NextSnippetTabstop",
"alt-left": "editor::PreviousSnippetTabstop"
}
},
// Bindings for accepting edit predictions
//
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
@@ -946,7 +951,6 @@
"ctrl-g ctrl-g": "git::Fetch",
"ctrl-g up": "git::Push",
"ctrl-g down": "git::Pull",
"ctrl-g shift-down": "git::PullRebase",
"ctrl-g shift-up": "git::ForcePush",
"ctrl-g d": "git::Diff",
"ctrl-g backspace": "git::RestoreTrackedFiles",
@@ -1276,14 +1280,6 @@
"shift-alt-a": "onboarding::OpenAccount"
}
},
{
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-space": "git::WorktreeFromDefaultOnWindow",
"ctrl-space": "git::WorktreeFromDefault"
}
},
{
"context": "SettingsWindow",
"use_key_equivalents": true,

View File

@@ -255,19 +255,6 @@
// Whether to display inline and alongside documentation for items in the
// completions menu
"show_completion_documentation": true,
// When to show the scrollbar in the completion menu.
// This setting can take four values:
//
// 1. Show the scrollbar if there's important information or
// follow the system's configured behavior
// "auto"
// 2. Match the system's configured behavior:
// "system"
// 3. Always show the scrollbar:
// "always"
// 4. Never show the scrollbar:
// "never" (default)
"completion_menu_scrollbar": "never",
// Show method signatures in the editor, when inside parentheses.
"auto_signature_help": false,
// Whether to show the signature help after completion or a bracket pair inserted.
@@ -605,7 +592,7 @@
// to both the horizontal and vertical delta values while scrolling. Fast scrolling
// happens when a user holds the alt or option key while scrolling.
"fast_scroll_sensitivity": 4.0,
"relative_line_numbers": "disabled",
"relative_line_numbers": false,
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
"search_wrap": true,
// Search options to enable by default when opening new project and buffer searches.
@@ -615,9 +602,7 @@
"whole_word": false,
"case_sensitive": false,
"include_ignored": false,
"regex": false,
// Whether to center the cursor on each search match when navigating.
"center_on_match": false
"regex": false
},
// When to populate a new search's query based on the text under the cursor.
// This setting can take the following three values:
@@ -1247,9 +1232,6 @@
// that are overly broad can slow down Zed's file scanning. `file_scan_exclusions` takes
// precedence over these inclusions.
"file_scan_inclusions": [".env*"],
// Globs to match files that will be considered "hidden". These files can be hidden from the
// project panel by toggling the "hide_hidden" setting.
"hidden_files": ["**/.*"],
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -1737,9 +1719,6 @@
"allowed": true
}
},
"HTML+ERB": {
"language_servers": ["herb", "!ruby-lsp", "..."]
},
"Java": {
"prettier": {
"allowed": true,
@@ -1762,9 +1741,6 @@
"allowed": true
}
},
"JS+ERB": {
"language_servers": ["!ruby-lsp", "..."]
},
"Kotlin": {
"language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."]
},
@@ -1779,7 +1755,6 @@
"Markdown": {
"format_on_save": "off",
"use_on_type_format": false,
"remove_trailing_whitespace_on_save": false,
"allow_rewrap": "anywhere",
"soft_wrap": "editor_width",
"prettier": {
@@ -1870,9 +1845,6 @@
"allowed": true
}
},
"YAML+ERB": {
"language_servers": ["!ruby-lsp", "..."]
},
"Zig": {
"language_servers": ["zls", "..."]
}

View File

@@ -3,6 +3,7 @@ mod diff;
mod mention;
mod terminal;
use ::terminal::terminal_settings::TerminalSettings;
use agent_settings::AgentSettings;
use collections::HashSet;
pub use connection::*;
@@ -11,7 +12,7 @@ use language::language_settings::FormatOnSave;
pub use mention::*;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
use settings::{Settings as _, SettingsLocation};
use task::{Shell, ShellBuilder};
pub use terminal::*;
@@ -38,10 +39,10 @@ use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle
use uuid::Uuid;
#[derive(Debug)]
pub struct UserMessage<T> {
pub struct UserMessage {
pub id: Option<UserMessageId>,
pub content: ContentBlock,
pub chunks: Vec<acp::ContentBlock<T>>,
pub chunks: Vec<acp::ContentBlock>,
pub checkpoint: Option<Checkpoint>,
}
@@ -51,7 +52,7 @@ pub struct Checkpoint {
pub show: bool,
}
impl<T> UserMessage<T> {
impl UserMessage {
fn to_markdown(&self, cx: &App) -> String {
let mut markdown = String::new();
if self
@@ -116,13 +117,13 @@ impl AssistantMessageChunk {
}
#[derive(Debug)]
pub enum AgentThreadEntry<T> {
UserMessage(UserMessage<T>),
pub enum AgentThreadEntry {
UserMessage(UserMessage),
AssistantMessage(AssistantMessage),
ToolCall(ToolCall),
}
impl<T> AgentThreadEntry<T> {
impl AgentThreadEntry {
pub fn to_markdown(&self, cx: &App) -> String {
match self {
Self::UserMessage(message) => message.to_markdown(cx),
@@ -131,7 +132,7 @@ impl<T> AgentThreadEntry<T> {
}
}
pub fn user_message(&self) -> Option<&UserMessage<T>> {
pub fn user_message(&self) -> Option<&UserMessage> {
if let AgentThreadEntry::UserMessage(message) = self {
Some(message)
} else {
@@ -802,11 +803,9 @@ pub struct RetryStatus {
pub duration: Duration,
}
pub struct AnchoredText;
pub struct AcpThread<T = SharedString> {
title: T,
entries: Vec<AgentThreadEntry<AnchoredText>>,
pub struct AcpThread {
title: SharedString,
entries: Vec<AgentThreadEntry>,
plan: Plan,
project: Entity<Project>,
action_log: Entity<ActionLog>,
@@ -1004,7 +1003,7 @@ impl Display for LoadError {
impl Error for LoadError {}
impl<T> AcpThread<T> {
impl AcpThread {
pub fn new(
title: impl Into<SharedString>,
connection: Rc<dyn AgentConnection>,
@@ -1154,7 +1153,7 @@ impl<T> AcpThread<T> {
pub fn push_user_content_block(
&mut self,
message_id: Option<UserMessageId>,
chunk: acp::ContentBlock<T>,
chunk: acp::ContentBlock,
cx: &mut Context<Self>,
) {
let language_registry = self.project.read(cx).languages().clone();
@@ -1233,7 +1232,7 @@ impl<T> AcpThread<T> {
}
}
fn push_entry(&mut self, entry: AgentThreadEntry<T>, cx: &mut Context<Self>) {
fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
self.entries.push(entry);
cx.emit(AcpThreadEvent::NewEntry);
}
@@ -1926,7 +1925,7 @@ impl<T> AcpThread<T> {
})
}
fn last_user_message(&mut self) -> Option<(usize, &mut UserMessage<T>)> {
fn last_user_message(&mut self) -> Option<(usize, &mut UserMessage)> {
self.entries
.iter_mut()
.enumerate()
@@ -2142,9 +2141,17 @@ impl<T> AcpThread<T> {
) -> Task<Result<Entity<Terminal>>> {
let env = match &cwd {
Some(dir) => self.project.update(cx, |project, cx| {
project.environment().update(cx, |env, cx| {
env.directory_environment(dir.as_path().into(), cx)
})
let worktree = project.find_worktree(dir.as_path(), cx);
let shell = TerminalSettings::get(
worktree.as_ref().map(|(worktree, path)| SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: &path,
}),
cx,
)
.shell
.clone();
project.directory_environment(&shell, dir.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
};

View File

@@ -361,10 +361,12 @@ async fn build_buffer_diff(
) -> Result<Entity<BufferDiff>> {
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
let executor = cx.background_executor().clone();
let old_text_rope = cx
.background_spawn({
let old_text = old_text.clone();
async move { Rope::from(old_text.as_str()) }
let executor = executor.clone();
async move { Rope::from_str(old_text.as_str(), &executor) }
})
.await;
let base_buffer = cx

View File

@@ -5,8 +5,10 @@ use gpui::{App, AppContext, AsyncApp, Context, Entity, Task};
use language::LanguageRegistry;
use markdown::Markdown;
use project::Project;
use settings::{Settings as _, SettingsLocation};
use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
use task::Shell;
use terminal::terminal_settings::TerminalSettings;
use util::get_default_system_shell_preferring_bash;
pub struct Terminal {
@@ -185,9 +187,17 @@ pub async fn create_terminal_entity(
let mut env = if let Some(dir) = &cwd {
project
.update(cx, |project, cx| {
project.environment().update(cx, |env, cx| {
env.directory_environment(dir.clone().into(), cx)
})
let worktree = project.find_worktree(dir.as_path(), cx);
let shell = TerminalSettings::get(
worktree.as_ref().map(|(worktree, path)| SettingsLocation {
worktree_id: worktree.read(cx).id(),
path: &path,
}),
cx,
)
.shell
.clone();
project.directory_environment(&shell, dir.clone().into(), cx)
})?
.await
.unwrap_or_default()

View File

@@ -19,7 +19,7 @@ use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use settings::Settings;
use theme::ThemeSettings;
use ui::{Tooltip, WithScrollbar, prelude::*};
use ui::{Tooltip, prelude::*};
use util::ResultExt as _;
use workspace::{
Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -291,19 +291,17 @@ impl AcpTools {
let expanded = self.expanded.contains(&index);
v_flex()
.id(index)
.group("message")
.cursor_pointer()
.font_buffer(cx)
.w_full()
.px_4()
.py_3()
.pl_4()
.pr_5()
.gap_2()
.items_start()
.text_size(base_size)
.border_color(colors.border)
.border_b_1()
.gap_2()
.items_start()
.font_buffer(cx)
.text_size(base_size)
.id(index)
.group("message")
.hover(|this| this.bg(colors.element_background.opacity(0.5)))
.on_click(cx.listener(move |this, _, _, cx| {
if this.expanded.contains(&index) {
@@ -325,14 +323,15 @@ impl AcpTools {
h_flex()
.w_full()
.gap_2()
.items_center()
.flex_shrink_0()
.child(match message.direction {
acp::StreamMessageDirection::Incoming => Icon::new(IconName::ArrowDown)
.color(Color::Error)
.size(IconSize::Small),
acp::StreamMessageDirection::Outgoing => Icon::new(IconName::ArrowUp)
.color(Color::Success)
.size(IconSize::Small),
acp::StreamMessageDirection::Incoming => {
ui::Icon::new(ui::IconName::ArrowDown).color(Color::Error)
}
acp::StreamMessageDirection::Outgoing => {
ui::Icon::new(ui::IconName::ArrowUp).color(Color::Success)
}
})
.child(
Label::new(message.name.clone())
@@ -502,7 +501,7 @@ impl Focusable for AcpTools {
}
impl Render for AcpTools {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.track_focus(&self.focus_handle)
.size_full()
@@ -517,19 +516,13 @@ impl Render for AcpTools {
.child("No messages recorded yet")
.into_any()
} else {
div()
.size_full()
.flex_grow()
.child(
list(
connection.list_state.clone(),
cx.processor(Self::render_message),
)
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
.size_full(),
)
.vertical_scrollbar_for(connection.list_state.clone(), window, cx)
.into_any()
list(
connection.list_state.clone(),
cx.processor(Self::render_message),
)
.with_sizing_behavior(gpui::ListSizingBehavior::Auto)
.flex_grow()
.into_any()
}
}
None => h_flex()

View File

@@ -3,7 +3,9 @@ use buffer_diff::BufferDiff;
use clock;
use collections::BTreeMap;
use futures::{FutureExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
use gpui::{
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Subscription, Task, WeakEntity,
};
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
@@ -321,6 +323,7 @@ impl ActionLog {
let unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
let edits = diff_snapshots(&old_snapshot, &new_snapshot);
let mut has_user_changes = false;
let executor = cx.background_executor().clone();
async move {
if let ChangeAuthor::User = author {
has_user_changes = apply_non_conflicting_edits(
@@ -328,6 +331,7 @@ impl ActionLog {
edits,
&mut base_text,
new_snapshot.as_rope(),
&executor,
);
}
@@ -382,6 +386,7 @@ impl ActionLog {
let agent_diff_base = tracked_buffer.diff_base.clone();
let git_diff_base = git_diff.read(cx).base_text().as_rope().clone();
let buffer_text = tracked_buffer.snapshot.as_rope().clone();
let executor = cx.background_executor().clone();
anyhow::Ok(cx.background_spawn(async move {
let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable();
let committed_edits = language::line_diff(
@@ -416,8 +421,11 @@ impl ActionLog {
),
new_agent_diff_base.max_point(),
));
new_agent_diff_base
.replace(old_byte_start..old_byte_end, &unreviewed_new);
new_agent_diff_base.replace(
old_byte_start..old_byte_end,
&unreviewed_new,
&executor,
);
row_delta +=
unreviewed.new_len() as i32 - unreviewed.old_len() as i32;
}
@@ -611,6 +619,7 @@ impl ActionLog {
.snapshot
.text_for_range(new_range)
.collect::<String>(),
cx.background_executor(),
);
delta += edit.new_len() as i32 - edit.old_len() as i32;
false
@@ -824,6 +833,7 @@ fn apply_non_conflicting_edits(
edits: Vec<Edit<u32>>,
old_text: &mut Rope,
new_text: &Rope,
executor: &BackgroundExecutor,
) -> bool {
let mut old_edits = patch.edits().iter().cloned().peekable();
let mut new_edits = edits.into_iter().peekable();
@@ -877,6 +887,7 @@ fn apply_non_conflicting_edits(
old_text.replace(
old_bytes,
&new_text.chunks_in_range(new_bytes).collect::<String>(),
executor,
);
applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32;
has_made_changes = true;
@@ -2282,6 +2293,7 @@ mod tests {
old_text.replace(
old_start..old_end,
&new_text.slice_rows(edit.new.clone()).to_string(),
cx.background_executor(),
);
}
pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string());

View File

@@ -13,15 +13,7 @@ const EDITS_END_TAG: &str = "</edits>";
const SEARCH_MARKER: &str = "<<<<<<< SEARCH";
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] = [
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_2,
];
const END_TAGS: [&str; 3] = [OLD_TEXT_END_TAG, NEW_TEXT_END_TAG, EDITS_END_TAG];
#[derive(Debug)]
pub enum EditParserEvent {
@@ -555,37 +547,6 @@ mod tests {
);
}
#[gpui::test(iterations = 1000)]
fn test_xml_edits_with_closing_parameter_invoke(mut rng: StdRng) {
// This case is a regression with Claude Sonnet 4.5.
// Sometimes Sonnet thinks that it's doing a tool call
// and closes its response with '</parameter></invoke>'
// instead of properly closing </new_text>
let mut parser = EditParser::new(EditFormat::XmlTags);
assert_eq!(
parse_random_chunks(
indoc! {"
<old_text>some text</old_text><new_text>updated text</parameter></invoke>
"},
&mut parser,
&mut rng
),
vec![Edit {
old_text: "some text".to_string(),
new_text: "updated text".to_string(),
line_hint: None,
},]
);
assert_eq!(
parser.finish(),
EditParserMetrics {
tags: 2,
mismatched_tags: 1
}
);
}
#[gpui::test(iterations = 1000)]
fn test_xml_nested_tags(mut rng: StdRng) {
let mut parser = EditParser::new(EditFormat::XmlTags);
@@ -1074,11 +1035,6 @@ mod tests {
last_ix = chunk_ix;
}
if new_text.is_some() {
pending_edit.new_text = new_text.take().unwrap();
edits.push(pending_edit);
}
edits
}
}

View File

@@ -305,18 +305,20 @@ impl SearchMatrix {
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
use indoc::indoc;
use language::{BufferId, TextBuffer};
use rand::prelude::*;
use text::ReplicaId;
use util::test::{generate_marked_text, marked_text_ranges};
#[test]
fn test_empty_query() {
#[gpui::test]
fn test_empty_query(cx: &mut gpui::TestAppContext) {
let buffer = TextBuffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
"Hello world\nThis is a test\nFoo bar baz",
cx.background_executor(),
);
let snapshot = buffer.snapshot();
@@ -325,12 +327,13 @@ mod tests {
assert_eq!(finish(finder), None);
}
#[test]
fn test_streaming_exact_match() {
#[gpui::test]
fn test_streaming_exact_match(cx: &mut gpui::TestAppContext) {
let buffer = TextBuffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
"Hello world\nThis is a test\nFoo bar baz",
cx.background_executor(),
);
let snapshot = buffer.snapshot();
@@ -349,8 +352,8 @@ mod tests {
assert_eq!(finish(finder), Some("This is a test".to_string()));
}
#[test]
fn test_streaming_fuzzy_match() {
#[gpui::test]
fn test_streaming_fuzzy_match(cx: &mut gpui::TestAppContext) {
let buffer = TextBuffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
@@ -363,6 +366,7 @@ mod tests {
return x * y;
}
"},
cx.background_executor(),
);
let snapshot = buffer.snapshot();
@@ -383,12 +387,13 @@ mod tests {
);
}
#[test]
fn test_incremental_improvement() {
#[gpui::test]
fn test_incremental_improvement(cx: &mut gpui::TestAppContext) {
let buffer = TextBuffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
"Line 1\nLine 2\nLine 3\nLine 4\nLine 5",
cx.background_executor(),
);
let snapshot = buffer.snapshot();
@@ -408,8 +413,8 @@ mod tests {
assert_eq!(finish(finder), Some("Line 3\nLine 4".to_string()));
}
#[test]
fn test_incomplete_lines_buffering() {
#[gpui::test]
fn test_incomplete_lines_buffering(cx: &mut gpui::TestAppContext) {
let buffer = TextBuffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
@@ -418,6 +423,7 @@ mod tests {
jumps over the lazy dog
Pack my box with five dozen liquor jugs
"},
cx.background_executor(),
);
let snapshot = buffer.snapshot();
@@ -435,8 +441,8 @@ mod tests {
);
}
#[test]
fn test_multiline_fuzzy_match() {
#[gpui::test]
fn test_multiline_fuzzy_match(cx: &mut gpui::TestAppContext) {
let buffer = TextBuffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
@@ -456,6 +462,7 @@ mod tests {
}
}
"#},
cx.background_executor(),
);
let snapshot = buffer.snapshot();
@@ -509,7 +516,7 @@ mod tests {
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_single_line(mut rng: StdRng) {
fn test_resolve_location_single_line(mut rng: StdRng, cx: &mut TestAppContext) {
assert_location_resolution(
concat!(
" Lorem\n",
@@ -519,11 +526,12 @@ mod tests {
),
"ipsum",
&mut rng,
cx,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_multiline(mut rng: StdRng) {
fn test_resolve_location_multiline(mut rng: StdRng, cx: &mut TestAppContext) {
assert_location_resolution(
concat!(
" Lorem\n",
@@ -533,11 +541,12 @@ mod tests {
),
"ipsum\ndolor sit amet",
&mut rng,
cx,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_function_with_typo(mut rng: StdRng) {
fn test_resolve_location_function_with_typo(mut rng: StdRng, cx: &mut TestAppContext) {
assert_location_resolution(
indoc! {"
«fn foo1(a: usize) -> usize {
@@ -550,11 +559,12 @@ mod tests {
"},
"fn foo1(a: usize) -> u32 {\n40\n}",
&mut rng,
cx,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_class_methods(mut rng: StdRng) {
fn test_resolve_location_class_methods(mut rng: StdRng, cx: &mut TestAppContext) {
assert_location_resolution(
indoc! {"
class Something {
@@ -575,11 +585,12 @@ mod tests {
six() { return 6666; }
"},
&mut rng,
cx,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_imports_no_match(mut rng: StdRng) {
fn test_resolve_location_imports_no_match(mut rng: StdRng, cx: &mut TestAppContext) {
assert_location_resolution(
indoc! {"
use std::ops::Range;
@@ -609,11 +620,12 @@ mod tests {
use std::sync::Arc;
"},
&mut rng,
cx,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_nested_closure(mut rng: StdRng) {
fn test_resolve_location_nested_closure(mut rng: StdRng, cx: &mut TestAppContext) {
assert_location_resolution(
indoc! {"
impl Foo {
@@ -641,11 +653,12 @@ mod tests {
" });",
),
&mut rng,
cx,
);
}
#[gpui::test(iterations = 100)]
fn test_resolve_location_tool_invocation(mut rng: StdRng) {
fn test_resolve_location_tool_invocation(mut rng: StdRng, cx: &mut TestAppContext) {
assert_location_resolution(
indoc! {r#"
let tool = cx
@@ -673,11 +686,12 @@ mod tests {
" .output;",
),
&mut rng,
cx,
);
}
#[gpui::test]
fn test_line_hint_selection() {
fn test_line_hint_selection(cx: &mut TestAppContext) {
let text = indoc! {r#"
fn first_function() {
return 42;
@@ -696,6 +710,7 @@ mod tests {
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
text.to_string(),
cx.background_executor(),
);
let snapshot = buffer.snapshot();
let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
@@ -727,9 +742,19 @@ mod tests {
}
#[track_caller]
fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) {
fn assert_location_resolution(
text_with_expected_range: &str,
query: &str,
rng: &mut StdRng,
cx: &mut TestAppContext,
) {
let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false);
let buffer = TextBuffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text.clone());
let buffer = TextBuffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
text.clone(),
cx.background_executor(),
);
let snapshot = buffer.snapshot();
let mut matcher = StreamingFuzzyMatcher::new(snapshot);

View File

@@ -569,6 +569,7 @@ mod tests {
use prompt_store::ProjectContext;
use serde_json::json;
use settings::SettingsStore;
use text::Rope;
use util::{path, rel_path::rel_path};
#[gpui::test]
@@ -741,7 +742,7 @@ mod tests {
// Create the file
fs.save(
path!("/root/src/main.rs").as_ref(),
&"initial content".into(),
&Rope::from_str_small("initial content"),
language::LineEnding::Unix,
)
.await
@@ -908,7 +909,7 @@ mod tests {
// Create a simple file with trailing whitespace
fs.save(
path!("/root/src/main.rs").as_ref(),
&"initial content".into(),
&Rope::from_str_small("initial content"),
language::LineEnding::Unix,
)
.await

View File

@@ -178,7 +178,6 @@ impl AcpConnection {
meta: Some(serde_json::json!({
// Experimental: Allow for rendering terminal output from the agents
"terminal_output": true,
"terminal-auth": true,
})),
},
client_info: Some(acp::Implementation {

View File

@@ -4,7 +4,6 @@ mod message_editor;
mod mode_selector;
mod model_selector;
mod model_selector_popover;
mod thread_editor;
mod thread_history;
mod thread_view;

View File

@@ -4,7 +4,7 @@ use acp_thread::{AcpThread, AgentThreadEntry};
use agent::HistoryStore;
use agent_client_protocol::{self as acp, ToolCallId};
use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility, SizingBehavior};
use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
ScrollHandle, SharedString, TextStyleRefinement, WeakEntity, Window,
@@ -357,7 +357,7 @@ fn create_editor_diff(
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sizing_behavior: SizingBehavior::SizeByContent,
sized_by_content: true,
},
diff.read(cx).multibuffer().clone(),
None,

View File

@@ -1,10 +1,8 @@
use acp_thread::AgentSessionModes;
use agent_client_protocol as acp;
use agent_servers::AgentServer;
use agent_settings::AgentSettings;
use fs::Fs;
use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
use settings::Settings as _;
use std::{rc::Rc, sync::Arc};
use ui::{
Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding,
@@ -86,14 +84,6 @@ impl ModeSelector {
let current_mode = self.connection.current_mode();
let default_mode = self.agent_server.default_mode(cx);
let settings = AgentSettings::get_global(cx);
let side = match settings.dock {
settings::DockPosition::Left => DocumentationSide::Right,
settings::DockPosition::Bottom | settings::DockPosition::Right => {
DocumentationSide::Left
}
};
for mode in all_modes {
let is_selected = &mode.id == &current_mode;
let is_default = Some(&mode.id) == default_mode.as_ref();
@@ -101,7 +91,7 @@ impl ModeSelector {
.toggleable(IconPosition::End, is_selected);
let entry = if let Some(description) = &mode.description {
entry.documentation_aside(side, DocumentationEdge::Bottom, {
entry.documentation_aside(DocumentationSide::Left, DocumentationEdge::Bottom, {
let description = description.clone();
move |cx| {

View File

@@ -1,6 +0,0 @@
use acp_thread::AcpThread;
use gpui::Entity;
pub struct ThreadEditor {
thread: Entity<AcpThread>,
}

View File

@@ -17,9 +17,7 @@ use client::zed_urls;
use cloud_llm_client::PlanV1;
use collections::{HashMap, HashSet};
use editor::scroll::Autoscroll;
use editor::{
Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior,
};
use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects};
use file_icons::FileIcons;
use fs::Fs;
use futures::FutureExt as _;
@@ -104,7 +102,7 @@ impl ThreadError {
{
Self::AuthenticationRequired(acp_error.message.clone().into())
} else {
let string = format!("{:#}", error);
let string = error.to_string();
// TODO: we should have Gemini return better errors here.
if agent.clone().downcast::<agent_servers::Gemini>().is_some()
&& string.contains("Could not load the default credentials")
@@ -113,7 +111,7 @@ impl ThreadError {
{
Self::AuthenticationRequired(string.into())
} else {
Self::Other(string.into())
Self::Other(error.to_string().into())
}
}
}
@@ -795,8 +793,7 @@ impl AcpThreadView {
if let Some(load_err) = err.downcast_ref::<LoadError>() {
self.thread_state = ThreadState::LoadError(load_err.clone());
} else {
self.thread_state =
ThreadState::LoadError(LoadError::Other(format!("{:#}", err).into()))
self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
}
if self.message_editor.focus_handle(cx).is_focused(window) {
self.focus_handle.focus(window)
@@ -884,7 +881,6 @@ impl AcpThreadView {
cx: &mut Context<Self>,
) {
self.set_editor_is_expanded(!self.editor_expanded, cx);
cx.stop_propagation();
cx.notify();
}
@@ -896,7 +892,7 @@ impl AcpThreadView {
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sizing_behavior: SizingBehavior::ExcludeOverscrollMargin,
sized_by_content: false,
},
cx,
)
@@ -1473,114 +1469,6 @@ impl AcpThreadView {
return;
};
// Check for the experimental "terminal-auth" _meta field
let auth_method = connection.auth_methods().iter().find(|m| m.id == method);
if let Some(auth_method) = auth_method {
if let Some(meta) = &auth_method.meta {
if let Some(terminal_auth) = meta.get("terminal-auth") {
// Extract terminal auth details from meta
if let (Some(command), Some(label)) = (
terminal_auth.get("command").and_then(|v| v.as_str()),
terminal_auth.get("label").and_then(|v| v.as_str()),
) {
let args = terminal_auth
.get("args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let env = terminal_auth
.get("env")
.and_then(|v| v.as_object())
.map(|obj| {
obj.iter()
.filter_map(|(k, v)| {
v.as_str().map(|val| (k.clone(), val.to_string()))
})
.collect::<HashMap<String, String>>()
})
.unwrap_or_default();
// Run SpawnInTerminal in the same dir as the ACP server
let cwd = connection
.clone()
.downcast::<agent_servers::AcpConnection>()
.map(|acp_conn| acp_conn.root_dir().to_path_buf());
// Build SpawnInTerminal from _meta
let login = task::SpawnInTerminal {
id: task::TaskId(format!("external-agent-{}-login", label)),
full_label: label.to_string(),
label: label.to_string(),
command: Some(command.to_string()),
args,
command_label: label.to_string(),
cwd,
env,
use_new_terminal: true,
allow_concurrent_runs: true,
hide: task::HideStrategy::Always,
..Default::default()
};
self.thread_error.take();
configuration_view.take();
pending_auth_method.replace(method.clone());
if let Some(workspace) = self.workspace.upgrade() {
let project = self.project.clone();
let authenticate = Self::spawn_external_agent_login(
login, workspace, project, false, true, window, cx,
);
cx.notify();
self.auth_task = Some(cx.spawn_in(window, {
let agent = self.agent.clone();
async move |this, cx| {
let result = authenticate.await;
match &result {
Ok(_) => telemetry::event!(
"Authenticate Agent Succeeded",
agent = agent.telemetry_id()
),
Err(_) => {
telemetry::event!(
"Authenticate Agent Failed",
agent = agent.telemetry_id(),
)
}
}
this.update_in(cx, |this, window, cx| {
if let Err(err) = result {
if let ThreadState::Unauthenticated {
pending_auth_method,
..
} = &mut this.thread_state
{
pending_auth_method.take();
}
this.handle_thread_error(err, cx);
} else {
this.reset(window, cx);
}
this.auth_task.take()
})
.ok();
}
}));
}
return;
}
}
}
}
if method.0.as_ref() == "gemini-api-key" {
let registry = LanguageModelRegistry::global(cx);
let provider = registry
@@ -1679,10 +1567,7 @@ impl AcpThreadView {
&& let Some(login) = self.login.clone()
{
if let Some(workspace) = self.workspace.upgrade() {
let project = self.project.clone();
Self::spawn_external_agent_login(
login, workspace, project, false, false, window, cx,
)
Self::spawn_external_agent_login(login, workspace, false, window, cx)
} else {
Task::ready(Ok(()))
}
@@ -1732,40 +1617,17 @@ impl AcpThreadView {
fn spawn_external_agent_login(
login: task::SpawnInTerminal,
workspace: Entity<Workspace>,
project: Entity<Project>,
previous_attempt: bool,
check_exit_code: bool,
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>> {
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return Task::ready(Ok(()));
};
let project = workspace.read(cx).project().clone();
window.spawn(cx, async move |cx| {
let mut task = login.clone();
if let Some(cmd) = &task.command {
// Have "node" command use Zed's managed Node runtime by default
if cmd == "node" {
let resolved_node_runtime = project
.update(cx, |project, cx| {
let agent_server_store = project.agent_server_store().clone();
agent_server_store.update(cx, |store, cx| {
store.node_runtime().map(|node_runtime| {
cx.background_spawn(async move {
node_runtime.binary_path().await
})
})
})
});
if let Ok(Some(resolve_task)) = resolved_node_runtime {
if let Ok(node_path) = resolve_task.await {
task.command = Some(node_path.to_string_lossy().to_string());
}
}
}
}
task.shell = task::Shell::WithArguments {
program: task.command.take().expect("login command should be set"),
args: std::mem::take(&mut task.args),
@@ -1783,65 +1645,44 @@ impl AcpThreadView {
})?;
let terminal = terminal.await?;
let mut exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.fuse();
if check_exit_code {
// For extension-based auth, wait for the process to exit and check exit code
let exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.await;
match exit_status {
Some(status) if status.success() => {
Ok(())
}
Some(status) => {
Err(anyhow!("Login command failed with exit code: {:?}", status.code()))
}
None => {
Err(anyhow!("Login command terminated without exit status"))
}
}
} else {
// For hardcoded agents (claude-login, gemini-cli): look for specific output
let mut exit_status = terminal
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.fuse();
let logged_in = cx
.spawn({
let terminal = terminal.clone();
async move |cx| {
loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
let content =
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
if content.contains("Login successful")
|| content.contains("Type your message")
{
return anyhow::Ok(());
}
let logged_in = cx
.spawn({
let terminal = terminal.clone();
async move |cx| {
loop {
cx.background_executor().timer(Duration::from_secs(1)).await;
let content =
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
if content.contains("Login successful")
|| content.contains("Type your message")
{
return anyhow::Ok(());
}
}
})
.fuse();
futures::pin_mut!(logged_in);
futures::select_biased! {
result = logged_in => {
if let Err(e) = result {
log::error!("{e}");
return Err(anyhow!("exited before logging in"));
}
}
_ = exit_status => {
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), true, false, window, cx))?.await
}
})
.fuse();
futures::pin_mut!(logged_in);
futures::select_biased! {
result = logged_in => {
if let Err(e) = result {
log::error!("{e}");
return Err(anyhow!("exited before logging in"));
}
}
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
Ok(())
_ = exit_status => {
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, true, window, cx))?.await
}
return Err(anyhow!("exited before logging in"));
}
}
terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
Ok(())
})
}
@@ -2106,15 +1947,6 @@ impl AcpThreadView {
.into_any(),
};
let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry {
matches!(
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
)
} else {
false
};
let Some(thread) = self.thread() else {
return primary;
};
@@ -2123,13 +1955,7 @@ impl AcpThreadView {
v_flex()
.w_full()
.child(primary)
.map(|this| {
if needs_confirmation {
this.child(self.render_generating(true))
} else {
this.child(self.render_thread_controls(&thread, cx))
}
})
.child(self.render_thread_controls(&thread, cx))
.when_some(
self.thread_feedback.comments_editor.clone(),
|this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)),
@@ -3805,7 +3631,6 @@ impl AcpThreadView {
.child(
h_flex()
.id("edits-container")
.cursor_pointer()
.gap_1()
.child(Disclosure::new("edits-disclosure", expanded))
.map(|this| {
@@ -3945,7 +3770,6 @@ impl AcpThreadView {
Label::new(name.to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
.ml_1p5()
});
let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
@@ -3977,30 +3801,14 @@ impl AcpThreadView {
})
.child(
h_flex()
.id(("file-name-row", index))
.relative()
.id(("file-name", index))
.pr_8()
.gap_1p5()
.w_full()
.overflow_x_scroll()
.child(
h_flex()
.id(("file-name-path", index))
.cursor_pointer()
.pr_0p5()
.gap_0p5()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.rounded_xs()
.child(file_icon)
.children(file_name)
.children(file_path)
.tooltip(Tooltip::text("Go to File"))
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.open_edited_buffer(&buffer, window, cx);
})
}),
)
.child(file_icon)
.child(h_flex().gap_0p5().children(file_name).children(file_path))
.child(
div()
.absolute()
@@ -4010,7 +3818,13 @@ impl AcpThreadView {
.bottom_0()
.right_0()
.bg(overlay_gradient),
),
)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.open_edited_buffer(&buffer, window, cx);
})
}),
)
.child(
h_flex()
@@ -4152,12 +3966,8 @@ impl AcpThreadView {
)
}
})
.on_click(cx.listener(|this, _, window, cx| {
this.expand_message_editor(
&ExpandMessageEditor,
window,
cx,
);
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(Box::new(ExpandMessageEditor), cx);
})),
),
),
@@ -4761,29 +4571,14 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) {
if !self.notifications.is_empty() {
return;
}
let settings = AgentSettings::get_global(cx);
let window_is_inactive = !window.is_window_active();
let panel_is_hidden = self
.workspace
.upgrade()
.map(|workspace| AgentPanel::is_hidden(&workspace, cx))
.unwrap_or(true);
let should_notify = window_is_inactive || panel_is_hidden;
if !should_notify {
if window.is_window_active() || !self.notifications.is_empty() {
return;
}
// TODO: Change this once we have title summarization for external agents.
let title = self.agent.name();
match settings.notify_when_agent_waiting {
match AgentSettings::get_global(cx).notify_when_agent_waiting {
NotifyWhenAgentWaiting::PrimaryScreen => {
if let Some(primary) = cx.primary_display() {
self.pop_up(icon, caption.into(), title, window, primary, cx);
@@ -4899,31 +4694,6 @@ impl AcpThreadView {
}
}
fn render_generating(&self, confirmation: bool) -> impl IntoElement {
h_flex()
.id("generating-spinner")
.py_2()
.px(rems_from_px(22.))
.map(|this| {
if confirmation {
this.gap_2()
.child(
h_flex()
.w_2()
.child(SpinnerLabel::sand().size(LabelSize::Small)),
)
.child(
LoadingLabel::new("Waiting Confirmation")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else {
this.child(SpinnerLabel::new().size(LabelSize::Small))
}
})
.into_any_element()
}
fn render_thread_controls(
&self,
thread: &Entity<AcpThread>,
@@ -4931,7 +4701,12 @@ impl AcpThreadView {
) -> impl IntoElement {
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if is_generating {
return self.render_generating(false).into_any_element();
return h_flex().id("thread-controls-container").child(
div()
.py_2()
.px(rems_from_px(22.))
.child(SpinnerLabel::new().size(LabelSize::Small)),
);
}
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
@@ -5019,10 +4794,7 @@ impl AcpThreadView {
);
}
container
.child(open_as_markdown)
.child(scroll_to_top)
.into_any_element()
container.child(open_as_markdown).child(scroll_to_top)
}
fn render_feedback_feedback_editor(editor: Entity<Editor>, cx: &Context<Self>) -> Div {
@@ -5809,7 +5581,7 @@ fn default_markdown_style(
let theme_settings = ThemeSettings::get_global(cx);
let colors = cx.theme().colors();
let buffer_font_size = theme_settings.agent_buffer_font_size(cx);
let buffer_font_size = TextSize::Small.rems(cx);
let mut text_style = window.text_style();
let line_height = buffer_font_size * 1.75;
@@ -5821,9 +5593,9 @@ fn default_markdown_style(
};
let font_size = if buffer_font {
theme_settings.agent_buffer_font_size(cx)
TextSize::Small.rems(cx)
} else {
theme_settings.agent_ui_font_size(cx)
TextSize::Default.rems(cx)
};
let text_color = if muted_text {
@@ -6120,107 +5892,6 @@ pub(crate) mod tests {
);
}
#[gpui::test]
async fn test_notification_when_panel_hidden(cx: &mut TestAppContext) {
init_test(cx);
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
add_to_workspace(thread_view.clone(), cx);
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
// Window is active (don't deactivate), but panel will be hidden
// Note: In the test environment, the panel is not actually added to the dock,
// so is_agent_panel_hidden will return true
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.send(window, cx);
});
cx.run_until_parked();
// Should show notification because window is active but panel is hidden
assert!(
cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some()),
"Expected notification when panel is hidden"
);
}
#[gpui::test]
async fn test_notification_still_works_when_window_inactive(cx: &mut TestAppContext) {
init_test(cx);
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
// Deactivate window - should show notification regardless of setting
cx.deactivate_window();
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.send(window, cx);
});
cx.run_until_parked();
// Should still show notification when window is inactive (existing behavior)
assert!(
cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some()),
"Expected notification when window is inactive"
);
}
#[gpui::test]
async fn test_notification_respects_never_setting(cx: &mut TestAppContext) {
init_test(cx);
// Set notify_when_agent_waiting to Never
cx.update(|cx| {
AgentSettings::override_global(
AgentSettings {
notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
..AgentSettings::get_global(cx).clone()
},
cx,
);
});
let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
// Window is active
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.send(window, cx);
});
cx.run_until_parked();
// Should NOT show notification because notify_when_agent_waiting is Never
assert!(
!cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some()),
"Expected no notification when notify_when_agent_waiting is Never"
);
}
async fn setup_thread_view(
agent: impl AgentServer + 'static,
cx: &mut TestAppContext,

View File

@@ -23,17 +23,16 @@ use language::LanguageRegistry;
use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
use language_models::AllLanguageModelSettings;
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
};
use settings::{Settings, SettingsStore, update_settings_file};
use rope::Rope;
use settings::{SettingsStore, update_settings_file};
use ui::{
Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor,
ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, PopoverMenu, Switch,
SwitchColor, Tooltip, WithScrollbar, prelude::*,
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
Indicator, PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
@@ -154,42 +153,7 @@ pub enum AssistantConfigurationEvent {
impl EventEmitter<AssistantConfigurationEvent> for AgentConfiguration {}
enum AgentIcon {
Name(IconName),
Path(SharedString),
}
impl AgentConfiguration {
fn render_section_title(
&mut self,
title: impl Into<SharedString>,
description: impl Into<SharedString>,
menu: AnyElement,
) -> impl IntoElement {
h_flex()
.p_4()
.pb_0()
.mb_2p5()
.items_start()
.justify_between()
.child(
v_flex()
.w_full()
.gap_0p5()
.child(
h_flex()
.pr_1()
.w_full()
.gap_2()
.justify_between()
.flex_wrap()
.child(Headline::new(title.into()))
.child(menu),
)
.child(Label::new(description.into()).color(Color::Muted)),
)
}
fn render_provider_configuration_block(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
@@ -324,7 +288,7 @@ impl AgentConfiguration {
"Start New Thread",
)
.full_width()
.style(ButtonStyle::Outlined)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Thread)
@@ -340,122 +304,89 @@ impl AgentConfiguration {
}
})),
)
})
.when(
is_expanded && is_removable_provider(&provider.id(), cx),
|this| {
this.child(
Button::new(
SharedString::from(format!("delete-provider-{provider_id}")),
"Remove Provider",
)
.full_width()
.style(ButtonStyle::Outlined)
.icon_position(IconPosition::Start)
.icon(IconName::Trash)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(cx.listener({
let provider = provider.clone();
move |this, _event, window, cx| {
this.delete_provider(provider.clone(), window, cx);
}
})),
)
},
),
}),
)
}
fn delete_provider(
&mut self,
provider: Arc<dyn LanguageModelProvider>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let fs = self.fs.clone();
let provider_id = provider.id();
cx.spawn_in(window, async move |_, cx| {
cx.update(|_window, cx| {
update_settings_file(fs.clone(), cx, {
let provider_id = provider_id.clone();
move |settings, _| {
if let Some(ref mut openai_compatible) = settings
.language_models
.as_mut()
.and_then(|lm| lm.openai_compatible.as_mut())
{
let key_to_remove: Arc<str> = Arc::from(provider_id.0.as_ref());
openai_compatible.remove(&key_to_remove);
}
}
});
})
.log_err();
cx.update(|_window, cx| {
LanguageModelRegistry::global(cx).update(cx, {
let provider_id = provider_id.clone();
move |registry, cx| {
registry.unregister_provider(provider_id, cx);
}
})
})
.log_err();
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn render_provider_configuration_section(
&mut self,
cx: &mut Context<Self>,
) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let popover_menu = PopoverMenu::new("add-provider-popover")
.trigger(
Button::new("add-provider", "Add Provider")
.style(ButtonStyle::Outlined)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.anchor(gpui::Corner::TopRight)
.menu({
let workspace = self.workspace.clone();
move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.header("Compatible APIs").entry("OpenAI", None, {
let workspace = workspace.clone();
move |window, cx| {
workspace
.update(cx, |workspace, cx| {
AddLlmProviderModal::toggle(
LlmCompatibleProvider::OpenAi,
workspace,
window,
cx,
);
})
.log_err();
}
})
}))
}
});
v_flex()
.w_full()
.child(self.render_section_title(
"LLM Providers",
"Add at least one provider to use AI-powered features with Zed's native agent.",
popover_menu.into_any_element(),
))
.child(
h_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.pb_0()
.mb_2p5()
.items_start()
.justify_between()
.child(
v_flex()
.w_full()
.gap_0p5()
.child(
h_flex()
.pr_1()
.w_full()
.gap_2()
.justify_between()
.child(Headline::new("LLM Providers"))
.child(
PopoverMenu::new("add-provider-popover")
.trigger(
Button::new("add-provider", "Add Provider")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.anchor(gpui::Corner::TopRight)
.menu({
let workspace = self.workspace.clone();
move |window, cx| {
Some(ContextMenu::build(
window,
cx,
|menu, _window, _cx| {
menu.header("Compatible APIs").entry(
"OpenAI",
None,
{
let workspace =
workspace.clone();
move |window, cx| {
workspace
.update(cx, |workspace, cx| {
AddLlmProviderModal::toggle(
LlmCompatibleProvider::OpenAi,
workspace,
window,
cx,
);
})
.log_err();
}
},
)
},
))
}
}),
),
)
.child(
Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
.color(Color::Muted),
),
),
)
.child(
div()
.w_full()
@@ -534,7 +465,8 @@ impl AgentConfiguration {
let add_server_popover = PopoverMenu::new("add-server-popover")
.trigger(
Button::new("add-server", "Add Server")
.style(ButtonStyle::Outlined)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
@@ -567,57 +499,61 @@ impl AgentConfiguration {
});
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(self.render_section_title(
"Model Context Protocol (MCP) Servers",
"All MCP servers connected directly or via a Zed extension.",
add_server_popover.into_any_element(),
))
.child(
v_flex()
.pl_4()
.pb_4()
.pr_5()
h_flex()
.w_full()
.items_start()
.justify_between()
.gap_1()
.map(|mut parent| {
if context_server_ids.is_empty() {
parent.child(
h_flex()
.p_4()
.justify_center()
.border_1()
.border_dashed()
.border_color(cx.theme().colors().border.opacity(0.6))
.rounded_sm()
.child(
Label::new("No MCP servers added yet.")
.color(Color::Muted)
.size(LabelSize::Small),
),
)
} else {
for (index, context_server_id) in
context_server_ids.into_iter().enumerate()
{
if index > 0 {
parent = parent.child(
Divider::horizontal()
.color(DividerColor::BorderFaded)
.into_any_element(),
);
}
parent = parent.child(self.render_context_server(
context_server_id,
window,
cx,
));
}
parent
}
}),
.child(
v_flex()
.gap_0p5()
.child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(
Label::new(
"All MCP servers connected directly or via a Zed extension.",
)
.color(Color::Muted),
),
)
.child(add_server_popover),
)
.child(v_flex().w_full().gap_1().map(|mut parent| {
if context_server_ids.is_empty() {
parent.child(
h_flex()
.p_4()
.justify_center()
.border_1()
.border_dashed()
.border_color(cx.theme().colors().border.opacity(0.6))
.rounded_sm()
.child(
Label::new("No MCP servers added yet.")
.color(Color::Muted)
.size(LabelSize::Small),
),
)
} else {
for (index, context_server_id) in context_server_ids.into_iter().enumerate() {
if index > 0 {
parent = parent.child(
Divider::horizontal()
.color(DividerColor::BorderFaded)
.into_any_element(),
);
}
parent =
parent.child(self.render_context_server(context_server_id, window, cx));
}
parent
}
}))
}
fn render_context_server(
@@ -662,12 +598,12 @@ impl AgentConfiguration {
let (source_icon, source_tooltip) = if is_from_extension {
(
IconName::ZedSrcExtension,
IconName::ZedMcpExtension,
"This MCP server was installed from an extension.",
)
} else {
(
IconName::ZedSrcCustom,
IconName::ZedMcpCustom,
"This custom MCP server was installed directly.",
)
};
@@ -949,9 +885,9 @@ impl AgentConfiguration {
}
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let agent_server_store = self.agent_server_store.read(cx);
let user_defined_agents = agent_server_store
let user_defined_agents = self
.agent_server_store
.read(cx)
.external_agents()
.filter(|name| {
name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME
@@ -962,121 +898,102 @@ impl AgentConfiguration {
let user_defined_agents = user_defined_agents
.into_iter()
.map(|name| {
let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
AgentIcon::Path(icon_path)
} else {
AgentIcon::Name(IconName::Ai)
};
self.render_agent_server(icon, name, true)
self.render_agent_server(IconName::Ai, name)
.into_any_element()
})
.collect::<Vec<_>>();
let add_agens_button = Button::new("add-agent", "Add Agent")
.style(ButtonStyle::Outlined)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(move |_, window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(workspace, cx).await
})
.detach_and_log_err(cx);
}
});
v_flex()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.child(self.render_section_title(
"External Agents",
"All agents connected through the Agent Client Protocol.",
add_agens_button.into_any_element(),
))
.p(DynamicSpacing::Base16.rems(cx))
.pr(DynamicSpacing::Base20.rems(cx))
.gap_2()
.child(
v_flex()
.p_4()
.pt_0()
.gap_2()
.child(self.render_agent_server(
AgentIcon::Name(IconName::AiClaude),
"Claude Code",
false,
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
AgentIcon::Name(IconName::AiOpenAi),
"Codex CLI",
false,
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
AgentIcon::Name(IconName::AiGemini),
"Gemini CLI",
false,
))
.map(|mut parent| {
for agent in user_defined_agents {
parent = parent
.child(
Divider::horizontal().color(DividerColor::BorderFaded),
)
.child(agent);
}
parent
}),
),
.gap_0p5()
.child(
h_flex()
.pr_1()
.w_full()
.gap_2()
.justify_between()
.child(Headline::new("External Agents"))
.child(
Button::new("add-agent", "Add Agent")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(
move |_, window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(
workspace,
cx,
).await
})
.detach_and_log_err(cx);
}
}
),
)
)
.child(
Label::new(
"All agents connected through the Agent Client Protocol.",
)
.color(Color::Muted),
),
)
.child(self.render_agent_server(
IconName::AiClaude,
"Claude Code",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiOpenAi,
"Codex",
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
IconName::AiGemini,
"Gemini CLI",
))
.map(|mut parent| {
for agent in user_defined_agents {
parent = parent.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(agent);
}
parent
})
)
}
fn render_agent_server(
&self,
icon: AgentIcon,
icon: IconName,
name: impl Into<SharedString>,
external: bool,
) -> impl IntoElement {
let name = name.into();
let icon = match icon {
AgentIcon::Name(icon_name) => Icon::new(icon_name)
.size(IconSize::Small)
.color(Color::Muted),
AgentIcon::Path(icon_path) => Icon::from_path(icon_path)
.size(IconSize::Small)
.color(Color::Muted),
};
let tooltip_id = SharedString::new(format!("agent-source-{}", name));
let tooltip_message = format!("The {} agent was installed from an extension.", name);
h_flex()
.gap_1p5()
.child(icon)
.child(Label::new(name))
.when(external, |this| {
this.child(
div()
.id(tooltip_id)
.flex_none()
.tooltip(Tooltip::text(tooltip_message))
.child(
Icon::new(IconName::ZedSrcExtension)
.size(IconSize::Small)
.color(Color::Muted),
),
)
})
.child(
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::Small),
)
h_flex().gap_1p5().justify_between().child(
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(name.into()))
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
),
)
}
}
@@ -1198,8 +1115,11 @@ async fn open_new_agent_servers_entry_in_settings_editor(
) -> Result<()> {
let settings_editor = workspace
.update_in(cx, |_, window, cx| {
create_and_open_local_file(paths::settings_file(), window, cx, || {
settings::initial_user_settings_content().as_ref().into()
create_and_open_local_file(paths::settings_file(), window, cx, |cx| {
Rope::from_str(
&settings::initial_user_settings_content(),
cx.background_executor(),
)
})
})?
.await?
@@ -1305,14 +1225,3 @@ fn find_text_in_buffer(
None
}
}
// OpenAI-compatible providers are user-configured and can be removed,
// whereas built-in providers (like Anthropic, OpenAI, Google, etc.) can't.
//
// If in the future we have more "API-compatible-type" of providers,
// they should be included here as removable providers.
fn is_removable_provider(provider_id: &LanguageModelProviderId, cx: &App) -> bool {
AllLanguageModelSettings::get_global(cx)
.openai_compatible
.contains_key(provider_id.0.as_ref())
}

View File

@@ -70,6 +70,14 @@ impl AgentDiffThread {
}
}
fn is_generating(&self, cx: &App) -> bool {
match self {
AgentDiffThread::AcpThread(thread) => {
thread.read(cx).status() == acp_thread::ThreadStatus::Generating
}
}
}
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
match self {
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
@@ -962,7 +970,9 @@ impl AgentDiffToolbar {
None => ToolbarItemLocation::Hidden,
Some(AgentDiffToolbarItem::Pane(_)) => ToolbarItemLocation::PrimaryRight,
Some(AgentDiffToolbarItem::Editor { state, .. }) => match state {
EditorState::Reviewing => ToolbarItemLocation::PrimaryRight,
EditorState::Generating | EditorState::Reviewing => {
ToolbarItemLocation::PrimaryRight
}
EditorState::Idle => ToolbarItemLocation::Hidden,
},
}
@@ -1040,6 +1050,7 @@ impl Render for AgentDiffToolbar {
let content = match state {
EditorState::Idle => return Empty.into_any(),
EditorState::Generating => vec![spinner_icon],
EditorState::Reviewing => vec![
h_flex()
.child(
@@ -1211,6 +1222,7 @@ pub struct AgentDiff {
pub enum EditorState {
Idle,
Reviewing,
Generating,
}
struct WorkspaceThread {
@@ -1533,11 +1545,15 @@ impl AgentDiff {
multibuffer.add_diff(diff_handle.clone(), cx);
});
let reviewing_state = EditorState::Reviewing;
let new_state = if thread.is_generating(cx) {
EditorState::Generating
} else {
EditorState::Reviewing
};
let previous_state = self
.reviewing_editors
.insert(weak_editor.clone(), reviewing_state.clone());
.insert(weak_editor.clone(), new_state.clone());
if previous_state.is_none() {
editor.update(cx, |editor, cx| {
@@ -1550,9 +1566,7 @@ impl AgentDiff {
unaffected.remove(weak_editor);
}
if reviewing_state == EditorState::Reviewing
&& previous_state != Some(reviewing_state)
{
if new_state == EditorState::Reviewing && previous_state != Some(new_state) {
// Jump to first hunk when we enter review mode
editor.update(cx, |editor, cx| {
let snapshot = multibuffer.read(cx).snapshot(cx);

View File

@@ -19,6 +19,8 @@ use settings::{
use zed_actions::OpenBrowser;
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::context_store::ContextStore;
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
@@ -31,14 +33,9 @@ use crate::{
text_thread_editor::{AgentPanelDelegate, TextThreadEditor, make_lsp_adapter_delegate},
ui::{AgentOnboardingModal, EndTrialUpsell},
};
use crate::{
ExpandMessageEditor,
acp::{AcpThreadHistory, ThreadHistoryEvent},
};
use crate::{
ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
};
use crate::{ManageProfiles, context_store::ContextStore};
use agent_settings::AgentSettings;
use ai_onboarding::AgentPanelOnboarding;
use anyhow::{Result, anyhow};
@@ -109,12 +106,6 @@ pub fn init(cx: &mut App) {
}
},
)
.register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.expand_message_editor(window, cx));
}
})
.register_action(|workspace, _: &OpenHistory, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -738,25 +729,6 @@ impl AgentPanel {
&self.context_server_registry
}
pub fn is_hidden(workspace: &Entity<Workspace>, cx: &App) -> bool {
let workspace_read = workspace.read(cx);
workspace_read
.panel::<AgentPanel>(cx)
.map(|panel| {
let panel_id = Entity::entity_id(&panel);
let is_visible = workspace_read.all_docks().iter().any(|dock| {
dock.read(cx)
.visible_panel()
.is_some_and(|visible_panel| visible_panel.panel_id() == panel_id)
});
!is_visible
})
.unwrap_or(true)
}
fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
match &self.active_view {
ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view),
@@ -953,15 +925,6 @@ impl AgentPanel {
.detach_and_log_err(cx);
}
fn expand_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(thread_view) = self.active_thread_view() {
thread_view.update(cx, |view, cx| {
view.expand_message_editor(&ExpandMessageEditor, window, cx);
view.focus_handle(cx).focus(window);
});
}
}
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if matches!(self.active_view, ActiveView::History) {
if let Some(previous_view) = self.previous_view.take() {
@@ -1780,9 +1743,10 @@ impl AgentPanel {
}),
)
.action("Add Custom Server…", Box::new(AddContextServer))
.separator()
.separator();
menu = menu
.action("Rules", Box::new(OpenRulesLibrary::default()))
.action("Profiles", Box::new(ManageProfiles::default()))
.action("Settings", Box::new(OpenSettings))
.separator()
.action(full_screen_label, Box::new(ToggleZoom));
@@ -1880,12 +1844,7 @@ impl AgentPanel {
{
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in(
"New Thread…",
&ToggleNewThreadMenu,
&focus_handle,
cx,
)
Tooltip::for_action_in("New…", &ToggleNewThreadMenu, &focus_handle, cx)
}
},
)
@@ -1983,7 +1942,7 @@ impl AgentPanel {
.separator()
.header("External Agents")
.item(
ContextMenuEntry::new("New Claude Code")
ContextMenuEntry::new("New Claude Code Thread")
.icon(IconName::AiClaude)
.disabled(is_via_collab)
.icon_color(Color::Muted)
@@ -2009,7 +1968,7 @@ impl AgentPanel {
}),
)
.item(
ContextMenuEntry::new("New Codex CLI")
ContextMenuEntry::new("New Codex Thread")
.icon(IconName::AiOpenAi)
.disabled(is_via_collab)
.icon_color(Color::Muted)
@@ -2035,7 +1994,7 @@ impl AgentPanel {
}),
)
.item(
ContextMenuEntry::new("New Gemini CLI")
ContextMenuEntry::new("New Gemini CLI Thread")
.icon(IconName::AiGemini)
.icon_color(Color::Muted)
.disabled(is_via_collab)
@@ -2079,9 +2038,9 @@ impl AgentPanel {
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));
ContextMenuEntry::new(format!("New {} Thread", agent_name));
if let Some(icon_path) = icon_path {
entry = entry.custom_icon_svg(icon_path);
entry = entry.custom_icon_path(icon_path);
} else {
entry = entry.icon(IconName::Terminal);
}
@@ -2150,7 +2109,7 @@ impl AgentPanel {
.when_some(selected_agent_custom_icon, |this, icon_path| {
let label = selected_agent_label.clone();
this.px(DynamicSpacing::Base02.rems(cx))
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
.child(Icon::from_path(icon_path).color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
})

View File

@@ -487,9 +487,10 @@ impl CodegenAlternative {
) {
let start_time = Instant::now();
let snapshot = self.snapshot.clone();
let selected_text = snapshot
.text_for_range(self.range.start..self.range.end)
.collect::<Rope>();
let selected_text = Rope::from_iter(
snapshot.text_for_range(self.range.start..self.range.end),
cx.background_executor(),
);
let selection_start = self.range.start.to_point(&snapshot);

View File

@@ -620,18 +620,8 @@ impl TextThreadContextHandle {
impl Display for TextThreadContext {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "<text_thread title=\"")?;
for c in self.title.chars() {
match c {
'&' => write!(f, "&amp;")?,
'<' => write!(f, "&lt;")?,
'>' => write!(f, "&gt;")?,
'"' => write!(f, "&quot;")?,
'\'' => write!(f, "&apos;")?,
_ => write!(f, "{}", c)?,
}
}
writeln!(f, "\">")?;
// TODO: escape title?
writeln!(f, "<text_thread title=\"{}\">", self.title)?;
write!(f, "{}", self.text.trim())?;
write!(f, "\n</text_thread>")
}

View File

@@ -260,10 +260,10 @@ impl<T: 'static> PromptEditor<T> {
let agent_panel_keybinding =
ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
.map(|keybinding| format!("{keybinding} to chat"))
.map(|keybinding| format!("{keybinding} to chat"))
.unwrap_or_default();
format!("{action}… ({agent_panel_keybinding}↓↑ for history — @ to include context)")
format!("{action}… ({agent_panel_keybinding}↓↑ for history)")
}
pub fn prompt(&self, cx: &App) -> String {

View File

@@ -744,12 +744,13 @@ impl TextThread {
telemetry: Option<Arc<Telemetry>>,
cx: &mut Context<Self>,
) -> Self {
let buffer = cx.new(|_cx| {
let buffer = cx.new(|cx| {
let buffer = Buffer::remote(
language::BufferId::new(1).unwrap(),
replica_id,
capability,
"",
cx.background_executor(),
);
buffer.set_language_registry(language_registry.clone());
buffer

View File

@@ -26,7 +26,6 @@ serde_json.workspace = true
settings.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true
workspace.workspace = true
[target.'cfg(not(target_os = "windows"))'.dependencies]

View File

@@ -331,16 +331,6 @@ impl AutoUpdater {
pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
#[cfg(target_os = "windows")]
{
use util::ResultExt;
cleanup_windows()
.await
.context("failed to cleanup old directories")
.log_err();
}
loop {
this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;
cx.background_executor().timer(POLL_INTERVAL).await;
@@ -933,32 +923,6 @@ async fn install_release_macos(
Ok(None)
}
#[cfg(target_os = "windows")]
async fn cleanup_windows() -> Result<()> {
use util::ResultExt;
let parent = std::env::current_exe()?
.parent()
.context("No parent dir for Zed.exe")?
.to_owned();
// keep in sync with crates/auto_update_helper/src/updater.rs
smol::fs::remove_dir(parent.join("updates"))
.await
.context("failed to remove updates dir")
.log_err();
smol::fs::remove_dir(parent.join("install"))
.await
.context("failed to remove install dir")
.log_err();
smol::fs::remove_dir(parent.join("old"))
.await
.context("failed to remove old version dir")
.log_err();
Ok(())
}
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
let output = Command::new(downloaded_installer)
.arg("/verysilent")
@@ -998,7 +962,7 @@ pub async fn finalize_auto_update_on_quit() {
.parent()
.map(|p| p.join("tools").join("auto_update_helper.exe"))
{
let mut command = util::command::new_smol_command(helper);
let mut command = smol::process::Command::new(helper);
command.arg("--launch");
command.arg("false");
if let Ok(mut cmd) = command.spawn() {

View File

@@ -21,9 +21,6 @@ simplelog.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[target.'cfg(target_os = "windows")'.dev-dependencies]
tempfile.workspace = true
[target.'cfg(target_os = "windows")'.build-dependencies]
winresource = "0.1"

View File

@@ -1,5 +1,4 @@
use std::{
cell::LazyCell,
path::Path,
time::{Duration, Instant},
};
@@ -12,274 +11,210 @@ use windows::Win32::{
use crate::windows_impl::WM_JOB_UPDATED;
pub(crate) struct Job {
pub apply: Box<dyn Fn(&Path) -> Result<()>>,
pub rollback: Box<dyn Fn(&Path) -> Result<()>>,
}
type Job = fn(&Path) -> Result<()>;
impl Job {
pub fn mkdir(name: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let dir = app_dir.join(name);
std::fs::create_dir_all(&dir)
.context(format!("Failed to create directory {}", dir.display()))
}),
rollback: Box::new(move |app_dir| {
let dir = app_dir.join(name);
std::fs::remove_dir_all(&dir)
.context(format!("Failed to remove directory {}", dir.display()))
}),
}
}
pub fn mkdir_if_exists(name: &'static Path, check: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let dir = app_dir.join(name);
let check = app_dir.join(check);
if check.exists() {
std::fs::create_dir_all(&dir)
.context(format!("Failed to create directory {}", dir.display()))?
}
Ok(())
}),
rollback: Box::new(move |app_dir| {
let dir = app_dir.join(name);
if dir.exists() {
std::fs::remove_dir_all(&dir)
.context(format!("Failed to remove directory {}", dir.display()))?
}
Ok(())
}),
}
}
pub fn move_file(filename: &'static Path, new_filename: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
log::info!(
"Moving file: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&old_file, new_file)
.context(format!("Failed to move file {}", old_file.display()))
}),
rollback: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
log::info!(
"Rolling back file move: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&new_file, &old_file).context(format!(
"Failed to rollback file move {}->{}",
new_file.display(),
old_file.display()
))
}),
}
}
pub fn move_if_exists(filename: &'static Path, new_filename: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
if old_file.exists() {
log::info!(
"Moving file: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&old_file, new_file)
.context(format!("Failed to move file {}", old_file.display()))?;
}
Ok(())
}),
rollback: Box::new(move |app_dir| {
let old_file = app_dir.join(filename);
let new_file = app_dir.join(new_filename);
if new_file.exists() {
log::info!(
"Rolling back file move: {}->{}",
old_file.display(),
new_file.display()
);
std::fs::rename(&new_file, &old_file).context(format!(
"Failed to rollback file move {}->{}",
new_file.display(),
old_file.display()
))?
}
Ok(())
}),
}
}
pub fn rmdir_nofail(filename: &'static Path) -> Self {
Job {
apply: Box::new(move |app_dir| {
let filename = app_dir.join(filename);
log::info!("Removing file: {}", filename.display());
if let Err(e) = std::fs::remove_dir_all(&filename) {
log::warn!("Failed to remove directory: {}", e);
}
Ok(())
}),
rollback: Box::new(move |app_dir| {
let filename = app_dir.join(filename);
anyhow::bail!(
"Delete operations cannot be rolled back, file: {}",
filename.display()
)
}),
}
}
}
// app is single threaded
#[cfg(not(test))]
#[allow(clippy::declare_interior_mutable_const)]
pub(crate) const JOBS: LazyCell<[Job; 22]> = LazyCell::new(|| {
fn p(value: &str) -> &Path {
Path::new(value)
}
[
// Move old files
// Not deleting because installing new files can fail
Job::mkdir(p("old")),
Job::move_file(p("Zed.exe"), p("old\\Zed.exe")),
Job::mkdir(p("old\\bin")),
Job::move_file(p("bin\\Zed.exe"), p("old\\bin\\Zed.exe")),
Job::move_file(p("bin\\zed"), p("old\\bin\\zed")),
//
// TODO: remove after a few weeks once everyone is on the new version and this file never exists
Job::move_if_exists(p("OpenConsole.exe"), p("old\\OpenConsole.exe")),
Job::mkdir(p("old\\x64")),
Job::mkdir(p("old\\arm64")),
Job::move_if_exists(p("x64\\OpenConsole.exe"), p("old\\x64\\OpenConsole.exe")),
Job::move_if_exists(
p("arm64\\OpenConsole.exe"),
p("old\\arm64\\OpenConsole.exe"),
),
//
Job::move_file(p("conpty.dll"), p("old\\conpty.dll")),
// Copy new files
Job::move_file(p("install\\Zed.exe"), p("Zed.exe")),
Job::move_file(p("install\\bin\\Zed.exe"), p("bin\\Zed.exe")),
Job::move_file(p("install\\bin\\zed"), p("bin\\zed")),
//
Job::mkdir_if_exists(p("x64"), p("install\\x64")),
Job::mkdir_if_exists(p("arm64"), p("install\\arm64")),
Job::move_if_exists(
p("install\\x64\\OpenConsole.exe"),
p("x64\\OpenConsole.exe"),
),
Job::move_if_exists(
p("install\\arm64\\OpenConsole.exe"),
p("arm64\\OpenConsole.exe"),
),
//
Job::move_file(p("install\\conpty.dll"), p("conpty.dll")),
// Cleanup installer and updates folder
Job::rmdir_nofail(p("updates")),
Job::rmdir_nofail(p("install")),
// Cleanup old installation
Job::rmdir_nofail(p("old")),
]
});
pub(crate) const JOBS: &[Job] = &[
// Delete old files
|app_dir| {
let zed_executable = app_dir.join("Zed.exe");
log::info!("Removing old file: {}", zed_executable.display());
std::fs::remove_file(&zed_executable).context(format!(
"Failed to remove old file {}",
zed_executable.display()
))
},
|app_dir| {
let zed_cli = app_dir.join("bin\\zed.exe");
log::info!("Removing old file: {}", zed_cli.display());
std::fs::remove_file(&zed_cli)
.context(format!("Failed to remove old file {}", zed_cli.display()))
},
|app_dir| {
let zed_wsl = app_dir.join("bin\\zed");
log::info!("Removing old file: {}", zed_wsl.display());
std::fs::remove_file(&zed_wsl)
.context(format!("Failed to remove old file {}", zed_wsl.display()))
},
// TODO: remove after a few weeks once everyone is on the new version and this file never exists
|app_dir| {
let open_console = app_dir.join("OpenConsole.exe");
if open_console.exists() {
log::info!("Removing old file: {}", open_console.display());
std::fs::remove_file(&open_console).context(format!(
"Failed to remove old file {}",
open_console.display()
))?
}
Ok(())
},
|app_dir| {
let archs = ["x64", "arm64"];
for arch in archs {
let open_console = app_dir.join(format!("{arch}\\OpenConsole.exe"));
if open_console.exists() {
log::info!("Removing old file: {}", open_console.display());
std::fs::remove_file(&open_console).context(format!(
"Failed to remove old file {}",
open_console.display()
))?
}
}
Ok(())
},
|app_dir| {
let conpty = app_dir.join("conpty.dll");
log::info!("Removing old file: {}", conpty.display());
std::fs::remove_file(&conpty)
.context(format!("Failed to remove old file {}", conpty.display()))
},
// Copy new files
|app_dir| {
let zed_executable_source = app_dir.join("install\\Zed.exe");
let zed_executable_dest = app_dir.join("Zed.exe");
log::info!(
"Copying new file {} to {}",
zed_executable_source.display(),
zed_executable_dest.display()
);
std::fs::copy(&zed_executable_source, &zed_executable_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
zed_executable_source.display(),
zed_executable_dest.display()
))
},
|app_dir| {
let zed_cli_source = app_dir.join("install\\bin\\zed.exe");
let zed_cli_dest = app_dir.join("bin\\zed.exe");
log::info!(
"Copying new file {} to {}",
zed_cli_source.display(),
zed_cli_dest.display()
);
std::fs::copy(&zed_cli_source, &zed_cli_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
zed_cli_source.display(),
zed_cli_dest.display()
))
},
|app_dir| {
let zed_wsl_source = app_dir.join("install\\bin\\zed");
let zed_wsl_dest = app_dir.join("bin\\zed");
log::info!(
"Copying new file {} to {}",
zed_wsl_source.display(),
zed_wsl_dest.display()
);
std::fs::copy(&zed_wsl_source, &zed_wsl_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
zed_wsl_source.display(),
zed_wsl_dest.display()
))
},
|app_dir| {
let archs = ["x64", "arm64"];
for arch in archs {
let open_console_source = app_dir.join(format!("install\\{arch}\\OpenConsole.exe"));
let open_console_dest = app_dir.join(format!("{arch}\\OpenConsole.exe"));
if open_console_source.exists() {
log::info!(
"Copying new file {} to {}",
open_console_source.display(),
open_console_dest.display()
);
let parent = open_console_dest.parent().context(format!(
"Failed to get parent directory of {}",
open_console_dest.display()
))?;
std::fs::create_dir_all(parent)
.context(format!("Failed to create directory {}", parent.display()))?;
std::fs::copy(&open_console_source, &open_console_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
open_console_source.display(),
open_console_dest.display()
))?
}
}
Ok(())
},
|app_dir| {
let conpty_source = app_dir.join("install\\conpty.dll");
let conpty_dest = app_dir.join("conpty.dll");
log::info!(
"Copying new file {} to {}",
conpty_source.display(),
conpty_dest.display()
);
std::fs::copy(&conpty_source, &conpty_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
conpty_source.display(),
conpty_dest.display()
))
},
// Clean up installer folder and updates folder
|app_dir| {
let updates_folder = app_dir.join("updates");
log::info!("Cleaning up: {}", updates_folder.display());
std::fs::remove_dir_all(&updates_folder).context(format!(
"Failed to remove updates folder {}",
updates_folder.display()
))
},
|app_dir| {
let installer_folder = app_dir.join("install");
log::info!("Cleaning up: {}", installer_folder.display());
std::fs::remove_dir_all(&installer_folder).context(format!(
"Failed to remove installer folder {}",
installer_folder.display()
))
},
];
// app is single threaded
#[cfg(test)]
#[allow(clippy::declare_interior_mutable_const)]
pub(crate) const JOBS: LazyCell<[Job; 9]> = LazyCell::new(|| {
fn p(value: &str) -> &Path {
Path::new(value)
}
[
Job {
apply: Box::new(|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
"err1" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
"err2" => Ok(()),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
Ok(())
}
}),
rollback: Box::new(|_| {
unsafe { std::env::set_var("ZED_AUTO_UPDATE_RB", "rollback1") };
Ok(())
}),
},
Job::mkdir(p("test1")),
Job::mkdir_if_exists(p("test_exists"), p("test1")),
Job::mkdir_if_exists(p("test_missing"), p("dont")),
Job {
apply: Box::new(|folder| {
std::fs::write(folder.join("test1/test"), "test")?;
Ok(())
}),
rollback: Box::new(|folder| {
std::fs::remove_file(folder.join("test1/test"))?;
Ok(())
}),
},
Job::move_file(p("test1/test"), p("test1/moved")),
Job::move_if_exists(p("test1/test"), p("test1/noop")),
Job {
apply: Box::new(|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
"err1" => Ok(()),
"err2" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
Ok(())
}
}),
rollback: Box::new(|_| Ok(())),
},
Job::rmdir_nofail(p("test1/nofolder")),
]
});
pub(crate) const JOBS: &[Job] = &[
|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
"err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
Ok(())
}
},
|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
"err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
Ok(())
}
},
];
pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool) -> Result<()> {
let hwnd = hwnd.map(|ptr| HWND(ptr as _));
let mut last_successful_job = None;
'outer: for (i, job) in JOBS.iter().enumerate() {
for job in JOBS.iter() {
let start = Instant::now();
loop {
if start.elapsed().as_secs() > 2 {
log::error!("Timed out, rolling back");
break 'outer;
}
match (job.apply)(app_dir) {
anyhow::ensure!(start.elapsed().as_secs() <= 2, "Timed out");
match (*job)(app_dir) {
Ok(_) => {
last_successful_job = Some(i);
unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
break;
}
@@ -288,7 +223,6 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool)
let io_err = err.downcast_ref::<std::io::Error>().unwrap();
if io_err.kind() == std::io::ErrorKind::NotFound {
log::warn!("File or folder not found.");
last_successful_job = Some(i);
unsafe { PostMessageW(hwnd, WM_JOB_UPDATED, WPARAM(0), LPARAM(0))? };
break;
}
@@ -299,28 +233,6 @@ pub(crate) fn perform_update(app_dir: &Path, hwnd: Option<isize>, launch: bool)
}
}
}
if last_successful_job
.map(|job| job != JOBS.len() - 1)
.unwrap_or(true)
{
let Some(last_successful_job) = last_successful_job else {
anyhow::bail!("Autoupdate failed, nothing to rollback");
};
for job in (0..=last_successful_job).rev() {
let job = &JOBS[job];
if let Err(e) = (job.rollback)(app_dir) {
anyhow::bail!(
"Job rollback failed, the app might be left in an inconsistent state: ({:?})",
e
);
}
}
anyhow::bail!("Autoupdate failed, rollback successful");
}
if launch {
#[allow(clippy::disallowed_methods, reason = "doesn't run in the main binary")]
let _ = std::process::Command::new(app_dir.join("Zed.exe")).spawn();
@@ -335,27 +247,12 @@ mod test {
#[test]
fn test_perform_update() {
let app_dir = tempfile::tempdir().unwrap();
let app_dir = app_dir.path();
let app_dir = std::path::Path::new("C:/");
assert!(perform_update(app_dir, None, false).is_ok());
let app_dir = tempfile::tempdir().unwrap();
let app_dir = app_dir.path();
// Simulate a timeout
unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err1") };
unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err") };
let ret = perform_update(app_dir, None, false);
assert!(
ret.is_err_and(|e| e.to_string().as_str() == "Autoupdate failed, nothing to rollback")
);
let app_dir = tempfile::tempdir().unwrap();
let app_dir = app_dir.path();
// Simulate a timeout
unsafe { std::env::set_var("ZED_AUTO_UPDATE", "err2") };
let ret = perform_update(app_dir, None, false);
assert!(
ret.is_err_and(|e| e.to_string().as_str() == "Autoupdate failed, rollback successful")
);
assert!(std::env::var("ZED_AUTO_UPDATE_RB").is_ok_and(|e| e == "rollback1"));
assert!(ret.is_err_and(|e| e.to_string().as_str() == "Timed out"));
}
}

View File

@@ -100,21 +100,13 @@ impl Render for Breadcrumbs {
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
let prefix_element = active_item.breadcrumb_prefix(window, cx);
let breadcrumbs = if let Some(prefix) = prefix_element {
h_flex().gap_1p5().child(prefix).child(breadcrumbs_stack)
} else {
breadcrumbs_stack
};
match active_item
.downcast::<Editor>()
.map(|editor| editor.downgrade())
{
Some(editor) => element.child(
ButtonLike::new("toggle outline view")
.child(breadcrumbs)
.child(breadcrumbs_stack)
.style(ButtonStyle::Transparent)
.on_click({
let editor = editor.clone();
@@ -149,7 +141,7 @@ impl Render for Breadcrumbs {
// Match the height and padding of the `ButtonLike` in the other arm.
.h(rems_from_px(22.))
.pl_1()
.child(breadcrumbs),
.child(breadcrumbs_stack),
}
}
}

View File

@@ -1,6 +1,9 @@
use futures::channel::oneshot;
use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, TaskLabel};
use gpui::{
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Task,
TaskLabel,
};
use language::{Language, LanguageRegistry};
use rope::Rope;
use std::{
@@ -191,7 +194,7 @@ impl BufferDiffSnapshot {
let base_text_exists;
let base_text_snapshot;
if let Some(text) = &base_text {
let base_text_rope = Rope::from(text.as_str());
let base_text_rope = Rope::from_str(text.as_str(), cx.background_executor());
base_text_pair = Some((text.clone(), base_text_rope.clone()));
let snapshot =
language::Buffer::build_snapshot(base_text_rope, language, language_registry, cx);
@@ -311,6 +314,7 @@ impl BufferDiffInner {
hunks: &[DiffHunk],
buffer: &text::BufferSnapshot,
file_exists: bool,
cx: &BackgroundExecutor,
) -> Option<Rope> {
let head_text = self
.base_text_exists
@@ -505,7 +509,7 @@ impl BufferDiffInner {
for (old_range, replacement_text) in edits {
new_index_text.append(index_cursor.slice(old_range.start));
index_cursor.seek_forward(old_range.end);
new_index_text.push(&replacement_text);
new_index_text.push(&replacement_text, cx);
}
new_index_text.append(index_cursor.suffix());
Some(new_index_text)
@@ -962,6 +966,7 @@ impl BufferDiff {
hunks,
buffer,
file_exists,
cx.background_executor(),
);
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
@@ -1385,7 +1390,12 @@ mod tests {
"
.unindent();
let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
let mut buffer = Buffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
buffer_text,
cx.background_executor(),
);
let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
assert_hunks(
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
@@ -1394,7 +1404,7 @@ mod tests {
&[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
);
buffer.edit([(0..0, "point five\n")]);
buffer.edit([(0..0, "point five\n")], cx.background_executor());
diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
assert_hunks(
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
@@ -1459,7 +1469,12 @@ mod tests {
"
.unindent();
let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
let buffer = Buffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
buffer_text,
cx.background_executor(),
);
let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
let mut uncommitted_diff =
BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
@@ -1528,7 +1543,12 @@ mod tests {
"
.unindent();
let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
let buffer = Buffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
buffer_text,
cx.background_executor(),
);
let diff = cx
.update(|cx| {
BufferDiffSnapshot::new_with_base_text(
@@ -1791,7 +1811,12 @@ mod tests {
for example in table {
let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false);
let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
let buffer = Buffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
buffer_text,
cx.background_executor(),
);
let hunk_range =
buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
@@ -1868,6 +1893,7 @@ mod tests {
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
buffer_text.clone(),
cx.background_executor(),
);
let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
@@ -1941,7 +1967,12 @@ mod tests {
"
.unindent();
let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1);
let mut buffer = Buffer::new(
ReplicaId::LOCAL,
BufferId::new(1).unwrap(),
buffer_text_1,
cx.background_executor(),
);
let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
@@ -1961,6 +1992,7 @@ mod tests {
NINE
"
.unindent(),
cx.background_executor(),
);
let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer));
@@ -1978,6 +2010,7 @@ mod tests {
NINE
"
.unindent(),
cx.background_executor(),
);
let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
let range = diff_3.inner.compare(&diff_2.inner, &buffer).unwrap();
@@ -1995,6 +2028,7 @@ mod tests {
NINE
"
.unindent(),
cx.background_executor(),
);
let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
let range = diff_4.inner.compare(&diff_3.inner, &buffer).unwrap();
@@ -2013,6 +2047,7 @@ mod tests {
NINE
"
.unindent(),
cx.background_executor(),
);
let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
let range = diff_5.inner.compare(&diff_4.inner, &buffer).unwrap();
@@ -2031,6 +2066,7 @@ mod tests {
«nine»
"
.unindent(),
cx.background_executor(),
);
let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
let range = diff_6.inner.compare(&diff_5.inner, &buffer).unwrap();
@@ -2140,14 +2176,14 @@ mod tests {
let working_copy = gen_working_copy(rng, &head_text);
let working_copy = cx.new(|cx| {
language::Buffer::local_normalized(
Rope::from(working_copy.as_str()),
Rope::from_str(working_copy.as_str(), cx.background_executor()),
text::LineEnding::default(),
cx,
)
});
let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
let mut index_text = if rng.random() {
Rope::from(head_text.as_str())
Rope::from_str(head_text.as_str(), cx.background_executor())
} else {
working_copy.as_rope().clone()
};

View File

@@ -70,6 +70,7 @@ impl ChannelBuffer {
ReplicaId::new(response.replica_id as u16),
capability,
base_text,
cx.background_executor(),
)
})?;
buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?;

View File

@@ -1,5 +1,4 @@
pub mod predict_edits_v3;
pub mod udiff;
use std::str::FromStr;
use std::sync::Arc;

View File

@@ -1,294 +0,0 @@
use std::{borrow::Cow, fmt::Display};
#[derive(Debug, PartialEq)]
pub enum DiffLine<'a> {
OldPath { path: Cow<'a, str> },
NewPath { path: Cow<'a, str> },
HunkHeader(Option<HunkLocation>),
Context(&'a str),
Deletion(&'a str),
Addition(&'a str),
Garbage(&'a str),
}
#[derive(Debug, PartialEq)]
pub struct HunkLocation {
start_line_old: u32,
count_old: u32,
start_line_new: u32,
count_new: u32,
}
impl<'a> DiffLine<'a> {
pub fn parse(line: &'a str) -> Self {
Self::try_parse(line).unwrap_or(Self::Garbage(line))
}
fn try_parse(line: &'a str) -> Option<Self> {
if let Some(header) = line.strip_prefix("---").and_then(eat_required_whitespace) {
let path = parse_header_path("a/", header);
Some(Self::OldPath { path })
} else if let Some(header) = line.strip_prefix("+++").and_then(eat_required_whitespace) {
Some(Self::NewPath {
path: parse_header_path("b/", header),
})
} else if let Some(header) = line.strip_prefix("@@").and_then(eat_required_whitespace) {
if header.starts_with("...") {
return Some(Self::HunkHeader(None));
}
let (start_line_old, header) = header.strip_prefix('-')?.split_once(',')?;
let mut parts = header.split_ascii_whitespace();
let count_old = parts.next()?;
let (start_line_new, count_new) = parts.next()?.strip_prefix('+')?.split_once(',')?;
Some(Self::HunkHeader(Some(HunkLocation {
start_line_old: start_line_old.parse::<u32>().ok()?.saturating_sub(1),
count_old: count_old.parse().ok()?,
start_line_new: start_line_new.parse::<u32>().ok()?.saturating_sub(1),
count_new: count_new.parse().ok()?,
})))
} else if let Some(deleted_header) = line.strip_prefix("-") {
Some(Self::Deletion(deleted_header))
} else if line.is_empty() {
Some(Self::Context(""))
} else if let Some(context) = line.strip_prefix(" ") {
Some(Self::Context(context))
} else {
Some(Self::Addition(line.strip_prefix("+")?))
}
}
}
impl<'a> Display for DiffLine<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DiffLine::OldPath { path } => write!(f, "--- {path}"),
DiffLine::NewPath { path } => write!(f, "+++ {path}"),
DiffLine::HunkHeader(Some(hunk_location)) => {
write!(
f,
"@@ -{},{} +{},{} @@",
hunk_location.start_line_old + 1,
hunk_location.count_old,
hunk_location.start_line_new + 1,
hunk_location.count_new
)
}
DiffLine::HunkHeader(None) => write!(f, "@@ ... @@"),
DiffLine::Context(content) => write!(f, " {content}"),
DiffLine::Deletion(content) => write!(f, "-{content}"),
DiffLine::Addition(content) => write!(f, "+{content}"),
DiffLine::Garbage(line) => write!(f, "{line}"),
}
}
}
fn parse_header_path<'a>(strip_prefix: &'static str, header: &'a str) -> Cow<'a, str> {
if !header.contains(['"', '\\']) {
let path = header.split_ascii_whitespace().next().unwrap_or(header);
return Cow::Borrowed(path.strip_prefix(strip_prefix).unwrap_or(path));
}
let mut path = String::with_capacity(header.len());
let mut in_quote = false;
let mut chars = header.chars().peekable();
let mut strip_prefix = Some(strip_prefix);
while let Some(char) = chars.next() {
if char == '"' {
in_quote = !in_quote;
} else if char == '\\' {
let Some(&next_char) = chars.peek() else {
break;
};
chars.next();
path.push(next_char);
} else if char.is_ascii_whitespace() && !in_quote {
break;
} else {
path.push(char);
}
if let Some(prefix) = strip_prefix
&& path == prefix
{
strip_prefix.take();
path.clear();
}
}
Cow::Owned(path)
}
fn eat_required_whitespace(header: &str) -> Option<&str> {
let trimmed = header.trim_ascii_start();
if trimmed.len() == header.len() {
None
} else {
Some(trimmed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
#[test]
fn parse_lines_simple() {
let input = indoc! {"
diff --git a/text.txt b/text.txt
index 86c770d..a1fd855 100644
--- a/file.txt
+++ b/file.txt
@@ -1,2 +1,3 @@
context
-deleted
+inserted
garbage
--- b/file.txt
+++ a/file.txt
"};
let lines = input.lines().map(DiffLine::parse).collect::<Vec<_>>();
pretty_assertions::assert_eq!(
lines,
&[
DiffLine::Garbage("diff --git a/text.txt b/text.txt"),
DiffLine::Garbage("index 86c770d..a1fd855 100644"),
DiffLine::OldPath {
path: "file.txt".into()
},
DiffLine::NewPath {
path: "file.txt".into()
},
DiffLine::HunkHeader(Some(HunkLocation {
start_line_old: 0,
count_old: 2,
start_line_new: 0,
count_new: 3
})),
DiffLine::Context("context"),
DiffLine::Deletion("deleted"),
DiffLine::Addition("inserted"),
DiffLine::Garbage("garbage"),
DiffLine::Context(""),
DiffLine::OldPath {
path: "b/file.txt".into()
},
DiffLine::NewPath {
path: "a/file.txt".into()
},
]
);
}
#[test]
fn file_header_extra_space() {
let options = ["--- file", "--- file", "---\tfile"];
for option in options {
pretty_assertions::assert_eq!(
DiffLine::parse(option),
DiffLine::OldPath {
path: "file".into()
},
"{option}",
);
}
}
#[test]
fn hunk_header_extra_space() {
let options = [
"@@ -1,2 +1,3 @@",
"@@ -1,2 +1,3 @@",
"@@\t-1,2\t+1,3\t@@",
"@@ -1,2 +1,3 @@",
"@@ -1,2 +1,3 @@",
"@@ -1,2 +1,3 @@",
"@@ -1,2 +1,3 @@ garbage",
];
for option in options {
pretty_assertions::assert_eq!(
DiffLine::parse(option),
DiffLine::HunkHeader(Some(HunkLocation {
start_line_old: 0,
count_old: 2,
start_line_new: 0,
count_new: 3
})),
"{option}",
);
}
}
#[test]
fn hunk_header_without_location() {
pretty_assertions::assert_eq!(DiffLine::parse("@@ ... @@"), DiffLine::HunkHeader(None));
}
#[test]
fn test_parse_path() {
assert_eq!(parse_header_path("a/", "foo.txt"), "foo.txt");
assert_eq!(
parse_header_path("a/", "foo/bar/baz.txt"),
"foo/bar/baz.txt"
);
assert_eq!(parse_header_path("a/", "a/foo.txt"), "foo.txt");
assert_eq!(
parse_header_path("a/", "a/foo/bar/baz.txt"),
"foo/bar/baz.txt"
);
// Extra
assert_eq!(
parse_header_path("a/", "a/foo/bar/baz.txt 2025"),
"foo/bar/baz.txt"
);
assert_eq!(
parse_header_path("a/", "a/foo/bar/baz.txt\t2025"),
"foo/bar/baz.txt"
);
assert_eq!(
parse_header_path("a/", "a/foo/bar/baz.txt \""),
"foo/bar/baz.txt"
);
// Quoted
assert_eq!(
parse_header_path("a/", "a/foo/bar/\"baz quox.txt\""),
"foo/bar/baz quox.txt"
);
assert_eq!(
parse_header_path("a/", "\"a/foo/bar/baz quox.txt\""),
"foo/bar/baz quox.txt"
);
assert_eq!(
parse_header_path("a/", "\"foo/bar/baz quox.txt\""),
"foo/bar/baz quox.txt"
);
assert_eq!(parse_header_path("a/", "\"whatever 🤷\""), "whatever 🤷");
assert_eq!(
parse_header_path("a/", "\"foo/bar/baz quox.txt\" 2025"),
"foo/bar/baz quox.txt"
);
// unescaped quotes are dropped
assert_eq!(parse_header_path("a/", "foo/\"bar\""), "foo/bar");
// Escaped
assert_eq!(
parse_header_path("a/", "\"foo/\\\"bar\\\"/baz.txt\""),
"foo/\"bar\"/baz.txt"
);
assert_eq!(
parse_header_path("a/", "\"C:\\\\Projects\\\\My App\\\\old file.txt\""),
"C:\\Projects\\My App\\old file.txt"
);
}
}

View File

@@ -212,7 +212,7 @@ pub fn write_codeblock<'a>(
include_line_numbers: bool,
output: &'a mut String,
) {
writeln!(output, "`````{}", path.display()).unwrap();
writeln!(output, "`````path={}", path.display()).unwrap();
write_excerpts(
excerpts,
sorted_insertions,

View File

@@ -701,12 +701,12 @@ impl Database {
return Ok(());
}
let mut text_buffer = text::Buffer::new(
let mut text_buffer = text::Buffer::new_slow(
clock::ReplicaId::LOCAL,
text::BufferId::new(1).unwrap(),
base_text,
);
text_buffer.apply_ops(operations.into_iter().filter_map(operation_from_wire));
text_buffer.apply_ops(operations.into_iter().filter_map(operation_from_wire), None);
let base_text = text_buffer.text();
let epoch = buffer.epoch + 1;

View File

@@ -74,11 +74,21 @@ async fn test_channel_buffers(db: &Arc<Database>) {
ReplicaId::new(0),
text::BufferId::new(1).unwrap(),
"".to_string(),
&db.test_options.as_ref().unwrap().executor,
);
let operations = vec![
buffer_a.edit([(0..0, "hello world")]),
buffer_a.edit([(5..5, ", cruel")]),
buffer_a.edit([(0..5, "goodbye")]),
buffer_a.edit(
[(0..0, "hello world")],
&db.test_options.as_ref().unwrap().executor,
),
buffer_a.edit(
[(5..5, ", cruel")],
&db.test_options.as_ref().unwrap().executor,
),
buffer_a.edit(
[(0..5, "goodbye")],
&db.test_options.as_ref().unwrap().executor,
),
buffer_a.undo().unwrap().1,
];
assert_eq!(buffer_a.text(), "hello, cruel world");
@@ -102,15 +112,19 @@ async fn test_channel_buffers(db: &Arc<Database>) {
ReplicaId::new(0),
text::BufferId::new(1).unwrap(),
buffer_response_b.base_text,
&db.test_options.as_ref().unwrap().executor,
);
buffer_b.apply_ops(
buffer_response_b.operations.into_iter().map(|operation| {
let operation = proto::deserialize_operation(operation).unwrap();
if let language::Operation::Buffer(operation) = operation {
operation
} else {
unreachable!()
}
}),
None,
);
buffer_b.apply_ops(buffer_response_b.operations.into_iter().map(|operation| {
let operation = proto::deserialize_operation(operation).unwrap();
if let language::Operation::Buffer(operation) = operation {
operation
} else {
unreachable!()
}
}));
assert_eq!(buffer_b.text(), "hello, cruel world");
@@ -247,6 +261,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
ReplicaId::new(res.replica_id as u16),
text::BufferId::new(1).unwrap(),
"".to_string(),
&db.test_options.as_ref().unwrap().executor,
));
}
@@ -255,9 +270,9 @@ async fn test_channel_buffers_last_operations(db: &Database) {
user_id,
db,
vec![
text_buffers[0].edit([(0..0, "a")]),
text_buffers[0].edit([(0..0, "b")]),
text_buffers[0].edit([(0..0, "c")]),
text_buffers[0].edit([(0..0, "a")], &db.test_options.as_ref().unwrap().executor),
text_buffers[0].edit([(0..0, "b")], &db.test_options.as_ref().unwrap().executor),
text_buffers[0].edit([(0..0, "c")], &db.test_options.as_ref().unwrap().executor),
],
)
.await;
@@ -267,9 +282,9 @@ async fn test_channel_buffers_last_operations(db: &Database) {
user_id,
db,
vec![
text_buffers[1].edit([(0..0, "d")]),
text_buffers[1].edit([(1..1, "e")]),
text_buffers[1].edit([(2..2, "f")]),
text_buffers[1].edit([(0..0, "d")], &db.test_options.as_ref().unwrap().executor),
text_buffers[1].edit([(1..1, "e")], &db.test_options.as_ref().unwrap().executor),
text_buffers[1].edit([(2..2, "f")], &db.test_options.as_ref().unwrap().executor),
],
)
.await;
@@ -286,14 +301,15 @@ async fn test_channel_buffers_last_operations(db: &Database) {
replica_id,
text::BufferId::new(1).unwrap(),
"def".to_string(),
&db.test_options.as_ref().unwrap().executor,
);
update_buffer(
buffers[1].channel_id,
user_id,
db,
vec![
text_buffers[1].edit([(0..0, "g")]),
text_buffers[1].edit([(0..0, "h")]),
text_buffers[1].edit([(0..0, "g")], &db.test_options.as_ref().unwrap().executor),
text_buffers[1].edit([(0..0, "h")], &db.test_options.as_ref().unwrap().executor),
],
)
.await;
@@ -302,7 +318,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
buffers[2].channel_id,
user_id,
db,
vec![text_buffers[2].edit([(0..0, "i")])],
vec![text_buffers[2].edit([(0..0, "i")], &db.test_options.as_ref().unwrap().executor)],
)
.await;

View File

@@ -39,7 +39,6 @@ use std::{
Arc,
atomic::{self, AtomicBool, AtomicUsize},
},
time::Duration,
};
use text::Point;
use util::{path, rel_path::rel_path, uri};
@@ -1818,7 +1817,14 @@ async fn test_mutual_editor_inlay_hint_cache_update(
settings.project.all_languages.defaults.inlay_hints =
Some(InlayHintSettingsContent {
enabled: Some(true),
..InlayHintSettingsContent::default()
show_value_hints: Some(true),
edit_debounce_ms: Some(0),
scroll_debounce_ms: Some(0),
show_type_hints: Some(true),
show_parameter_hints: Some(false),
show_other_hints: Some(true),
show_background: Some(false),
toggle_on_modifiers_press: None,
})
});
});
@@ -1828,8 +1834,15 @@ async fn test_mutual_editor_inlay_hint_cache_update(
store.update_user_settings(cx, |settings| {
settings.project.all_languages.defaults.inlay_hints =
Some(InlayHintSettingsContent {
show_value_hints: Some(true),
enabled: Some(true),
..InlayHintSettingsContent::default()
edit_debounce_ms: Some(0),
scroll_debounce_ms: Some(0),
show_type_hints: Some(true),
show_parameter_hints: Some(false),
show_other_hints: Some(true),
show_background: Some(false),
toggle_on_modifiers_press: None,
})
});
});
@@ -1922,7 +1935,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
});
let fake_language_server = fake_language_servers.next().await.unwrap();
let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
executor.advance_clock(Duration::from_millis(100));
executor.run_until_parked();
let initial_edit = edits_made.load(atomic::Ordering::Acquire);
@@ -1943,7 +1955,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.downcast::<Editor>()
.unwrap();
executor.advance_clock(Duration::from_millis(100));
executor.run_until_parked();
editor_b.update(cx_b, |editor, cx| {
assert_eq!(
@@ -1962,7 +1973,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
});
cx_b.focus(&editor_b);
executor.advance_clock(Duration::from_secs(1));
executor.run_until_parked();
editor_a.update(cx_a, |editor, cx| {
assert_eq!(
@@ -1986,7 +1996,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
});
cx_a.focus(&editor_a);
executor.advance_clock(Duration::from_secs(1));
executor.run_until_parked();
editor_a.update(cx_a, |editor, cx| {
assert_eq!(
@@ -2008,7 +2017,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
.into_response()
.expect("inlay refresh request failed");
executor.advance_clock(Duration::from_secs(1));
executor.run_until_parked();
editor_a.update(cx_a, |editor, cx| {
assert_eq!(

View File

@@ -3694,7 +3694,7 @@ async fn test_buffer_reloading(
assert_eq!(buf.line_ending(), LineEnding::Unix);
});
let new_contents = Rope::from("d\ne\nf");
let new_contents = Rope::from_str_small("d\ne\nf");
client_a
.fs()
.save(
@@ -4479,7 +4479,7 @@ async fn test_reloading_buffer_manually(
.fs()
.save(
path!("/a/a.rs").as_ref(),
&Rope::from("let seven = 7;"),
&Rope::from_str_small("let seven = 7;"),
LineEnding::Unix,
)
.await

View File

@@ -27,6 +27,7 @@ use std::{
rc::Rc,
sync::Arc,
};
use text::Rope;
use util::{
ResultExt, path,
paths::PathStyle,
@@ -938,7 +939,11 @@ impl RandomizedTest for ProjectCollaborationTest {
client
.fs()
.save(&path, &content.as_str().into(), text::LineEnding::Unix)
.save(
&path,
&Rope::from_str_small(content.as_str()),
text::LineEnding::Unix,
)
.await
.unwrap();
}

View File

@@ -7,11 +7,10 @@ use anyhow::Context as _;
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelStore};
use client::{ChannelId, Client, Contact, User, UserStore};
use collections::{HashMap, HashSet};
use contact_finder::ContactFinder;
use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use fuzzy::{StringMatchCandidate, match_strings};
use gpui::{
AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent,
Div, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement,
@@ -31,9 +30,9 @@ use smallvec::SmallVec;
use std::{mem, sync::Arc};
use theme::{ActiveTheme, ThemeSettings};
use ui::{
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, HighlightedLabel,
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip,
prelude::*, tooltip_container,
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, Icon, IconButton,
IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip, prelude::*,
tooltip_container,
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::{
@@ -262,8 +261,6 @@ enum ListEntry {
channel: Arc<Channel>,
depth: usize,
has_children: bool,
// `None` when the channel is a parent of a matched channel.
string_match: Option<StringMatch>,
},
ChannelNotes {
channel_id: ChannelId,
@@ -633,10 +630,6 @@ impl CollabPanel {
.enumerate()
.map(|(ix, (_, channel))| StringMatchCandidate::new(ix, &channel.name)),
);
let mut channels = channel_store
.ordered_channels()
.map(|(_, chan)| chan)
.collect::<Vec<_>>();
let matches = executor.block(match_strings(
&self.match_candidates,
&query,
@@ -646,34 +639,14 @@ impl CollabPanel {
&Default::default(),
executor.clone(),
));
let matches_by_id: HashMap<_, _> = matches
.iter()
.map(|mat| (channels[mat.candidate_id].id, mat.clone()))
.collect();
let channel_ids_of_matches_or_parents: HashSet<_> = matches
.iter()
.flat_map(|mat| {
let match_channel = channels[mat.candidate_id];
match_channel
.parent_path
.iter()
.copied()
.chain(Some(match_channel.id))
})
.collect();
channels.retain(|chan| channel_ids_of_matches_or_parents.contains(&chan.id));
if let Some(state) = &self.channel_editing_state
&& matches!(state, ChannelEditingState::Create { location: None, .. })
{
self.entries.push(ListEntry::ChannelEditor { depth: 0 });
}
let mut collapse_depth = None;
for (idx, channel) in channels.into_iter().enumerate() {
for mat in matches {
let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
let depth = channel.parent_path.len();
if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
@@ -690,7 +663,7 @@ impl CollabPanel {
}
let has_children = channel_store
.channel_at_index(idx + 1)
.channel_at_index(mat.candidate_id + 1)
.is_some_and(|next_channel| next_channel.parent_path.ends_with(&[channel.id]));
match &self.channel_editing_state {
@@ -702,7 +675,6 @@ impl CollabPanel {
channel: channel.clone(),
depth,
has_children: false,
string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
});
self.entries
.push(ListEntry::ChannelEditor { depth: depth + 1 });
@@ -718,7 +690,6 @@ impl CollabPanel {
channel: channel.clone(),
depth,
has_children,
string_match: matches_by_id.get(&channel.id).map(|mat| (*mat).clone()),
});
}
}
@@ -2350,17 +2321,8 @@ impl CollabPanel {
channel,
depth,
has_children,
string_match,
} => self
.render_channel(
channel,
*depth,
*has_children,
is_selected,
ix,
string_match.as_ref(),
cx,
)
.render_channel(channel, *depth, *has_children, is_selected, ix, cx)
.into_any_element(),
ListEntry::ChannelEditor { depth } => self
.render_channel_editor(*depth, window, cx)
@@ -2757,7 +2719,6 @@ impl CollabPanel {
has_children: bool,
is_selected: bool,
ix: usize,
string_match: Option<&StringMatch>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let channel_id = channel.id;
@@ -2894,14 +2855,7 @@ impl CollabPanel {
.child(
h_flex()
.id(channel_id.0 as usize)
.child(match string_match {
None => Label::new(channel.name.clone()).into_any_element(),
Some(string_match) => HighlightedLabel::new(
channel.name.clone(),
string_match.positions.clone(),
)
.into_any_element(),
})
.child(Label::new(channel.name.clone()))
.children(face_pile.map(|face_pile| face_pile.p_1())),
),
)

View File

@@ -489,11 +489,7 @@ impl Copilot {
let node_path = node_runtime.binary_path().await?;
ensure_node_version_for_copilot(&node_path).await?;
let arguments: Vec<OsString> = vec![
"--experimental-sqlite".into(),
server_path.into(),
"--stdio".into(),
];
let arguments: Vec<OsString> = vec![server_path.into(), "--stdio".into()];
let binary = LanguageServerBinary {
path: node_path,
arguments,

View File

@@ -265,10 +265,9 @@ impl minidumper::ServerHandler for CrashServer {
3 => {
let gpu_specs: system_specs::GpuSpecs =
bincode::deserialize(&buffer).expect("gpu specs");
// we ignore the case where it was already set because this message is sent
// on each new window. in theory all zed windows should be using the same
// GPU so this is fine.
self.active_gpu.set(gpu_specs).ok();
self.active_gpu
.set(gpu_specs)
.expect("already set active gpu");
}
_ => {
panic!("invalid message kind");

View File

@@ -41,10 +41,6 @@ util.workspace = true
[dev-dependencies]
dap = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
http_client.workspace = true
node_runtime.workspace = true
settings = { workspace = true, features = ["test-support"] }
task = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }

View File

@@ -4,8 +4,6 @@ mod go;
mod javascript;
mod python;
#[cfg(test)]
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
@@ -40,65 +38,3 @@ pub fn init(cx: &mut App) {
}
})
}
#[cfg(test)]
mod test_mocks {
use super::*;
pub(crate) struct MockDelegate {
worktree_root: PathBuf,
}
impl MockDelegate {
pub(crate) fn new() -> Arc<dyn adapters::DapDelegate> {
Arc::new(Self {
worktree_root: PathBuf::from("/tmp/test"),
})
}
}
#[async_trait::async_trait]
impl adapters::DapDelegate for MockDelegate {
fn worktree_id(&self) -> settings::WorktreeId {
settings::WorktreeId::from_usize(0)
}
fn worktree_root_path(&self) -> &std::path::Path {
&self.worktree_root
}
fn http_client(&self) -> Arc<dyn http_client::HttpClient> {
unimplemented!("Not needed for tests")
}
fn node_runtime(&self) -> node_runtime::NodeRuntime {
unimplemented!("Not needed for tests")
}
fn toolchain_store(&self) -> Arc<dyn language::LanguageToolchainStore> {
unimplemented!("Not needed for tests")
}
fn fs(&self) -> Arc<dyn fs::Fs> {
unimplemented!("Not needed for tests")
}
fn output_to_console(&self, _msg: String) {}
async fn which(&self, _command: &std::ffi::OsStr) -> Option<PathBuf> {
None
}
async fn read_text_file(&self, _path: &util::rel_path::RelPath) -> Result<String> {
Ok(String::new())
}
async fn shell_env(&self) -> collections::HashMap<String, String> {
collections::HashMap::default()
}
fn is_headless(&self) -> bool {
false
}
}
}

View File

@@ -23,11 +23,6 @@ use std::{
use util::command::new_smol_command;
use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
enum DebugpyLaunchMode<'a> {
Normal,
AttachWithConnect { host: Option<&'a str> },
}
#[derive(Default)]
pub(crate) struct PythonDebugAdapter {
base_venv_path: OnceCell<Result<Arc<Path>, String>>,
@@ -41,11 +36,10 @@ impl PythonDebugAdapter {
const LANGUAGE_NAME: &'static str = "Python";
async fn generate_debugpy_arguments<'a>(
host: &'a Ipv4Addr,
async fn generate_debugpy_arguments(
host: &Ipv4Addr,
port: u16,
launch_mode: DebugpyLaunchMode<'a>,
user_installed_path: Option<&'a Path>,
user_installed_path: Option<&Path>,
user_args: Option<Vec<String>>,
) -> Result<Vec<String>> {
let mut args = if let Some(user_installed_path) = user_installed_path {
@@ -68,20 +62,7 @@ impl PythonDebugAdapter {
args.extend(if let Some(args) = user_args {
args
} else {
match launch_mode {
DebugpyLaunchMode::Normal => {
vec![format!("--host={}", host), format!("--port={}", port)]
}
DebugpyLaunchMode::AttachWithConnect { host } => {
let mut args = vec!["connect".to_string()];
if let Some(host) = host {
args.push(format!("{host}:"));
}
args.push(format!("{port}"));
args
}
}
vec![format!("--host={}", host), format!("--port={}", port)]
});
Ok(args)
}
@@ -334,46 +315,7 @@ impl PythonDebugAdapter {
user_env: Option<HashMap<String, String>>,
python_from_toolchain: Option<String>,
) -> Result<DebugAdapterBinary> {
let mut tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let (config_port, config_host) = config
.config
.get("connect")
.map(|value| {
(
value
.get("port")
.and_then(|val| val.as_u64().map(|p| p as u16)),
value.get("host").and_then(|val| val.as_str()),
)
})
.unwrap_or_else(|| {
(
config
.config
.get("port")
.and_then(|port| port.as_u64().map(|p| p as u16)),
config.config.get("host").and_then(|host| host.as_str()),
)
});
let is_attach_with_connect = if config
.config
.get("request")
.is_some_and(|val| val.as_str().is_some_and(|request| request == "attach"))
{
if tcp_connection.host.is_some() && config_host.is_some() {
bail!("Cannot have two different hosts in debug configuration")
} else if tcp_connection.port.is_some() && config_port.is_some() {
bail!("Cannot have two different ports in debug configuration")
}
tcp_connection.port = config_port;
DebugpyLaunchMode::AttachWithConnect { host: config_host }
} else {
DebugpyLaunchMode::Normal
};
let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let python_path = if let Some(toolchain) = python_from_toolchain {
@@ -388,7 +330,6 @@ impl PythonDebugAdapter {
let arguments = Self::generate_debugpy_arguments(
&host,
port,
is_attach_with_connect,
user_installed_path.as_deref(),
user_args,
)
@@ -824,58 +765,29 @@ impl DebugAdapter for PythonDebugAdapter {
.await;
}
let base_paths = ["cwd", "program", "module"]
.into_iter()
.filter_map(|key| {
config.config.get(key).and_then(|cwd| {
RelPath::new(
cwd.as_str()
.map(Path::new)?
.strip_prefix(delegate.worktree_root_path())
.ok()?,
PathStyle::local(),
)
.ok()
})
})
.chain(
// While Debugpy's wiki saids absolute paths are required, but it actually supports relative paths when cwd is passed in.
// (Which should always be the case because Zed defaults to the cwd worktree root)
// So we want to check that these relative paths find toolchains as well. Otherwise, they won't be checked
// because the strip prefix in the iteration above will return an error
config
.config
.get("cwd")
.map(|_| {
["program", "module"].into_iter().filter_map(|key| {
config.config.get(key).and_then(|value| {
let path = Path::new(value.as_str()?);
RelPath::new(path, PathStyle::local()).ok()
})
})
})
.into_iter()
.flatten(),
)
.chain([RelPath::empty().into()]);
let mut toolchain = None;
for base_path in base_paths {
if let Some(found_toolchain) = delegate
.toolchain_store()
.active_toolchain(
delegate.worktree_id(),
base_path.into_arc(),
language::LanguageName::new(Self::LANGUAGE_NAME),
cx,
let base_path = config
.config
.get("cwd")
.and_then(|cwd| {
RelPath::new(
cwd.as_str()
.map(Path::new)?
.strip_prefix(delegate.worktree_root_path())
.ok()?,
PathStyle::local(),
)
.await
{
toolchain = Some(found_toolchain);
break;
}
}
.ok()
})
.unwrap_or_else(|| RelPath::empty().into());
let toolchain = delegate
.toolchain_store()
.active_toolchain(
delegate.worktree_id(),
base_path.into_arc(),
language::LanguageName::new(Self::LANGUAGE_NAME),
cx,
)
.await;
self.fetch_debugpy_whl(toolchain.clone(), delegate)
.await
@@ -912,148 +824,7 @@ mod tests {
use util::path;
use super::*;
use task::TcpArgumentsTemplate;
#[gpui::test]
async fn test_tcp_connection_conflict_with_connect_args() {
let adapter = PythonDebugAdapter {
base_venv_path: OnceCell::new(),
debugpy_whl_base_path: OnceCell::new(),
};
let config_with_port_conflict = json!({
"request": "attach",
"connect": {
"port": 5679
}
});
let tcp_connection = TcpArgumentsTemplate {
host: None,
port: Some(5678),
timeout: None,
};
let task_def = DebugTaskDefinition {
label: "test".into(),
adapter: PythonDebugAdapter::ADAPTER_NAME.into(),
config: config_with_port_conflict,
tcp_connection: Some(tcp_connection.clone()),
};
let result = adapter
.get_installed_binary(
&test_mocks::MockDelegate::new(),
&task_def,
None,
None,
None,
Some("python3".to_string()),
)
.await;
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Cannot have two different ports")
);
let host = Ipv4Addr::new(127, 0, 0, 1);
let config_with_host_conflict = json!({
"request": "attach",
"connect": {
"host": "192.168.1.1",
"port": 5678
}
});
let tcp_connection_with_host = TcpArgumentsTemplate {
host: Some(host),
port: None,
timeout: None,
};
let task_def_host = DebugTaskDefinition {
label: "test".into(),
adapter: PythonDebugAdapter::ADAPTER_NAME.into(),
config: config_with_host_conflict,
tcp_connection: Some(tcp_connection_with_host),
};
let result_host = adapter
.get_installed_binary(
&test_mocks::MockDelegate::new(),
&task_def_host,
None,
None,
None,
Some("python3".to_string()),
)
.await;
assert!(result_host.is_err());
assert!(
result_host
.unwrap_err()
.to_string()
.contains("Cannot have two different hosts")
);
}
#[gpui::test]
async fn test_attach_with_connect_mode_generates_correct_arguments() {
let host = Ipv4Addr::new(127, 0, 0, 1);
let port = 5678;
let args_without_host = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::AttachWithConnect { host: None },
None,
None,
)
.await
.unwrap();
let expected_suffix = path!("debug_adapters/Debugpy/debugpy/adapter");
assert!(args_without_host[0].ends_with(expected_suffix));
assert_eq!(args_without_host[1], "connect");
assert_eq!(args_without_host[2], "5678");
let args_with_host = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::AttachWithConnect {
host: Some("192.168.1.100"),
},
None,
None,
)
.await
.unwrap();
assert!(args_with_host[0].ends_with(expected_suffix));
assert_eq!(args_with_host[1], "connect");
assert_eq!(args_with_host[2], "192.168.1.100:");
assert_eq!(args_with_host[3], "5678");
let args_normal = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::Normal,
None,
None,
)
.await
.unwrap();
assert!(args_normal[0].ends_with(expected_suffix));
assert_eq!(args_normal[1], "--host=127.0.0.1");
assert_eq!(args_normal[2], "--port=5678");
assert!(!args_normal.contains(&"connect".to_string()));
}
use std::{net::Ipv4Addr, path::PathBuf};
#[gpui::test]
async fn test_debugpy_install_path_cases() {
@@ -1062,25 +833,15 @@ mod tests {
// Case 1: User-defined debugpy path (highest precedence)
let user_path = PathBuf::from("/custom/path/to/debugpy/src/debugpy/adapter");
let user_args = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::Normal,
Some(&user_path),
None,
)
.await
.unwrap();
let user_args =
PythonDebugAdapter::generate_debugpy_arguments(&host, port, Some(&user_path), None)
.await
.unwrap();
let venv_args = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::Normal,
None,
None,
)
.await
.unwrap();
// Case 2: Venv-installed debugpy (uses -m debugpy.adapter)
let venv_args = PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, None)
.await
.unwrap();
assert_eq!(user_args[0], "/custom/path/to/debugpy/src/debugpy/adapter");
assert_eq!(user_args[1], "--host=127.0.0.1");
@@ -1095,7 +856,6 @@ mod tests {
let user_args = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::Normal,
Some(&user_path),
Some(vec!["foo".into()]),
)
@@ -1104,7 +864,6 @@ mod tests {
let venv_args = PythonDebugAdapter::generate_debugpy_arguments(
&host,
port,
DebugpyLaunchMode::Normal,
None,
Some(vec!["foo".into()]),
)

View File

@@ -697,7 +697,6 @@ impl Render for NewProcessModal {
.justify_between()
.border_t_1()
.border_color(cx.theme().colors().border_variant);
let secondary_action = menu::SecondaryConfirm.boxed_clone();
match self.mode {
NewProcessMode::Launch => el.child(
container
@@ -707,7 +706,6 @@ impl Render for NewProcessModal {
.on_click(cx.listener(|this, _, window, cx| {
this.save_debug_scenario(window, cx);
}))
.key_binding(KeyBinding::for_action(&*secondary_action, cx))
.disabled(
self.debugger.is_none()
|| self
@@ -751,6 +749,7 @@ impl Render for NewProcessModal {
container
.child(div().child({
Button::new("edit-attach-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action(&*secondary_action, cx))
.on_click(move |_, window, cx| {
window.dispatch_action(secondary_action.boxed_clone(), cx)
@@ -1193,7 +1192,7 @@ impl PickerDelegate for DebugDelegate {
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
"Find a debug task, or debug a command".into()
"Find a debug task, or debug a command.".into()
}
fn update_matches(
@@ -1454,17 +1453,18 @@ impl PickerDelegate for DebugDelegate {
.child({
let action = menu::SecondaryConfirm.boxed_clone();
if self.matches.is_empty() {
Button::new("edit-debug-json", "Edit debug.json").on_click(cx.listener(
|_picker, _, window, cx| {
Button::new("edit-debug-json", "Edit debug.json")
.label_size(LabelSize::Small)
.on_click(cx.listener(|_picker, _, window, cx| {
window.dispatch_action(
zed_actions::OpenProjectDebugTasks.boxed_clone(),
cx,
);
cx.emit(DismissEvent);
},
))
}))
} else {
Button::new("edit-debug-task", "Edit in debug.json")
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action(&*action, cx))
.on_click(move |_, window, cx| {
window.dispatch_action(action.boxed_clone(), cx)

View File

@@ -6,10 +6,7 @@ use alacritty_terminal::vte::ansi;
use anyhow::Result;
use collections::HashMap;
use dap::{CompletionItem, CompletionItemType, OutputEvent};
use editor::{
Bias, CompletionProvider, Editor, EditorElement, EditorMode, EditorStyle, ExcerptId,
SizingBehavior,
};
use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
use fuzzy::StringMatchCandidate;
use gpui::{
Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla,
@@ -62,11 +59,6 @@ impl Console {
) -> Self {
let console = cx.new(|cx| {
let mut editor = Editor::multi_line(window, cx);
editor.set_mode(EditorMode::Full {
scale_ui_elements_with_buffer_font_size: true,
show_active_line_background: true,
sizing_behavior: SizingBehavior::ExcludeOverscrollMargin,
});
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
editor.set_read_only(true);
editor.disable_scrollbars_and_minimap(window, cx);

View File

@@ -34,7 +34,6 @@ theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
itertools.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }

View File

@@ -1,5 +1,5 @@
use crate::{
DIAGNOSTICS_UPDATE_DEBOUNCE, IncludeWarnings, ToggleWarnings, context_range_for_entry,
DIAGNOSTICS_UPDATE_DELAY, IncludeWarnings, ToggleWarnings, context_range_for_entry,
diagnostic_renderer::{DiagnosticBlock, DiagnosticRenderer},
toolbar_controls::DiagnosticsToolbarEditor,
};
@@ -283,7 +283,7 @@ impl BufferDiagnosticsEditor {
self.update_excerpts_task = Some(cx.spawn_in(window, async move |editor, cx| {
cx.background_executor()
.timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
.timer(DIAGNOSTICS_UPDATE_DELAY)
.await;
if let Some(buffer) = buffer {
@@ -938,6 +938,10 @@ impl DiagnosticsToolbarEditor for WeakEntity<BufferDiagnosticsEditor> {
.unwrap_or(false)
}
fn has_stale_excerpts(&self, _cx: &App) -> bool {
false
}
fn is_updating(&self, cx: &App) -> bool {
self.read_with(cx, |buffer_diagnostics_editor, cx| {
buffer_diagnostics_editor.update_excerpts_task.is_some()

View File

@@ -9,7 +9,7 @@ mod diagnostics_tests;
use anyhow::Result;
use buffer_diagnostics::BufferDiagnosticsEditor;
use collections::{BTreeSet, HashMap, HashSet};
use collections::{BTreeSet, HashMap};
use diagnostic_renderer::DiagnosticBlock;
use editor::{
Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
@@ -17,11 +17,10 @@ use editor::{
multibuffer_context_lines,
};
use gpui::{
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, FocusOutEvent,
Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
Styled, Subscription, Task, WeakEntity, Window, actions, div,
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, Task, WeakEntity, Window, actions, div,
};
use itertools::Itertools as _;
use language::{
Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, DiagnosticEntryRef, Point,
ToTreeSitterPoint,
@@ -33,7 +32,7 @@ use project::{
use settings::Settings;
use std::{
any::{Any, TypeId},
cmp,
cmp::{self, Ordering},
ops::{Range, RangeInclusive},
sync::Arc,
time::Duration,
@@ -90,8 +89,8 @@ pub(crate) struct ProjectDiagnosticsEditor {
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
const DIAGNOSTICS_SUMMARY_UPDATE_DEBOUNCE: Duration = Duration::from_millis(30);
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
const DIAGNOSTICS_SUMMARY_UPDATE_DELAY: Duration = Duration::from_millis(30);
impl Render for ProjectDiagnosticsEditor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -150,12 +149,6 @@ impl Render for ProjectDiagnosticsEditor {
}
}
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
enum RetainExcerpts {
Yes,
No,
}
impl ProjectDiagnosticsEditor {
pub fn register(
workspace: &mut Workspace,
@@ -172,21 +165,14 @@ impl ProjectDiagnosticsEditor {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let project_event_subscription = cx.subscribe_in(
&project_handle,
window,
|this, _project, event, window, cx| match event {
let project_event_subscription =
cx.subscribe_in(&project_handle, window, |this, _project, event, window, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
}
project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
log::debug!("disk based diagnostics finished for server {language_server_id}");
this.close_diagnosticless_buffers(
window,
cx,
this.editor.focus_handle(cx).contains_focused(window, cx)
|| this.focus_handle.contains_focused(window, cx),
);
this.update_stale_excerpts(window, cx);
}
project::Event::DiagnosticsUpdated {
language_server_id,
@@ -195,39 +181,34 @@ impl ProjectDiagnosticsEditor {
this.paths_to_update.extend(paths.clone());
this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(DIAGNOSTICS_SUMMARY_UPDATE_DEBOUNCE)
.timer(DIAGNOSTICS_SUMMARY_UPDATE_DELAY)
.await;
this.update(cx, |this, cx| {
this.update_diagnostic_summary(cx);
})
.log_err();
});
cx.emit(EditorEvent::TitleChanged);
log::debug!(
"diagnostics updated for server {language_server_id}, \
paths {paths:?}. updating excerpts"
);
let focused = this.editor.focus_handle(cx).contains_focused(window, cx)
|| this.focus_handle.contains_focused(window, cx);
this.update_stale_excerpts(
if focused {
RetainExcerpts::Yes
} else {
RetainExcerpts::No
},
window,
cx,
);
if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) {
log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. recording change");
} else {
log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. updating excerpts");
this.update_stale_excerpts(window, cx);
}
}
_ => {}
},
);
});
let focus_handle = cx.focus_handle();
cx.on_focus_in(&focus_handle, window, Self::focus_in)
.detach();
cx.on_focus_out(&focus_handle, window, Self::focus_out)
.detach();
cx.on_focus_in(&focus_handle, window, |this, window, cx| {
this.focus_in(window, cx)
})
.detach();
cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| {
this.focus_out(window, cx)
})
.detach();
let excerpts = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
let editor = cx.new(|cx| {
@@ -257,11 +238,8 @@ impl ProjectDiagnosticsEditor {
window.focus(&this.focus_handle);
}
}
EditorEvent::Blurred => this.close_diagnosticless_buffers(window, cx, false),
EditorEvent::Saved => this.close_diagnosticless_buffers(window, cx, true),
EditorEvent::SelectionsChanged { .. } => {
this.close_diagnosticless_buffers(window, cx, true)
}
EditorEvent::Blurred => this.update_stale_excerpts(window, cx),
EditorEvent::Saved => this.update_stale_excerpts(window, cx),
_ => {}
}
},
@@ -305,67 +283,15 @@ impl ProjectDiagnosticsEditor {
this
}
/// Closes all excerpts of buffers that:
/// - have no diagnostics anymore
/// - are saved (not dirty)
/// - and, if `reatin_selections` is true, do not have selections within them
fn close_diagnosticless_buffers(
&mut self,
_window: &mut Window,
cx: &mut Context<Self>,
retain_selections: bool,
) {
let buffer_ids = self.multibuffer.read(cx).all_buffer_ids();
let selected_buffers = self.editor.update(cx, |editor, cx| {
editor
.selections
.all_anchors(cx)
.iter()
.filter_map(|anchor| anchor.start.buffer_id)
.collect::<HashSet<_>>()
});
for buffer_id in buffer_ids {
if retain_selections && selected_buffers.contains(&buffer_id) {
continue;
}
let has_blocks = self
.blocks
.get(&buffer_id)
.is_none_or(|blocks| blocks.is_empty());
if !has_blocks {
continue;
}
let is_dirty = self
.multibuffer
.read(cx)
.buffer(buffer_id)
.is_some_and(|buffer| buffer.read(cx).is_dirty());
if !is_dirty {
continue;
}
self.multibuffer.update(cx, |b, cx| {
b.remove_excerpts_for_buffer(buffer_id, cx);
});
}
}
fn update_stale_excerpts(
&mut self,
mut retain_excerpts: RetainExcerpts,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.update_excerpts_task.is_some() {
fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.update_excerpts_task.is_some() || self.multibuffer.read(cx).is_dirty(cx) {
return;
}
if self.multibuffer.read(cx).is_dirty(cx) {
retain_excerpts = RetainExcerpts::Yes;
}
let project_handle = self.project.clone();
self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
.timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
.timer(DIAGNOSTICS_UPDATE_DELAY)
.await;
loop {
let Some(path) = this.update(cx, |this, cx| {
@@ -386,7 +312,7 @@ impl ProjectDiagnosticsEditor {
.log_err()
{
this.update_in(cx, |this, window, cx| {
this.update_excerpts(buffer, retain_excerpts, window, cx)
this.update_excerpts(buffer, window, cx)
})?
.await?;
}
@@ -452,10 +378,10 @@ impl ProjectDiagnosticsEditor {
}
}
fn focus_out(&mut self, _: FocusOutEvent, window: &mut Window, cx: &mut Context<Self>) {
fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
{
self.close_diagnosticless_buffers(window, cx, false);
self.update_stale_excerpts(window, cx);
}
}
@@ -477,13 +403,12 @@ impl ProjectDiagnosticsEditor {
});
}
}
multibuffer.clear(cx);
});
self.paths_to_update = project_paths;
});
self.update_stale_excerpts(RetainExcerpts::No, window, cx);
self.update_stale_excerpts(window, cx);
}
fn diagnostics_are_unchanged(
@@ -506,7 +431,6 @@ impl ProjectDiagnosticsEditor {
fn update_excerpts(
&mut self,
buffer: Entity<Buffer>,
retain_excerpts: RetainExcerpts,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
@@ -573,27 +497,24 @@ impl ProjectDiagnosticsEditor {
)
})?;
blocks.extend(more);
for item in more {
let i = blocks
.binary_search_by(|probe| {
probe
.initial_range
.start
.cmp(&item.initial_range.start)
.then(probe.initial_range.end.cmp(&item.initial_range.end))
.then(Ordering::Greater)
})
.unwrap_or_else(|i| i);
blocks.insert(i, item);
}
}
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = match retain_excerpts {
RetainExcerpts::No => Vec::new(),
RetainExcerpts::Yes => this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multi_buffer, cx| {
multi_buffer
.excerpts_for_buffer(buffer_id, cx)
.into_iter()
.map(|(_, range)| ExcerptRange {
context: range.context.to_point(&buffer_snapshot),
primary: range.primary.to_point(&buffer_snapshot),
})
.collect()
})
})?,
};
let mut result_blocks = vec![None; excerpt_ranges.len()];
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
let context_lines = cx.update(|_, cx| multibuffer_context_lines(cx))?;
for b in blocks {
for b in blocks.iter() {
let excerpt_range = context_range_for_entry(
b.initial_range.clone(),
context_lines,
@@ -620,8 +541,7 @@ impl ProjectDiagnosticsEditor {
context: excerpt_range,
primary: b.initial_range.clone(),
},
);
result_blocks.insert(i, Some(b));
)
}
this.update_in(cx, |this, window, cx| {
@@ -642,7 +562,7 @@ impl ProjectDiagnosticsEditor {
)
});
#[cfg(test)]
let cloned_blocks = result_blocks.clone();
let cloned_blocks = blocks.clone();
if was_empty && let Some(anchor_range) = anchor_ranges.first() {
let range_to_select = anchor_range.start..anchor_range.start;
@@ -656,20 +576,22 @@ impl ProjectDiagnosticsEditor {
}
}
let editor_blocks = anchor_ranges
.into_iter()
.zip_eq(result_blocks.into_iter())
.filter_map(|(anchor, block)| {
let block = block?;
let editor = this.editor.downgrade();
Some(BlockProperties {
placement: BlockPlacement::Near(anchor.start),
height: Some(1),
style: BlockStyle::Flex,
render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
priority: 1,
})
});
let editor_blocks =
anchor_ranges
.into_iter()
.zip(blocks.into_iter())
.map(|(anchor, block)| {
let editor = this.editor.downgrade();
BlockProperties {
placement: BlockPlacement::Near(anchor.start),
height: Some(1),
style: BlockStyle::Flex,
render: Arc::new(move |bcx| {
block.render_block(editor.clone(), bcx)
}),
priority: 1,
}
});
let block_ids = this.editor.update(cx, |editor, cx| {
editor.display_map.update(cx, |display_map, cx| {
@@ -679,9 +601,7 @@ impl ProjectDiagnosticsEditor {
#[cfg(test)]
{
for (block_id, block) in
block_ids.iter().zip(cloned_blocks.into_iter().flatten())
{
for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
let markdown = block.markdown.clone();
editor::test::set_block_content_for_tests(
&this.editor,
@@ -706,7 +626,6 @@ impl ProjectDiagnosticsEditor {
fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
self.summary = self.project.read(cx).diagnostic_summary(false, cx);
cx.emit(EditorEvent::TitleChanged);
}
}
@@ -924,6 +843,13 @@ impl DiagnosticsToolbarEditor for WeakEntity<ProjectDiagnosticsEditor> {
.unwrap_or(false)
}
fn has_stale_excerpts(&self, cx: &App) -> bool {
self.read_with(cx, |project_diagnostics_editor, _cx| {
!project_diagnostics_editor.paths_to_update.is_empty()
})
.unwrap_or(false)
}
fn is_updating(&self, cx: &App) -> bool {
self.read_with(cx, |project_diagnostics_editor, cx| {
project_diagnostics_editor.update_excerpts_task.is_some()
@@ -1084,6 +1010,12 @@ async fn heuristic_syntactic_expand(
return;
}
}
log::info!(
"Expanding to ancestor started on {} node\
exceeding row limit of {max_row_count}.",
node.grammar_name()
);
*ancestor_range = Some(None);
}
})

View File

@@ -119,7 +119,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
pretty_assertions::assert_eq!(
@@ -190,7 +190,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
});
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
pretty_assertions::assert_eq!(
@@ -277,7 +277,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
});
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
pretty_assertions::assert_eq!(
@@ -391,7 +391,7 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
// Only the first language server's diagnostics are shown.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
editor.update_in(cx, |editor, window, cx| {
editor.fold_ranges(vec![Point::new(0, 0)..Point::new(3, 0)], false, window, cx);
@@ -490,7 +490,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// Only the first language server's diagnostics are shown.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
@@ -530,7 +530,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// Both language server's diagnostics are shown.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
@@ -587,7 +587,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// Only the first language server's diagnostics are updated.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
@@ -629,7 +629,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// Both language servers' diagnostics are updated.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.executor().run_until_parked();
pretty_assertions::assert_eq!(
@@ -760,7 +760,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
.unwrap()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
@@ -769,7 +769,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
log::info!("updating mutated diagnostics view");
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
diagnostics.update_stale_excerpts(window, cx)
});
log::info!("constructing reference diagnostics view");
@@ -777,7 +777,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
let mutated_excerpts =
@@ -789,12 +789,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
// The mutated view may contain more than the reference view as
// we don't currently shrink excerpts when diagnostics were removed.
let mut ref_iter = reference_excerpts.lines().filter(|line| {
// ignore $ ---- and $ <file>.rs
!line.starts_with('§')
|| line.starts_with("§ diagnostic")
|| line.starts_with("§ related info")
});
let mut ref_iter = reference_excerpts.lines().filter(|line| *line != "§ -----");
let mut next_ref_line = ref_iter.next();
let mut skipped_block = false;
@@ -802,12 +797,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
if let Some(ref_line) = next_ref_line {
if mut_line == ref_line {
next_ref_line = ref_iter.next();
} else if mut_line.contains('§')
// ignore $ ---- and $ <file>.rs
&& (!mut_line.starts_with('§')
|| mut_line.starts_with("§ diagnostic")
|| mut_line.starts_with("§ related info"))
{
} else if mut_line.contains('§') && mut_line != "§ -----" {
skipped_block = true;
}
}
@@ -887,7 +877,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
vec![Inlay::edit_prediction(
post_inc(&mut next_inlay_id),
snapshot.buffer_snapshot().anchor_before(position),
Rope::from_iter(["Test inlay ", "next_inlay_id"]),
Rope::from_iter_small(["Test inlay ", "next_inlay_id"]),
)],
cx,
);
@@ -959,7 +949,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
.unwrap()
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
@@ -968,11 +958,11 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
log::info!("updating mutated diagnostics view");
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
diagnostics.update_stale_excerpts(window, cx)
});
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
cx.run_until_parked();
}
@@ -1437,7 +1427,7 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
diagnostics
.next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
.next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
.await;
// Verify that the diagnostic codes are displayed correctly
@@ -1714,7 +1704,7 @@ async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
@@ -1847,7 +1837,7 @@ async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
@@ -1981,7 +1971,7 @@ async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
@@ -2080,7 +2070,7 @@ fn random_lsp_diagnostic(
const ERROR_MARGIN: usize = 10;
let file_content = fs.read_file_sync(path).unwrap();
let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref());
let file_text = Rope::from_str_small(String::from_utf8_lossy(&file_content).as_ref());
let start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN));
let end = rng.random_range(start..file_text.len().saturating_add(ERROR_MARGIN));

View File

@@ -16,6 +16,9 @@ pub(crate) trait DiagnosticsToolbarEditor: Send + Sync {
/// Toggles whether warning diagnostics should be displayed by the
/// diagnostics editor.
fn toggle_warnings(&self, window: &mut Window, cx: &mut App);
/// Indicates whether any of the excerpts displayed by the diagnostics
/// editor are stale.
fn has_stale_excerpts(&self, cx: &App) -> bool;
/// Indicates whether the diagnostics editor is currently updating the
/// diagnostics.
fn is_updating(&self, cx: &App) -> bool;
@@ -34,12 +37,14 @@ pub(crate) trait DiagnosticsToolbarEditor: Send + Sync {
impl Render for ToolbarControls {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let mut has_stale_excerpts = false;
let mut include_warnings = false;
let mut is_updating = false;
match &self.editor {
Some(editor) => {
include_warnings = editor.include_warnings(cx);
has_stale_excerpts = editor.has_stale_excerpts(cx);
is_updating = editor.is_updating(cx);
}
None => {}
@@ -81,6 +86,7 @@ impl Render for ToolbarControls {
IconButton::new("refresh-diagnostics", IconName::ArrowCircle)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
.disabled(!has_stale_excerpts)
.tooltip(Tooltip::for_action_title(
"Refresh diagnostics",
&ToggleDiagnosticsRefresh,

View File

@@ -13,7 +13,7 @@ use gpui::{
};
use indoc::indoc;
use language::{
EditPredictionsMode, File, Language,
EditPredictionsMode, File, Language, Rope,
language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings},
};
use project::DisableAiSettings;
@@ -1056,8 +1056,11 @@ async fn open_disabled_globs_setting_in_editor(
) -> Result<()> {
let settings_editor = workspace
.update_in(cx, |_, window, cx| {
create_and_open_local_file(paths::settings_file(), window, cx, || {
settings::initial_user_settings_content().as_ref().into()
create_and_open_local_file(paths::settings_file(), window, cx, |cx| {
Rope::from_str(
settings::initial_user_settings_content().as_ref(),
cx.background_executor(),
)
})
})?
.await?

View File

@@ -621,6 +621,8 @@ actions!(
NextEditPrediction,
/// Scrolls to the next screen.
NextScreen,
/// Goes to the next snippet tabstop if one exists.
NextSnippetTabstop,
/// Opens the context menu at cursor position.
OpenContextMenu,
/// Opens excerpts from the current file.
@@ -654,6 +656,8 @@ actions!(
Paste,
/// Navigates to the previous edit prediction.
PreviousEditPrediction,
/// Goes to the previous snippet tabstop if one exists.
PreviousSnippetTabstop,
/// Redoes the last undone edit.
Redo,
/// Redoes the last selection change.

View File

@@ -28,12 +28,10 @@ use std::{
rc::Rc,
};
use task::ResolvedTask;
use ui::{
Color, IntoElement, ListItem, Pixels, Popover, ScrollAxes, Scrollbars, Styled, WithScrollbar,
prelude::*,
};
use ui::{Color, IntoElement, ListItem, Pixels, Popover, Styled, prelude::*};
use util::ResultExt;
use crate::CodeActionSource;
use crate::hover_popover::{hover_markdown_style, open_markdown_url};
use crate::{
CodeActionProvider, CompletionId, CompletionItemKind, CompletionProvider, DisplayRow, Editor,
@@ -41,8 +39,7 @@ use crate::{
actions::{ConfirmCodeAction, ConfirmCompletion},
split_words, styled_runs_for_code_label,
};
use crate::{CodeActionSource, EditorSettings};
use settings::{Settings, SnippetSortOrder};
use settings::SnippetSortOrder;
pub const MENU_GAP: Pixels = px(4.);
pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
@@ -264,20 +261,6 @@ impl Drop for CompletionsMenu {
}
}
struct CompletionMenuScrollBarSetting;
impl ui::scrollbars::GlobalSetting for CompletionMenuScrollBarSetting {
fn get_value(_cx: &App) -> &Self {
&Self
}
}
impl ui::scrollbars::ScrollbarVisibility for CompletionMenuScrollBarSetting {
fn visibility(&self, cx: &App) -> ui::scrollbars::ShowScrollbar {
EditorSettings::get_global(cx).completion_menu_scrollbar
}
}
impl CompletionsMenu {
pub fn new(
id: CompletionId,
@@ -915,17 +898,7 @@ impl CompletionsMenu {
}
});
Popover::new()
.child(
div().child(list).custom_scrollbars(
Scrollbars::for_settings::<CompletionMenuScrollBarSetting>()
.show_along(ScrollAxes::Vertical)
.tracked_scroll_handle(self.scroll_handle.clone()),
window,
cx,
),
)
.into_any_element()
Popover::new().child(list).into_any_element()
}
fn render_aside(

View File

@@ -17,9 +17,6 @@
//! [Editor]: crate::Editor
//! [EditorElement]: crate::element::EditorElement
#[macro_use]
mod dimensions;
mod block_map;
mod crease_map;
mod custom_highlights;
@@ -170,12 +167,11 @@ impl DisplayMap {
}
pub fn snapshot(&mut self, cx: &mut Context<Self>) -> DisplaySnapshot {
let tab_size = Self::tab_size(&self.buffer, cx);
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot, edits);
let tab_size = Self::tab_size(&self.buffer, cx);
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot, edits, tab_size);
let (wrap_snapshot, edits) = self
.wrap_map
@@ -919,7 +915,7 @@ impl DisplaySnapshot {
pub fn text_chunks(&self, display_row: DisplayRow) -> impl Iterator<Item = &str> {
self.block_snapshot
.chunks(
BlockRow(display_row.0)..BlockRow(self.max_point().row().next_row().0),
display_row.0..self.max_point().row().next_row().0,
false,
self.masked,
Highlights::default(),
@@ -931,12 +927,7 @@ impl DisplaySnapshot {
pub fn reverse_text_chunks(&self, display_row: DisplayRow) -> impl Iterator<Item = &str> {
(0..=display_row.0).rev().flat_map(move |row| {
self.block_snapshot
.chunks(
BlockRow(row)..BlockRow(row + 1),
false,
self.masked,
Highlights::default(),
)
.chunks(row..row + 1, false, self.masked, Highlights::default())
.map(|h| h.text)
.collect::<Vec<_>>()
.into_iter()
@@ -951,7 +942,7 @@ impl DisplaySnapshot {
highlight_styles: HighlightStyles,
) -> DisplayChunks<'_> {
self.block_snapshot.chunks(
BlockRow(display_rows.start.0)..BlockRow(display_rows.end.0),
display_rows.start.0..display_rows.end.0,
language_aware,
self.masked,
Highlights {
@@ -1187,8 +1178,8 @@ impl DisplaySnapshot {
rows: Range<DisplayRow>,
) -> impl Iterator<Item = (DisplayRow, &Block)> {
self.block_snapshot
.blocks_in_range(BlockRow(rows.start.0)..BlockRow(rows.end.0))
.map(|(row, block)| (DisplayRow(row.0), block))
.blocks_in_range(rows.start.0..rows.end.0)
.map(|(row, block)| (DisplayRow(row), block))
}
pub fn sticky_header_excerpt(&self, row: f64) -> Option<StickyHeaderExcerpt<'_>> {
@@ -1220,7 +1211,7 @@ impl DisplaySnapshot {
pub fn soft_wrap_indent(&self, display_row: DisplayRow) -> Option<u32> {
let wrap_row = self
.block_snapshot
.to_wrap_point(BlockPoint::new(BlockRow(display_row.0), 0), Bias::Left)
.to_wrap_point(BlockPoint::new(display_row.0, 0), Bias::Left)
.row();
self.wrap_snapshot().soft_wrap_indent(wrap_row)
}
@@ -1251,7 +1242,7 @@ impl DisplaySnapshot {
}
pub fn longest_row(&self) -> DisplayRow {
DisplayRow(self.block_snapshot.longest_row().0)
DisplayRow(self.block_snapshot.longest_row())
}
pub fn longest_row_in_range(&self, range: Range<DisplayRow>) -> DisplayRow {
@@ -1578,6 +1569,7 @@ pub mod tests {
use lsp::LanguageServerId;
use project::Project;
use rand::{Rng, prelude::*};
use rope::Rope;
use settings::{SettingsContent, SettingsStore};
use smol::stream::StreamExt;
use std::{env, sync::Arc};
@@ -2083,7 +2075,7 @@ pub mod tests {
vec![Inlay::edit_prediction(
0,
buffer_snapshot.anchor_after(0),
"\n",
Rope::from_str_small("\n"),
)],
cx,
);

File diff suppressed because it is too large Load Diff

View File

@@ -1,96 +0,0 @@
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct RowDelta(pub u32);
impl RowDelta {
pub fn saturating_sub(self, other: RowDelta) -> RowDelta {
RowDelta(self.0.saturating_sub(other.0))
}
}
impl ::std::ops::Add for RowDelta {
type Output = RowDelta;
fn add(self, rhs: RowDelta) -> Self::Output {
RowDelta(self.0 + rhs.0)
}
}
impl ::std::ops::Sub for RowDelta {
type Output = RowDelta;
fn sub(self, rhs: RowDelta) -> Self::Output {
RowDelta(self.0 - rhs.0)
}
}
impl ::std::ops::AddAssign for RowDelta {
fn add_assign(&mut self, rhs: RowDelta) {
self.0 += rhs.0;
}
}
impl ::std::ops::SubAssign for RowDelta {
fn sub_assign(&mut self, rhs: RowDelta) {
self.0 -= rhs.0;
}
}
macro_rules! impl_for_row_types {
($row:ident => $row_delta:ident) => {
impl $row {
pub fn saturating_sub(self, other: $row_delta) -> $row {
$row(self.0.saturating_sub(other.0))
}
}
impl ::std::ops::Add for $row {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl ::std::ops::Add<$row_delta> for $row {
type Output = Self;
fn add(self, rhs: $row_delta) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl ::std::ops::Sub for $row {
type Output = $row_delta;
fn sub(self, rhs: Self) -> Self::Output {
$row_delta(self.0 - rhs.0)
}
}
impl ::std::ops::Sub<$row_delta> for $row {
type Output = $row;
fn sub(self, rhs: $row_delta) -> Self::Output {
$row(self.0 - rhs.0)
}
}
impl ::std::ops::AddAssign for $row {
fn add_assign(&mut self, rhs: Self) {
self.0 += rhs.0;
}
}
impl ::std::ops::AddAssign<$row_delta> for $row {
fn add_assign(&mut self, rhs: $row_delta) {
self.0 += rhs.0;
}
}
impl ::std::ops::SubAssign<$row_delta> for $row {
fn sub_assign(&mut self, rhs: $row_delta) {
self.0 -= rhs.0;
}
}
};
}

View File

@@ -700,16 +700,20 @@ impl InlayMap {
.collect::<String>();
let next_inlay = if i % 2 == 0 {
use rope::Rope;
Inlay::mock_hint(
post_inc(next_inlay_id),
snapshot.buffer.anchor_at(position, bias),
&text,
Rope::from_str_small(&text),
)
} else {
use rope::Rope;
Inlay::edit_prediction(
post_inc(next_inlay_id),
snapshot.buffer.anchor_at(position, bias),
&text,
Rope::from_str_small(&text),
)
};
let inlay_id = next_inlay.id;
@@ -1301,7 +1305,7 @@ mod tests {
vec![Inlay::mock_hint(
post_inc(&mut next_inlay_id),
buffer.read(cx).snapshot(cx).anchor_after(3),
"|123|",
Rope::from_str_small("|123|"),
)],
);
assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
@@ -1378,12 +1382,12 @@ mod tests {
Inlay::mock_hint(
post_inc(&mut next_inlay_id),
buffer.read(cx).snapshot(cx).anchor_before(3),
"|123|",
Rope::from_str_small("|123|"),
),
Inlay::edit_prediction(
post_inc(&mut next_inlay_id),
buffer.read(cx).snapshot(cx).anchor_after(3),
"|456|",
Rope::from_str_small("|456|"),
),
],
);
@@ -1593,17 +1597,17 @@ mod tests {
Inlay::mock_hint(
post_inc(&mut next_inlay_id),
buffer.read(cx).snapshot(cx).anchor_before(0),
"|123|\n",
Rope::from_str_small("|123|\n"),
),
Inlay::mock_hint(
post_inc(&mut next_inlay_id),
buffer.read(cx).snapshot(cx).anchor_before(4),
"|456|",
Rope::from_str_small("|456|"),
),
Inlay::edit_prediction(
post_inc(&mut next_inlay_id),
buffer.read(cx).snapshot(cx).anchor_before(7),
"\n|567|\n",
Rope::from_str_small("\n|567|\n"),
),
],
);
@@ -1677,9 +1681,14 @@ mod tests {
(offset, inlay.clone())
})
.collect::<Vec<_>>();
let mut expected_text = Rope::from(&buffer_snapshot.text());
let mut expected_text =
Rope::from_str(&buffer_snapshot.text(), cx.background_executor());
for (offset, inlay) in inlays.iter().rev() {
expected_text.replace(*offset..*offset, &inlay.text().to_string());
expected_text.replace(
*offset..*offset,
&inlay.text().to_string(),
cx.background_executor(),
);
}
assert_eq!(inlay_snapshot.text(), expected_text.to_string());
@@ -2067,7 +2076,7 @@ mod tests {
let inlay = Inlay {
id: InlayId::Hint(0),
position,
content: InlayContent::Text(text::Rope::from(inlay_text)),
content: InlayContent::Text(text::Rope::from_str(inlay_text, cx.background_executor())),
};
let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]);
@@ -2181,7 +2190,10 @@ mod tests {
let inlay = Inlay {
id: InlayId::Hint(0),
position,
content: InlayContent::Text(text::Rope::from(test_case.inlay_text)),
content: InlayContent::Text(text::Rope::from_str(
test_case.inlay_text,
cx.background_executor(),
)),
};
let (inlay_snapshot, _) = inlay_map.splice(&[], vec![inlay]);

View File

@@ -1042,7 +1042,7 @@ mod tests {
let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
let text = text::Rope::from(tabs_snapshot.text().as_str());
let text = text::Rope::from_str(tabs_snapshot.text().as_str(), cx.background_executor());
log::info!(
"TabMap text (tab size: {}): {:?}",
tab_size,

View File

@@ -1,6 +1,5 @@
use super::{
Highlights,
dimensions::RowDelta,
fold_map::{Chunk, FoldRows},
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
};
@@ -8,20 +7,13 @@ use gpui::{App, AppContext as _, Context, Entity, Font, LineWrapper, Pixels, Tas
use language::Point;
use multi_buffer::{MultiBufferSnapshot, RowInfo};
use smol::future::yield_now;
use std::{cmp, collections::VecDeque, mem, ops::Range, sync::LazyLock, time::Duration};
use std::sync::LazyLock;
use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
use sum_tree::{Bias, Cursor, Dimensions, SumTree};
use text::Patch;
pub use super::tab_map::TextSummary;
pub type WrapEdit = text::Edit<WrapRow>;
pub type WrapPatch = text::Patch<WrapRow>;
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct WrapRow(pub u32);
impl_for_row_types! {
WrapRow => RowDelta
}
pub type WrapEdit = text::Edit<u32>;
/// Handles soft wrapping of text.
///
@@ -29,8 +21,8 @@ impl_for_row_types! {
pub struct WrapMap {
snapshot: WrapSnapshot,
pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
interpolated_edits: WrapPatch,
edits_since_sync: WrapPatch,
interpolated_edits: Patch<u32>,
edits_since_sync: Patch<u32>,
wrap_width: Option<Pixels>,
background_task: Option<Task<()>>,
font_with_size: (Font, Pixels),
@@ -62,7 +54,7 @@ pub struct WrapChunks<'a> {
input_chunks: tab_map::TabChunks<'a>,
input_chunk: Chunk<'a>,
output_position: WrapPoint,
max_output_row: WrapRow,
max_output_row: u32,
transforms: Cursor<'a, 'static, Transform, Dimensions<WrapPoint, TabPoint>>,
snapshot: &'a WrapSnapshot,
}
@@ -71,19 +63,19 @@ pub struct WrapChunks<'a> {
pub struct WrapRows<'a> {
input_buffer_rows: FoldRows<'a>,
input_buffer_row: RowInfo,
output_row: WrapRow,
output_row: u32,
soft_wrapped: bool,
max_output_row: WrapRow,
max_output_row: u32,
transforms: Cursor<'a, 'static, Transform, Dimensions<WrapPoint, TabPoint>>,
}
impl WrapRows<'_> {
pub(crate) fn seek(&mut self, start_row: WrapRow) {
pub(crate) fn seek(&mut self, start_row: u32) {
self.transforms
.seek(&WrapPoint::new(start_row, 0), Bias::Left);
let mut input_row = self.transforms.start().1.row();
if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
input_row += (start_row - self.transforms.start().0.row()).0;
input_row += start_row - self.transforms.start().0.row();
}
self.soft_wrapped = self.transforms.item().is_some_and(|t| !t.is_isomorphic());
self.input_buffer_rows.seek(input_row);
@@ -128,7 +120,7 @@ impl WrapMap {
tab_snapshot: TabSnapshot,
edits: Vec<TabEdit>,
cx: &mut Context<Self>,
) -> (WrapSnapshot, WrapPatch) {
) -> (WrapSnapshot, Patch<u32>) {
if self.wrap_width.is_some() {
self.pending_edits.push_back((tab_snapshot, edits));
self.flush_edits(cx);
@@ -234,8 +226,8 @@ impl WrapMap {
let new_rows = self.snapshot.transforms.summary().output.lines.row + 1;
self.snapshot.interpolated = false;
self.edits_since_sync = self.edits_since_sync.compose(Patch::new(vec![WrapEdit {
old: WrapRow(0)..WrapRow(old_rows),
new: WrapRow(0)..WrapRow(new_rows),
old: 0..old_rows,
new: 0..new_rows,
}]));
}
}
@@ -339,7 +331,7 @@ impl WrapSnapshot {
self.tab_snapshot.buffer_snapshot()
}
fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> WrapPatch {
fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch<u32> {
let mut new_transforms;
if tab_edits.is_empty() {
new_transforms = self.transforms.clone();
@@ -409,7 +401,7 @@ impl WrapSnapshot {
tab_edits: &[TabEdit],
wrap_width: Pixels,
line_wrapper: &mut LineWrapper,
) -> WrapPatch {
) -> Patch<u32> {
#[derive(Debug)]
struct RowEdit {
old_rows: Range<u32>,
@@ -562,7 +554,7 @@ impl WrapSnapshot {
old_snapshot.compute_edits(tab_edits, self)
}
fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &WrapSnapshot) -> WrapPatch {
fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &WrapSnapshot) -> Patch<u32> {
let mut wrap_edits = Vec::with_capacity(tab_edits.len());
let mut old_cursor = self.transforms.cursor::<TransformSummary>(());
let mut new_cursor = new_snapshot.transforms.cursor::<TransformSummary>(());
@@ -589,8 +581,8 @@ impl WrapSnapshot {
new_end += tab_edit.new.end.0 - new_cursor.start().input.lines;
wrap_edits.push(WrapEdit {
old: WrapRow(old_start.row)..WrapRow(old_end.row),
new: WrapRow(new_start.row)..WrapRow(new_end.row),
old: old_start.row..old_end.row,
new: new_start.row..new_end.row,
});
}
@@ -600,7 +592,7 @@ impl WrapSnapshot {
pub(crate) fn chunks<'a>(
&'a self,
rows: Range<WrapRow>,
rows: Range<u32>,
language_aware: bool,
highlights: Highlights<'a>,
) -> WrapChunks<'a> {
@@ -635,17 +627,17 @@ impl WrapSnapshot {
WrapPoint(self.transforms.summary().output.lines)
}
pub fn line_len(&self, row: WrapRow) -> u32 {
pub fn line_len(&self, row: u32) -> u32 {
let (start, _, item) = self.transforms.find::<Dimensions<WrapPoint, TabPoint>, _>(
(),
&WrapPoint::new(row + WrapRow(1), 0),
&WrapPoint::new(row + 1, 0),
Bias::Left,
);
if item.is_some_and(|transform| transform.is_isomorphic()) {
let overshoot = row - start.0.row();
let tab_row = start.1.row() + overshoot.0;
let tab_row = start.1.row() + overshoot;
let tab_line_len = self.tab_snapshot.line_len(tab_row);
if overshoot.0 == 0 {
if overshoot == 0 {
start.0.column() + (tab_line_len - start.1.column())
} else {
tab_line_len
@@ -655,7 +647,7 @@ impl WrapSnapshot {
}
}
pub fn text_summary_for_range(&self, rows: Range<WrapRow>) -> TextSummary {
pub fn text_summary_for_range(&self, rows: Range<u32>) -> TextSummary {
let mut summary = TextSummary::default();
let start = WrapPoint::new(rows.start, 0);
@@ -716,12 +708,10 @@ impl WrapSnapshot {
summary
}
pub fn soft_wrap_indent(&self, row: WrapRow) -> Option<u32> {
let (.., item) = self.transforms.find::<WrapPoint, _>(
(),
&WrapPoint::new(row + WrapRow(1), 0),
Bias::Right,
);
pub fn soft_wrap_indent(&self, row: u32) -> Option<u32> {
let (.., item) =
self.transforms
.find::<WrapPoint, _>((), &WrapPoint::new(row + 1, 0), Bias::Right);
item.and_then(|transform| {
if transform.is_isomorphic() {
None
@@ -735,14 +725,14 @@ impl WrapSnapshot {
self.transforms.summary().output.longest_row
}
pub fn row_infos(&self, start_row: WrapRow) -> WrapRows<'_> {
pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> {
let mut transforms = self
.transforms
.cursor::<Dimensions<WrapPoint, TabPoint>>(());
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left);
let mut input_row = transforms.start().1.row();
if transforms.item().is_some_and(|t| t.is_isomorphic()) {
input_row += (start_row - transforms.start().0.row()).0;
input_row += start_row - transforms.start().0.row();
}
let soft_wrapped = transforms.item().is_some_and(|t| !t.is_isomorphic());
let mut input_buffer_rows = self.tab_snapshot.rows(input_row);
@@ -797,9 +787,9 @@ impl WrapSnapshot {
self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
}
pub fn prev_row_boundary(&self, mut point: WrapPoint) -> WrapRow {
pub fn prev_row_boundary(&self, mut point: WrapPoint) -> u32 {
if self.transforms.is_empty() {
return WrapRow(0);
return 0;
}
*point.column_mut() = 0;
@@ -823,7 +813,7 @@ impl WrapSnapshot {
unreachable!()
}
pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<WrapRow> {
pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<u32> {
point.0 += Point::new(1, 0);
let mut cursor = self
@@ -843,13 +833,13 @@ impl WrapSnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
self.text_chunks(WrapRow(0)).collect()
self.text_chunks(0).collect()
}
#[cfg(test)]
pub fn text_chunks(&self, wrap_row: WrapRow) -> impl Iterator<Item = &str> {
pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
self.chunks(
wrap_row..self.max_point().row() + WrapRow(1),
wrap_row..self.max_point().row() + 1,
false,
Highlights::default(),
)
@@ -873,26 +863,25 @@ impl WrapSnapshot {
}
}
let text = language::Rope::from(self.text().as_str());
let text = language::Rope::from_str_small(self.text().as_str());
let mut input_buffer_rows = self.tab_snapshot.rows(0);
let mut expected_buffer_rows = Vec::new();
let mut prev_tab_row = 0;
for display_row in 0..=self.max_point().row().0 {
let display_row = WrapRow(display_row);
for display_row in 0..=self.max_point().row() {
let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
if tab_point.row() == prev_tab_row && display_row != WrapRow(0) {
if tab_point.row() == prev_tab_row && display_row != 0 {
expected_buffer_rows.push(None);
} else {
expected_buffer_rows.push(input_buffer_rows.next().unwrap().buffer_row);
}
prev_tab_row = tab_point.row();
assert_eq!(self.line_len(display_row), text.line_len(display_row.0));
assert_eq!(self.line_len(display_row), text.line_len(display_row));
}
for start_display_row in 0..expected_buffer_rows.len() {
assert_eq!(
self.row_infos(WrapRow(start_display_row as u32))
self.row_infos(start_display_row as u32)
.map(|row_info| row_info.buffer_row)
.collect::<Vec<_>>(),
&expected_buffer_rows[start_display_row..],
@@ -905,7 +894,7 @@ impl WrapSnapshot {
}
impl WrapChunks<'_> {
pub(crate) fn seek(&mut self, rows: Range<WrapRow>) {
pub(crate) fn seek(&mut self, rows: Range<u32>) {
let output_start = WrapPoint::new(rows.start, 0);
let output_end = WrapPoint::new(rows.end, 0);
self.transforms.seek(&output_start, Bias::Right);
@@ -942,7 +931,7 @@ impl<'a> Iterator for WrapChunks<'a> {
// Exclude newline starting prior to the desired row.
start_ix = 1;
summary.row = 0;
} else if self.output_position.row() + WrapRow(1) >= self.max_output_row {
} else if self.output_position.row() + 1 >= self.max_output_row {
// Exclude soft indentation ending after the desired row.
end_ix = 1;
summary.column = 0;
@@ -1008,7 +997,7 @@ impl Iterator for WrapRows<'_> {
let soft_wrapped = self.soft_wrapped;
let diff_status = self.input_buffer_row.diff_status;
self.output_row += WrapRow(1);
self.output_row += 1;
self.transforms
.seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left);
if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
@@ -1025,7 +1014,6 @@ impl Iterator for WrapRows<'_> {
multibuffer_row: None,
diff_status,
expand_info: None,
wrapped_buffer_row: buffer_row.buffer_row,
}
} else {
buffer_row
@@ -1119,12 +1107,12 @@ impl SumTreeExt for SumTree<Transform> {
}
impl WrapPoint {
pub fn new(row: WrapRow, column: u32) -> Self {
Self(Point::new(row.0, column))
pub fn new(row: u32, column: u32) -> Self {
Self(Point::new(row, column))
}
pub fn row(self) -> WrapRow {
WrapRow(self.0.row)
pub fn row(self) -> u32 {
self.0.row
}
pub fn row_mut(&mut self) -> &mut u32 {
@@ -1425,25 +1413,26 @@ mod tests {
}
}
let mut initial_text = Rope::from(initial_snapshot.text().as_str());
let mut initial_text =
Rope::from_str(initial_snapshot.text().as_str(), cx.background_executor());
for (snapshot, patch) in edits {
let snapshot_text = Rope::from(snapshot.text().as_str());
let snapshot_text = Rope::from_str(snapshot.text().as_str(), cx.background_executor());
for edit in &patch {
let old_start = initial_text.point_to_offset(Point::new(edit.new.start.0, 0));
let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
let old_end = initial_text.point_to_offset(cmp::min(
Point::new(edit.new.start.0 + (edit.old.end - edit.old.start).0, 0),
Point::new(edit.new.start + edit.old.len() as u32, 0),
initial_text.max_point(),
));
let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start.0, 0));
let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0));
let new_end = snapshot_text.point_to_offset(cmp::min(
Point::new(edit.new.end.0, 0),
Point::new(edit.new.end, 0),
snapshot_text.max_point(),
));
let new_text = snapshot_text
.chunks_in_range(new_start..new_end)
.collect::<String>();
initial_text.replace(old_start..old_end, &new_text);
initial_text.replace(old_start..old_end, &new_text, cx.background_executor());
}
assert_eq!(initial_text.to_string(), snapshot_text.to_string());
}
@@ -1496,11 +1485,11 @@ mod tests {
impl WrapSnapshot {
fn verify_chunks(&mut self, rng: &mut impl Rng) {
for _ in 0..5 {
let mut end_row = rng.random_range(0..=self.max_point().row().0);
let mut end_row = rng.random_range(0..=self.max_point().row());
let start_row = rng.random_range(0..=end_row);
end_row += 1;
let mut expected_text = self.text_chunks(WrapRow(start_row)).collect::<String>();
let mut expected_text = self.text_chunks(start_row).collect::<String>();
if expected_text.ends_with('\n') {
expected_text.push('\n');
}
@@ -1509,16 +1498,12 @@ mod tests {
.take((end_row - start_row) as usize)
.collect::<Vec<_>>()
.join("\n");
if end_row <= self.max_point().row().0 {
if end_row <= self.max_point().row() {
expected_text.push('\n');
}
let actual_text = self
.chunks(
WrapRow(start_row)..WrapRow(end_row),
true,
Highlights::default(),
)
.chunks(start_row..end_row, true, Highlights::default())
.map(|c| c.text)
.collect::<String>();
assert_eq!(

View File

@@ -1,13 +1,12 @@
use edit_prediction::EditPredictionProvider;
use gpui::{Entity, KeyBinding, Modifiers, prelude::*};
use gpui::{Entity, prelude::*};
use indoc::indoc;
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
use std::ops::Range;
use text::{Point, ToOffset};
use crate::{
AcceptEditPrediction, EditPrediction, MenuEditPredictionsPolicy, editor_tests::init_test,
test::editor_test_context::EditorTestContext,
EditPrediction, editor_tests::init_test, test::editor_test_context::EditorTestContext,
};
#[gpui::test]
@@ -271,63 +270,6 @@ async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui:
});
}
#[gpui::test]
async fn test_edit_prediction_preview_cleanup_on_toggle_off(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
// Bind `ctrl-shift-a` to accept the provided edit prediction. The actual key
// binding here doesn't matter, we simply need to confirm that holding the
// binding's modifiers triggers the edit prediction preview.
cx.update(|cx| cx.bind_keys([KeyBinding::new("ctrl-shift-a", AcceptEditPrediction, None)]));
let mut cx = EditorTestContext::new(cx).await;
let provider = cx.new(|_| FakeEditPredictionProvider::default());
assign_editor_completion_provider(provider.clone(), &mut cx);
cx.set_state("let x = ˇ;");
propose_edits(&provider, vec![(8..8, "42")], &mut cx);
cx.update_editor(|editor, window, cx| {
editor.set_menu_edit_predictions_policy(MenuEditPredictionsPolicy::ByProvider);
editor.update_visible_edit_prediction(window, cx)
});
cx.editor(|editor, _, _| {
assert!(editor.has_active_edit_prediction());
});
// Simulate pressing the modifiers for `AcceptEditPrediction`, namely
// `ctrl-shift`, so that we can confirm that the edit prediction preview is
// activated.
let modifiers = Modifiers::control_shift();
cx.simulate_modifiers_change(modifiers);
cx.run_until_parked();
cx.editor(|editor, _, _| {
assert!(editor.edit_prediction_preview_is_active());
});
// Disable showing edit predictions without issuing a new modifiers changed
// event, to confirm that the edit prediction preview is still active.
cx.update_editor(|editor, window, cx| {
editor.set_show_edit_predictions(Some(false), window, cx);
});
cx.editor(|editor, _, _| {
assert!(!editor.has_active_edit_prediction());
assert!(editor.edit_prediction_preview_is_active());
});
// Now release the modifiers
// Simulate releasing all modifiers, ensuring that even with edit prediction
// disabled, the edit prediction preview is cleaned up.
cx.simulate_modifiers_change(Modifiers::none());
cx.run_until_parked();
cx.editor(|editor, _, _| {
assert!(!editor.edit_prediction_preview_is_active());
});
}
fn assert_editor_active_edit_completion(
cx: &mut EditorTestContext,
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
@@ -453,7 +395,7 @@ impl EditPredictionProvider for FakeEditPredictionProvider {
}
fn show_completions_in_menu() -> bool {
true
false
}
fn supports_jump_to_edit() -> bool {

View File

@@ -163,10 +163,7 @@ use rpc::{ErrorCode, ErrorExt, proto::PeerId};
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
use selections_collection::{MutableSelectionsCollection, SelectionsCollection};
use serde::{Deserialize, Serialize};
use settings::{
GitGutterSetting, RelativeLineNumbers, Settings, SettingsLocation, SettingsStore,
update_settings_file,
};
use settings::{GitGutterSetting, Settings, SettingsLocation, SettingsStore, update_settings_file};
use smallvec::{SmallVec, smallvec};
use snippet::Snippet;
use std::{
@@ -455,20 +452,6 @@ pub enum SelectMode {
All,
}
#[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
pub enum SizingBehavior {
/// The editor will layout itself using `size_full` and will include the vertical
/// scroll margin as requested by user settings.
#[default]
Default,
/// The editor will layout itself using `size_full`, but will not have any
/// vertical overscroll.
ExcludeOverscrollMargin,
/// The editor will request a vertical size according to its content and will be
/// layouted without a vertical scroll margin.
SizeByContent,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum EditorMode {
SingleLine,
@@ -481,8 +464,8 @@ pub enum EditorMode {
scale_ui_elements_with_buffer_font_size: bool,
/// When set to `true`, the editor will render a background for the active line.
show_active_line_background: bool,
/// Determines the sizing behavior for this editor
sizing_behavior: SizingBehavior,
/// When set to `true`, the editor's height will be determined by its content.
sized_by_content: bool,
},
Minimap {
parent: WeakEntity<Editor>,
@@ -494,7 +477,7 @@ impl EditorMode {
Self::Full {
scale_ui_elements_with_buffer_font_size: true,
show_active_line_background: true,
sizing_behavior: SizingBehavior::Default,
sized_by_content: false,
}
}
@@ -1849,15 +1832,9 @@ impl Editor {
project::Event::RefreshCodeLens => {
// we always query lens with actions, without storing them, always refreshing them
}
project::Event::RefreshInlayHints {
server_id,
request_id,
} => {
project::Event::RefreshInlayHints(server_id) => {
editor.refresh_inlay_hints(
InlayHintRefreshReason::RefreshRequested {
server_id: *server_id,
request_id: *request_id,
},
InlayHintRefreshReason::RefreshRequested(*server_id),
cx,
);
}
@@ -2281,32 +2258,16 @@ impl Editor {
|editor, _, e: &EditorEvent, window, cx| match e {
EditorEvent::ScrollPositionChanged { local, .. } => {
if *local {
let new_anchor = editor.scroll_manager.anchor();
let snapshot = editor.snapshot(window, cx);
editor.update_restoration_data(cx, move |data| {
data.scroll_position = (
new_anchor.top_row(snapshot.buffer_snapshot()),
new_anchor.offset,
);
});
editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
editor.inline_blame_popover.take();
editor.post_scroll_update = cx.spawn_in(window, async move |editor, cx| {
cx.background_executor()
.timer(Duration::from_millis(50))
.await;
editor
.update_in(cx, |editor, window, cx| {
editor.register_visible_buffers(cx);
editor.refresh_colors_for_visible_range(None, window, cx);
editor.refresh_inlay_hints(
InlayHintRefreshReason::NewLinesShown,
cx,
);
let new_anchor = editor.scroll_manager.anchor();
let snapshot = editor.snapshot(window, cx);
editor.update_restoration_data(cx, move |data| {
data.scroll_position = (
new_anchor.top_row(snapshot.buffer_snapshot()),
new_anchor.offset,
);
});
})
.ok();
});
}
}
EditorEvent::Edited { .. } => {
@@ -2477,6 +2438,10 @@ impl Editor {
key_context.add("renaming");
}
if !self.snippet_stack.is_empty() {
key_context.add("in_snippet");
}
match self.context_menu.borrow().as_ref() {
Some(CodeContextMenu::Completions(menu)) => {
if menu.visible() {
@@ -7588,14 +7553,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
// Ensure that the edit prediction preview is updated, even when not
// enabled, if there's an active edit prediction preview.
if self.show_edit_predictions_in_menu()
|| matches!(
self.edit_prediction_preview,
EditPredictionPreview::Active { .. }
)
{
if self.show_edit_predictions_in_menu() {
self.update_edit_prediction_preview(&modifiers, window, cx);
}
@@ -7615,17 +7573,18 @@ impl Editor {
)
}
fn is_cmd_or_ctrl_pressed(modifiers: &Modifiers, cx: &mut Context<Self>) -> bool {
match EditorSettings::get_global(cx).multi_cursor_modifier {
MultiCursorModifier::Alt => modifiers.secondary(),
MultiCursorModifier::CmdOrCtrl => modifiers.alt,
}
}
fn is_alt_pressed(modifiers: &Modifiers, cx: &mut Context<Self>) -> bool {
match EditorSettings::get_global(cx).multi_cursor_modifier {
MultiCursorModifier::Alt => modifiers.alt,
MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
fn multi_cursor_modifier(invert: bool, modifiers: &Modifiers, cx: &mut Context<Self>) -> bool {
let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
if invert {
match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.alt,
MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
}
} else {
match multi_cursor_setting {
MultiCursorModifier::Alt => modifiers.secondary(),
MultiCursorModifier::CmdOrCtrl => modifiers.alt,
}
}
}
@@ -7634,9 +7593,9 @@ impl Editor {
cx: &mut Context<Self>,
) -> Option<ColumnarMode> {
if modifiers.shift && modifiers.number_of_modifiers() == 2 {
if Self::is_cmd_or_ctrl_pressed(modifiers, cx) {
if Self::multi_cursor_modifier(false, modifiers, cx) {
Some(ColumnarMode::FromMouse)
} else if Self::is_alt_pressed(modifiers, cx) {
} else if Self::multi_cursor_modifier(true, modifiers, cx) {
Some(ColumnarMode::FromSelection)
} else {
None
@@ -7893,7 +7852,7 @@ impl Editor {
let inlay = Inlay::edit_prediction(
post_inc(&mut self.next_inlay_id),
range.start,
new_text.as_str(),
Rope::from_str_small(new_text.as_str()),
);
inlay_ids.push(inlay.id);
inlays.push(inlay);
@@ -9991,6 +9950,38 @@ impl Editor {
self.outdent(&Outdent, window, cx);
}
pub fn next_snippet_tabstop(
&mut self,
_: &NextSnippetTabstop,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.mode.is_single_line() || self.snippet_stack.is_empty() {
return;
}
if self.move_to_next_snippet_tabstop(window, cx) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
return;
}
}
pub fn previous_snippet_tabstop(
&mut self,
_: &PreviousSnippetTabstop,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.mode.is_single_line() || self.snippet_stack.is_empty() {
return;
}
if self.move_to_prev_snippet_tabstop(window, cx) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
return;
}
}
pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context<Self>) {
if self.mode.is_single_line() {
cx.propagate();
@@ -16004,6 +15995,7 @@ impl Editor {
}
fn filtered<'a>(
snapshot: EditorSnapshot,
severity: GoToDiagnosticSeverityFilter,
diagnostics: impl Iterator<Item = DiagnosticEntryRef<'a, usize>>,
) -> impl Iterator<Item = DiagnosticEntryRef<'a, usize>> {
@@ -16011,15 +16003,19 @@ impl Editor {
.filter(move |entry| severity.matches(entry.diagnostic.severity))
.filter(|entry| entry.range.start != entry.range.end)
.filter(|entry| !entry.diagnostic.is_unnecessary)
.filter(move |entry| !snapshot.intersects_fold(entry.range.start))
}
let snapshot = self.snapshot(window, cx);
let before = filtered(
snapshot.clone(),
severity,
buffer
.diagnostics_in_range(0..selection.start)
.filter(|entry| entry.range.start <= selection.start),
);
let after = filtered(
snapshot,
severity,
buffer
.diagnostics_in_range(selection.start..buffer.len())
@@ -16058,15 +16054,6 @@ impl Editor {
let Some(buffer_id) = buffer.buffer_id_for_anchor(next_diagnostic_start) else {
return;
};
let snapshot = self.snapshot(window, cx);
if snapshot.intersects_fold(next_diagnostic.range.start) {
self.unfold_ranges(
std::slice::from_ref(&next_diagnostic.range),
true,
false,
cx,
);
}
self.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(vec![
next_diagnostic.range.start..next_diagnostic.range.start,
@@ -17840,7 +17827,6 @@ impl Editor {
.unwrap_or(self.diagnostics_max_severity);
if !self.inline_diagnostics_enabled()
|| !self.diagnostics_enabled()
|| !self.show_inline_diagnostics
|| max_severity == DiagnosticSeverity::Off
{
@@ -17919,7 +17905,7 @@ impl Editor {
window: &Window,
cx: &mut Context<Self>,
) -> Option<()> {
if self.ignore_lsp_data() || !self.diagnostics_enabled() {
if self.ignore_lsp_data() {
return None;
}
let pull_diagnostics_settings = ProjectSettings::get_global(cx)
@@ -19535,16 +19521,9 @@ impl Editor {
EditorSettings::get_global(cx).gutter.line_numbers
}
pub fn relative_line_numbers(&self, cx: &mut App) -> RelativeLineNumbers {
match (
self.use_relative_line_numbers,
EditorSettings::get_global(cx).relative_line_numbers,
) {
(None, setting) => setting,
(Some(false), _) => RelativeLineNumbers::Disabled,
(Some(true), RelativeLineNumbers::Wrapped) => RelativeLineNumbers::Wrapped,
(Some(true), _) => RelativeLineNumbers::Enabled,
}
pub fn should_use_relative_line_numbers(&self, cx: &mut App) -> bool {
self.use_relative_line_numbers
.unwrap_or(EditorSettings::get_global(cx).relative_line_numbers)
}
pub fn toggle_relative_line_numbers(
@@ -19553,8 +19532,8 @@ impl Editor {
_: &mut Window,
cx: &mut Context<Self>,
) {
let is_relative = self.relative_line_numbers(cx);
self.set_relative_line_number(Some(!is_relative.enabled()), cx)
let is_relative = self.should_use_relative_line_numbers(cx);
self.set_relative_line_number(Some(!is_relative), cx)
}
pub fn set_relative_line_number(&mut self, is_relative: Option<bool>, cx: &mut Context<Self>) {
@@ -24173,10 +24152,6 @@ impl EntityInputHandler for Editor {
let utf16_offset = anchor.to_offset_utf16(&position_map.snapshot.buffer_snapshot());
Some(utf16_offset.0)
}
fn accepts_text_input(&self, _window: &mut Window, _cx: &mut Context<Self>) -> bool {
self.input_enabled
}
}
trait SelectionExt {

View File

@@ -3,12 +3,12 @@ use core::num;
use gpui::App;
use language::CursorShape;
use project::project_settings::DiagnosticSeverity;
use settings::Settings;
pub use settings::{
CurrentLineHighlight, DelayMs, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer,
GoToDefinitionFallback, HideMouseMode, MinimapThumb, MinimapThumbBorder, MultiCursorModifier,
ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder,
};
use settings::{RelativeLineNumbers, Settings};
use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar};
/// Imports from the VSCode settings at
@@ -33,7 +33,7 @@ pub struct EditorSettings {
pub horizontal_scroll_margin: f32,
pub scroll_sensitivity: f32,
pub fast_scroll_sensitivity: f32,
pub relative_line_numbers: RelativeLineNumbers,
pub relative_line_numbers: bool,
pub seed_search_query_from_cursor: SeedQuerySetting,
pub use_smartcase_search: bool,
pub multi_cursor_modifier: MultiCursorModifier,
@@ -55,7 +55,6 @@ pub struct EditorSettings {
pub drag_and_drop_selection: DragAndDropSelection,
pub lsp_document_colors: DocumentColorsRenderMode,
pub minimum_contrast_for_highlights: f32,
pub completion_menu_scrollbar: ShowScrollbar,
}
#[derive(Debug, Clone)]
pub struct Jupyter {
@@ -160,7 +159,6 @@ pub struct SearchSettings {
pub case_sensitive: bool,
pub include_ignored: bool,
pub regex: bool,
pub center_on_match: bool,
}
impl EditorSettings {
@@ -251,7 +249,6 @@ impl Settings for EditorSettings {
case_sensitive: search.case_sensitive.unwrap(),
include_ignored: search.include_ignored.unwrap(),
regex: search.regex.unwrap(),
center_on_match: search.center_on_match.unwrap(),
},
auto_signature_help: editor.auto_signature_help.unwrap(),
show_signature_help_after_edits: editor.show_signature_help_after_edits.unwrap(),
@@ -269,7 +266,6 @@ impl Settings for EditorSettings {
},
lsp_document_colors: editor.lsp_document_colors.unwrap(),
minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap().0,
completion_menu_scrollbar: editor.completion_menu_scrollbar.map(Into::into).unwrap(),
}
}
}

View File

@@ -3136,77 +3136,6 @@ fn test_newline(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_newline_yaml(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
// Object (between 2 fields)
cx.set_state(indoc! {"
test:ˇ
hello: bye"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
ˇ
hello: bye"});
// Object (first and single line)
cx.set_state(indoc! {"
test:ˇ"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
ˇ"});
// Array with objects (after first element)
cx.set_state(indoc! {"
test:
- foo: barˇ"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
- foo: bar
ˇ"});
// Array with objects and comment
cx.set_state(indoc! {"
test:
- foo: bar
- bar: # testˇ"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
- foo: bar
- bar: # test
ˇ"});
// Array with objects (after second element)
cx.set_state(indoc! {"
test:
- foo: bar
- bar: fooˇ"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
- foo: bar
- bar: foo
ˇ"});
// Array with strings (after first element)
cx.set_state(indoc! {"
test:
- fooˇ"});
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
cx.assert_editor_state(indoc! {"
test:
- foo
ˇ"});
}
#[gpui::test]
fn test_newline_with_old_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -11137,6 +11066,129 @@ async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
init_test(cx, |_| {});
fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
assert_eq!(editor.text(cx), expected_text);
assert_eq!(
editor
.selections
.ranges::<usize>(&editor.display_snapshot(cx)),
selection_ranges
);
}
let (text, insertion_ranges) = marked_text_ranges(
indoc! {"
ˇ
"},
false,
);
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
_ = editor.update_in(cx, |editor, window, cx| {
let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
editor
.insert_snippet(&insertion_ranges, snippet, window, cx)
.unwrap();
assert_state(
editor,
cx,
indoc! {"
type «» = ;•
"},
);
assert!(
editor.context_menu_visible(),
"Context menu should be visible for placeholder choices"
);
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
assert_state(
editor,
cx,
indoc! {"
type = «»;•
"},
);
assert!(
!editor.context_menu_visible(),
"Context menu should be hidden after moving to next tabstop"
);
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
assert_state(
editor,
cx,
indoc! {"
type = ; ˇ
"},
);
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
assert_state(
editor,
cx,
indoc! {"
type = ; ˇ
"},
);
});
_ = editor.update_in(cx, |editor, window, cx| {
editor.select_all(&SelectAll, window, cx);
editor.backspace(&Backspace, window, cx);
let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
let insertion_ranges = editor
.selections
.all(&editor.display_snapshot(cx))
.iter()
.map(|s| s.range())
.collect::<Vec<_>>();
editor
.insert_snippet(&insertion_ranges, snippet, window, cx)
.unwrap();
assert_state(editor, cx, "fn «» = value;•");
assert!(
editor.context_menu_visible(),
"Context menu should be visible for placeholder choices"
);
editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
assert_state(editor, cx, "fn = «valueˇ»;•");
editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
assert_state(editor, cx, "fn «» = value;•");
assert!(
editor.context_menu_visible(),
"Context menu should be visible again after returning to first tabstop"
);
editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
assert_state(editor, cx, "fn «» = value;•");
});
}
#[gpui::test]
async fn test_snippets(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -14165,7 +14217,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sizing_behavior: SizingBehavior::Default,
sized_by_content: false,
},
multi_buffer.clone(),
Some(project.clone()),

View File

@@ -8,8 +8,8 @@ use crate::{
HandleInput, HoveredCursor, InlayHintRefreshReason, JumpData, LineDown, LineHighlight, LineUp,
MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown,
PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
SelectedTextHighlight, Selection, SelectionDragState, SizingBehavior, SoftWrap,
StickyHeaderExcerpt, ToPoint, ToggleFold, ToggleFoldAll,
SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint,
ToggleFold, ToggleFoldAll,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
@@ -232,6 +232,8 @@ impl EditorElement {
register_action(editor, window, Editor::blame_hover);
register_action(editor, window, Editor::delete);
register_action(editor, window, Editor::tab);
register_action(editor, window, Editor::next_snippet_tabstop);
register_action(editor, window, Editor::previous_snippet_tabstop);
register_action(editor, window, Editor::backtab);
register_action(editor, window, Editor::indent);
register_action(editor, window, Editor::outdent);
@@ -764,14 +766,8 @@ impl EditorElement {
.row;
if line_numbers
.get(&MultiBufferRow(multi_buffer_row))
.is_some_and(|line_layout| {
line_layout.segments.iter().any(|segment| {
segment
.hitbox
.as_ref()
.is_some_and(|hitbox| hitbox.contains(&event.position))
})
})
.and_then(|line_number| line_number.hitbox.as_ref())
.is_some_and(|hitbox| hitbox.contains(&event.position))
{
let line_offset_from_top = display_row - position_map.scroll_position.y as u32;
@@ -818,7 +814,7 @@ impl EditorElement {
editor.select(
SelectPhase::Begin {
position,
add: Editor::is_alt_pressed(&modifiers, cx),
add: Editor::multi_cursor_modifier(true, &modifiers, cx),
click_count,
},
window,
@@ -1002,7 +998,7 @@ impl EditorElement {
let text_hitbox = &position_map.text_hitbox;
let pending_nonempty_selections = editor.has_pending_nonempty_selection();
let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&event.modifiers(), cx);
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &event.modifiers(), cx);
if let Some(mouse_position) = event.mouse_position()
&& !pending_nonempty_selections
@@ -1314,14 +1310,7 @@ impl EditorElement {
hover_at(editor, Some(anchor), window, cx);
Self::update_visible_cursor(editor, point, position_map, window, cx);
} else {
editor.update_inlay_link_and_hover_points(
&position_map.snapshot,
point_for_position,
modifiers.secondary(),
modifiers.shift,
window,
cx,
);
hover_at(editor, None, window, cx);
}
} else {
editor.hide_hovered_link(cx);
@@ -3161,7 +3150,6 @@ impl EditorElement {
snapshot: &EditorSnapshot,
rows: &Range<DisplayRow>,
relative_to: Option<DisplayRow>,
count_wrapped_lines: bool,
) -> HashMap<DisplayRow, DisplayRowDelta> {
let mut relative_rows: HashMap<DisplayRow, DisplayRowDelta> = Default::default();
let Some(relative_to) = relative_to else {
@@ -3179,15 +3167,8 @@ impl EditorElement {
let head_idx = relative_to.minus(start);
let mut delta = 1;
let mut i = head_idx + 1;
let should_count_line = |row_info: &RowInfo| {
if count_wrapped_lines {
row_info.buffer_row.is_some() || row_info.wrapped_buffer_row.is_some()
} else {
row_info.buffer_row.is_some()
}
};
while i < buffer_rows.len() as u32 {
if should_count_line(&buffer_rows[i as usize]) {
if buffer_rows[i as usize].buffer_row.is_some() {
if rows.contains(&DisplayRow(i + start.0)) {
relative_rows.insert(DisplayRow(i + start.0), delta);
}
@@ -3197,13 +3178,13 @@ impl EditorElement {
}
delta = 1;
i = head_idx.min(buffer_rows.len().saturating_sub(1) as u32);
while i > 0 && buffer_rows[i as usize].buffer_row.is_none() && !count_wrapped_lines {
while i > 0 && buffer_rows[i as usize].buffer_row.is_none() {
i -= 1;
}
while i > 0 {
i -= 1;
if should_count_line(&buffer_rows[i as usize]) {
if buffer_rows[i as usize].buffer_row.is_some() {
if rows.contains(&DisplayRow(i + start.0)) {
relative_rows.insert(DisplayRow(i + start.0), delta);
}
@@ -3235,7 +3216,7 @@ impl EditorElement {
return Arc::default();
}
let (newest_selection_head, relative) = self.editor.update(cx, |editor, cx| {
let (newest_selection_head, is_relative) = self.editor.update(cx, |editor, cx| {
let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
let newest = editor
.selections
@@ -3251,93 +3232,79 @@ impl EditorElement {
)
.head
});
let relative = editor.relative_line_numbers(cx);
(newest_selection_head, relative)
let is_relative = editor.should_use_relative_line_numbers(cx);
(newest_selection_head, is_relative)
});
let relative_to = if relative.enabled() {
let relative_to = if is_relative {
Some(newest_selection_head.row())
} else {
None
};
let relative_rows =
self.calculate_relative_line_numbers(snapshot, &rows, relative_to, relative.wrapped());
let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
let mut line_number = String::new();
let segments = buffer_rows.iter().enumerate().flat_map(|(ix, row_info)| {
let display_row = DisplayRow(rows.start.0 + ix as u32);
line_number.clear();
let non_relative_number = if relative.wrapped() {
row_info.buffer_row.or(row_info.wrapped_buffer_row)? + 1
} else {
row_info.buffer_row? + 1
};
let number = relative_rows
.get(&display_row)
.unwrap_or(&non_relative_number);
write!(&mut line_number, "{number}").unwrap();
if row_info
.diff_status
.is_some_and(|status| status.is_deleted())
{
return None;
}
let line_numbers = buffer_rows
.iter()
.enumerate()
.flat_map(|(ix, row_info)| {
let display_row = DisplayRow(rows.start.0 + ix as u32);
line_number.clear();
let non_relative_number = row_info.buffer_row? + 1;
let number = relative_rows
.get(&display_row)
.unwrap_or(&non_relative_number);
write!(&mut line_number, "{number}").unwrap();
if row_info
.diff_status
.is_some_and(|status| status.is_deleted())
{
return None;
}
let color = active_rows
.get(&display_row)
.map(|spec| {
if spec.breakpoint {
cx.theme().colors().debugger_accent
} else {
cx.theme().colors().editor_active_line_number
}
})
.unwrap_or_else(|| cx.theme().colors().editor_line_number);
let shaped_line =
self.shape_line_number(SharedString::from(&line_number), color, window);
let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
let line_origin = gutter_hitbox.map(|hitbox| {
hitbox.origin
+ point(
hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding,
ix as f32 * line_height
- Pixels::from(scroll_top % ScrollPixelOffset::from(line_height)),
let color = active_rows
.get(&display_row)
.map(|spec| {
if spec.breakpoint {
cx.theme().colors().debugger_accent
} else {
cx.theme().colors().editor_active_line_number
}
})
.unwrap_or_else(|| cx.theme().colors().editor_line_number);
let shaped_line =
self.shape_line_number(SharedString::from(&line_number), color, window);
let scroll_top = scroll_position.y * ScrollPixelOffset::from(line_height);
let line_origin = gutter_hitbox.map(|hitbox| {
hitbox.origin
+ point(
hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding,
ix as f32 * line_height
- Pixels::from(scroll_top % ScrollPixelOffset::from(line_height)),
)
});
#[cfg(not(test))]
let hitbox = line_origin.map(|line_origin| {
window.insert_hitbox(
Bounds::new(line_origin, size(shaped_line.width, line_height)),
HitboxBehavior::Normal,
)
});
});
#[cfg(test)]
let hitbox = {
let _ = line_origin;
None
};
#[cfg(not(test))]
let hitbox = line_origin.map(|line_origin| {
window.insert_hitbox(
Bounds::new(line_origin, size(shaped_line.width, line_height)),
HitboxBehavior::Normal,
)
});
#[cfg(test)]
let hitbox = {
let _ = line_origin;
None
};
let segment = LineNumberSegment {
shaped_line,
hitbox,
};
let buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row;
let multi_buffer_row = MultiBufferRow(buffer_row);
Some((multi_buffer_row, segment))
});
let mut line_numbers: HashMap<MultiBufferRow, LineNumberLayout> = HashMap::default();
for (buffer_row, segment) in segments {
line_numbers
.entry(buffer_row)
.or_insert_with(|| LineNumberLayout {
segments: Default::default(),
})
.segments
.push(segment);
}
let multi_buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row;
let multi_buffer_row = MultiBufferRow(multi_buffer_row);
let line_number = LineNumberLayout {
shaped_line,
hitbox,
};
Some((multi_buffer_row, line_number))
})
.collect();
Arc::new(line_numbers)
}
@@ -4004,51 +3971,41 @@ impl EditorElement {
.size_full()
.justify_between()
.overflow_hidden()
.child(h_flex().gap_2().map(|path_header| {
let filename = filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into());
.child(
h_flex()
.gap_2()
.map(|path_header| {
let filename = filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into());
path_header
.when(ItemSettings::get_global(cx).file_icons, |el| {
let path = path::Path::new(filename.as_str());
let icon =
FileIcons::get_icon(path, cx).unwrap_or_default();
let icon = Icon::from_path(icon).color(Color::Muted);
el.child(icon)
path_header
.when(ItemSettings::get_global(cx).file_icons, |el| {
let path = path::Path::new(filename.as_str());
let icon = FileIcons::get_icon(path, cx)
.unwrap_or_default();
let icon =
Icon::from_path(icon).color(Color::Muted);
el.child(icon)
})
.child(Label::new(filename).single_line().when_some(
file_status,
|el, status| {
el.color(if status.is_conflicted() {
Color::Conflict
} else if status.is_modified() {
Color::Modified
} else if status.is_deleted() {
Color::Disabled
} else {
Color::Created
})
.when(status.is_deleted(), |el| {
el.strikethrough()
})
},
))
})
.child(
ButtonLike::new("filename-button")
.style(ButtonStyle::Subtle)
.child(
div()
.child(
Label::new(filename)
.single_line()
.color(file_status_label_color(
file_status,
))
.when(
file_status.is_some_and(|s| {
s.is_deleted()
}),
|label| label.strikethrough(),
),
)
.group_hover("", |div| div.underline()),
)
.on_click(window.listener_for(&self.editor, {
let jump_data = jump_data.clone();
move |editor, e: &ClickEvent, window, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
}
})),
)
.when_some(parent_path, |then, path| {
then.child(div().child(path).text_color(
if file_status.is_some_and(FileStatus::is_deleted) {
@@ -4057,47 +4014,33 @@ impl EditorElement {
colors.text_muted
},
))
})
}))
}),
)
.when(
can_open_excerpts && is_selected && relative_path.is_some(),
|el| {
el.child(
ButtonLike::new("open-file-button")
.style(ButtonStyle::OutlinedGhost)
.child(
h_flex()
.gap_2p5()
.child(Label::new("Open file"))
.child(KeyBinding::for_action_in(
&OpenExcerpts,
&focus_handle,
cx,
)),
)
.on_click(window.listener_for(&self.editor, {
let jump_data = jump_data.clone();
move |editor, e: &ClickEvent, window, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
}
})),
h_flex()
.id("jump-to-file-button")
.gap_2p5()
.child(Label::new("Jump To File"))
.child(KeyBinding::for_action_in(
&OpenExcerpts,
&focus_handle,
cx,
)),
)
},
)
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_click(window.listener_for(&self.editor, {
let buffer_id = for_excerpt.buffer_id;
move |editor, _e: &ClickEvent, _window, cx| {
if is_folded {
editor.unfold_buffer(buffer_id, cx);
} else {
editor.fold_buffer(buffer_id, cx);
}
move |editor, e: &ClickEvent, window, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.modifiers().secondary(),
window,
cx,
);
}
})),
),
@@ -5902,36 +5845,34 @@ impl EditorElement {
let line_height = layout.position_map.line_height;
window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
for line_layout in layout.line_numbers.values() {
for LineNumberSegment {
shaped_line,
hitbox,
} in &line_layout.segments
{
let Some(hitbox) = hitbox else {
continue;
};
for LineNumberLayout {
shaped_line,
hitbox,
} in layout.line_numbers.values()
{
let Some(hitbox) = hitbox else {
continue;
};
let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
let color = cx.theme().colors().editor_hover_line_number;
let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
let color = cx.theme().colors().editor_hover_line_number;
let line = self.shape_line_number(shaped_line.text.clone(), color, window);
line.paint(hitbox.origin, line_height, window, cx).log_err()
} else {
shaped_line
.paint(hitbox.origin, line_height, window, cx)
.log_err()
}) else {
continue;
};
let line = self.shape_line_number(shaped_line.text.clone(), color, window);
line.paint(hitbox.origin, line_height, window, cx).log_err()
} else {
shaped_line
.paint(hitbox.origin, line_height, window, cx)
.log_err()
}) else {
continue;
};
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
if is_singleton {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
} else {
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
if is_singleton {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
} else {
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
}
}
@@ -7536,22 +7477,6 @@ impl EditorElement {
}
}
fn file_status_label_color(file_status: Option<FileStatus>) -> Color {
file_status.map_or(Color::Default, |status| {
if status.is_conflicted() {
Color::Conflict
} else if status.is_modified() {
Color::Modified
} else if status.is_deleted() {
Color::Disabled
} else if status.is_created() {
Color::Created
} else {
Color::Default
}
})
}
fn header_jump_data(
snapshot: &EditorSnapshot,
block_row_start: DisplayRow,
@@ -8516,11 +8441,11 @@ impl Element for EditorElement {
window.request_layout(style, None, cx)
}
EditorMode::Full {
sizing_behavior, ..
sized_by_content, ..
} => {
let mut style = Style::default();
style.size.width = relative(1.).into();
if sizing_behavior == SizingBehavior::SizeByContent {
if sized_by_content {
let snapshot = editor.snapshot(window, cx);
let line_height =
self.style.text.line_height_in_pixels(window.rem_size());
@@ -8684,8 +8609,7 @@ impl Element for EditorElement {
EditorMode::SingleLine
| EditorMode::AutoHeight { .. }
| EditorMode::Full {
sizing_behavior: SizingBehavior::ExcludeOverscrollMargin
| SizingBehavior::SizeByContent,
sized_by_content: true,
..
}
) {
@@ -9861,17 +9785,11 @@ impl EditorLayout {
}
}
#[derive(Debug)]
struct LineNumberSegment {
struct LineNumberLayout {
shaped_line: ShapedLine,
hitbox: Option<Hitbox>,
}
#[derive(Debug)]
struct LineNumberLayout {
segments: SmallVec<[LineNumberSegment; 1]>,
}
struct ColoredRange<T> {
start: T,
end: T,
@@ -10935,7 +10853,6 @@ mod tests {
&snapshot,
&(DisplayRow(0)..DisplayRow(6)),
Some(DisplayRow(3)),
false,
)
})
.unwrap();
@@ -10954,7 +10871,6 @@ mod tests {
&snapshot,
&(DisplayRow(3)..DisplayRow(6)),
Some(DisplayRow(1)),
false,
)
})
.unwrap();
@@ -10971,7 +10887,6 @@ mod tests {
&snapshot,
&(DisplayRow(0)..DisplayRow(3)),
Some(DisplayRow(6)),
false,
)
})
.unwrap();
@@ -10981,81 +10896,6 @@ mod tests {
assert_eq!(relative_rows[&DisplayRow(2)], 3);
}
#[gpui::test]
fn test_shape_line_numbers_wrapping(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let window = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
Editor::new(EditorMode::full(), buffer, None, window, cx)
});
update_test_language_settings(cx, |s| {
s.defaults.preferred_line_length = Some(5_u32);
s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
});
let editor = window.root(cx).unwrap();
let style = cx.update(|cx| editor.read(cx).style().unwrap().clone());
let line_height = window
.update(cx, |_, window, _| {
style.text.line_height_in_pixels(window.rem_size())
})
.unwrap();
let element = EditorElement::new(&editor, style);
let snapshot = window
.update(cx, |editor, window, cx| editor.snapshot(window, cx))
.unwrap();
let layouts = cx
.update_window(*window, |_, window, cx| {
element.layout_line_numbers(
None,
GutterDimensions {
left_padding: Pixels::ZERO,
right_padding: Pixels::ZERO,
width: px(30.0),
margin: Pixels::ZERO,
git_blame_entries_width: None,
},
line_height,
gpui::Point::default(),
DisplayRow(0)..DisplayRow(6),
&(0..6)
.map(|row| RowInfo {
buffer_row: Some(row),
..Default::default()
})
.collect::<Vec<_>>(),
&BTreeMap::default(),
Some(DisplayPoint::new(DisplayRow(0), 0)),
&snapshot,
window,
cx,
)
})
.unwrap();
assert_eq!(layouts.len(), 3);
let relative_rows = window
.update(cx, |editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
element.calculate_relative_line_numbers(
&snapshot,
&(DisplayRow(0)..DisplayRow(6)),
Some(DisplayRow(3)),
true,
)
})
.unwrap();
assert_eq!(relative_rows[&DisplayRow(0)], 3);
assert_eq!(relative_rows[&DisplayRow(1)], 2);
assert_eq!(relative_rows[&DisplayRow(2)], 1);
// current line has no relative number
assert_eq!(relative_rows[&DisplayRow(4)], 1);
assert_eq!(relative_rows[&DisplayRow(5)], 2);
}
#[gpui::test]
async fn test_vim_visual_selections(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -11169,13 +11009,7 @@ mod tests {
state
.line_numbers
.get(&MultiBufferRow(0))
.map(|line_number| line_number
.segments
.first()
.unwrap()
.shaped_line
.text
.as_ref()),
.map(|line_number| line_number.shaped_line.text.as_ref()),
Some("1")
);
}

View File

@@ -1115,18 +1115,19 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let buffer_initial_text_len = rng.random_range(5..15);
let mut buffer_initial_text = Rope::from(
let mut buffer_initial_text = Rope::from_str(
RandomCharIter::new(&mut rng)
.take(buffer_initial_text_len)
.collect::<String>()
.as_str(),
cx.background_executor(),
);
let mut newline_ixs = (0..buffer_initial_text_len).choose_multiple(&mut rng, 5);
newline_ixs.sort_unstable();
for newline_ix in newline_ixs.into_iter().rev() {
let newline_ix = buffer_initial_text.clip_offset(newline_ix, Bias::Right);
buffer_initial_text.replace(newline_ix..newline_ix, "\n");
buffer_initial_text.replace(newline_ix..newline_ix, "\n", cx.background_executor());
}
log::info!("initial buffer text: {:?}", buffer_initial_text);

View File

@@ -116,7 +116,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&modifiers, cx);
let hovered_link_modifier = Editor::multi_cursor_modifier(false, &modifiers, cx);
if !hovered_link_modifier || self.has_pending_selection() {
self.hide_hovered_link(cx);
return;
@@ -241,8 +241,8 @@ impl Editor {
}
})
.collect();
let split = Self::is_alt_pressed(&modifiers, cx);
let navigate_task = self.navigate_to_hover_links(None, links, split, window, cx);
let navigate_task =
self.navigate_to_hover_links(None, links, modifiers.alt, window, cx);
self.select(SelectPhase::End, window, cx);
return navigate_task;
}
@@ -261,8 +261,7 @@ impl Editor {
);
let navigate_task = if point.as_valid().is_some() {
let split = Self::is_alt_pressed(&modifiers, cx);
match (modifiers.shift, split) {
match (modifiers.shift, modifiers.alt) {
(true, true) => {
self.go_to_type_definition_split(&GoToTypeDefinitionSplit, window, cx)
}

View File

@@ -59,10 +59,10 @@ impl Inlay {
pub fn hint(id: InlayId, position: Anchor, hint: &InlayHint) -> Self {
let mut text = hint.text();
if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') {
text.push(" ");
text.push_small(" ");
}
if hint.padding_left && text.chars_at(0).next() != Some(' ') {
text.push_front(" ");
text.push_front_small(" ");
}
Self {
id,
@@ -72,11 +72,11 @@ impl Inlay {
}
#[cfg(any(test, feature = "test-support"))]
pub fn mock_hint(id: usize, position: Anchor, text: impl Into<Rope>) -> Self {
pub fn mock_hint(id: usize, position: Anchor, text: Rope) -> Self {
Self {
id: InlayId::Hint(id),
position,
content: InlayContent::Text(text.into()),
content: InlayContent::Text(text),
}
}
@@ -88,19 +88,19 @@ impl Inlay {
}
}
pub fn edit_prediction<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
pub fn edit_prediction(id: usize, position: Anchor, text: Rope) -> Self {
Self {
id: InlayId::EditPrediction(id),
position,
content: InlayContent::Text(text.into()),
content: InlayContent::Text(text),
}
}
pub fn debugger<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
pub fn debugger(id: usize, position: Anchor, text: Rope) -> Self {
Self {
id: InlayId::DebuggerValue(id),
position,
content: InlayContent::Text(text.into()),
content: InlayContent::Text(text),
}
}
@@ -108,7 +108,7 @@ impl Inlay {
static COLOR_TEXT: OnceLock<Rope> = OnceLock::new();
match &self.content {
InlayContent::Text(text) => text,
InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from("")),
InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from_str_small("")),
}
}

View File

@@ -1,4 +1,5 @@
use std::{
collections::hash_map,
ops::{ControlFlow, Range},
time::Duration,
};
@@ -48,8 +49,8 @@ pub struct LspInlayHintData {
allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
invalidate_debounce: Option<Duration>,
append_debounce: Option<Duration>,
hint_refresh_tasks: HashMap<BufferId, Vec<Task<()>>>,
hint_chunk_fetching: HashMap<BufferId, (Global, HashSet<Range<BufferRow>>)>,
hint_refresh_tasks: HashMap<BufferId, HashMap<Vec<Range<BufferRow>>, Vec<Task<()>>>>,
hint_chunk_fetched: HashMap<BufferId, (Global, HashSet<Range<BufferRow>>)>,
invalidate_hints_for_buffers: HashSet<BufferId>,
pub added_hints: HashMap<InlayId, Option<InlayHintKind>>,
}
@@ -62,7 +63,7 @@ impl LspInlayHintData {
enabled_in_settings: settings.enabled,
hint_refresh_tasks: HashMap::default(),
added_hints: HashMap::default(),
hint_chunk_fetching: HashMap::default(),
hint_chunk_fetched: HashMap::default(),
invalidate_hints_for_buffers: HashSet::default(),
invalidate_debounce: debounce_value(settings.edit_debounce_ms),
append_debounce: debounce_value(settings.scroll_debounce_ms),
@@ -98,8 +99,9 @@ impl LspInlayHintData {
pub fn clear(&mut self) {
self.hint_refresh_tasks.clear();
self.hint_chunk_fetching.clear();
self.hint_chunk_fetched.clear();
self.added_hints.clear();
self.invalidate_hints_for_buffers.clear();
}
/// Checks inlay hint settings for enabled hint kinds and general enabled state.
@@ -197,7 +199,7 @@ impl LspInlayHintData {
) {
for buffer_id in removed_buffer_ids {
self.hint_refresh_tasks.remove(buffer_id);
self.hint_chunk_fetching.remove(buffer_id);
self.hint_chunk_fetched.remove(buffer_id);
}
}
}
@@ -209,10 +211,7 @@ pub enum InlayHintRefreshReason {
SettingsChange(InlayHintSettings),
NewLinesShown,
BufferEdited(BufferId),
RefreshRequested {
server_id: LanguageServerId,
request_id: Option<usize>,
},
RefreshRequested(LanguageServerId),
ExcerptsRemoved(Vec<ExcerptId>),
}
@@ -297,7 +296,7 @@ impl Editor {
| InlayHintRefreshReason::Toggle(_)
| InlayHintRefreshReason::SettingsChange(_) => true,
InlayHintRefreshReason::NewLinesShown
| InlayHintRefreshReason::RefreshRequested { .. }
| InlayHintRefreshReason::RefreshRequested(_)
| InlayHintRefreshReason::ExcerptsRemoved(_) => false,
InlayHintRefreshReason::BufferEdited(buffer_id) => {
let Some(affected_language) = self
@@ -371,45 +370,48 @@ impl Editor {
let Some(buffer) = multi_buffer.read(cx).buffer(buffer_id) else {
continue;
};
let (fetched_for_version, fetched_chunks) = inlay_hints
.hint_chunk_fetching
.entry(buffer_id)
.or_default();
let fetched_tasks = inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default();
if visible_excerpts
.buffer_version
.changed_since(fetched_for_version)
.changed_since(&fetched_tasks.0)
{
*fetched_for_version = visible_excerpts.buffer_version.clone();
fetched_chunks.clear();
fetched_tasks.1.clear();
fetched_tasks.0 = visible_excerpts.buffer_version.clone();
inlay_hints.hint_refresh_tasks.remove(&buffer_id);
}
let known_chunks = if ignore_previous_fetches {
None
} else {
Some((fetched_for_version.clone(), fetched_chunks.clone()))
};
let mut applicable_chunks =
let applicable_chunks =
semantics_provider.applicable_inlay_chunks(&buffer, &visible_excerpts.ranges, cx);
applicable_chunks.retain(|chunk| fetched_chunks.insert(chunk.clone()));
if applicable_chunks.is_empty() && !ignore_previous_fetches {
continue;
}
inlay_hints
match inlay_hints
.hint_refresh_tasks
.entry(buffer_id)
.or_default()
.push(spawn_editor_hints_refresh(
buffer_id,
invalidate_cache,
debounce,
visible_excerpts,
known_chunks,
applicable_chunks,
cx,
));
.entry(applicable_chunks)
{
hash_map::Entry::Occupied(mut o) => {
if invalidate_cache.should_invalidate() || ignore_previous_fetches {
o.get_mut().push(spawn_editor_hints_refresh(
buffer_id,
invalidate_cache,
ignore_previous_fetches,
debounce,
visible_excerpts,
cx,
));
}
}
hash_map::Entry::Vacant(v) => {
v.insert(Vec::new()).push(spawn_editor_hints_refresh(
buffer_id,
invalidate_cache,
ignore_previous_fetches,
debounce,
visible_excerpts,
cx,
));
}
}
}
}
@@ -504,13 +506,9 @@ impl Editor {
}
InlayHintRefreshReason::NewLinesShown => InvalidationStrategy::None,
InlayHintRefreshReason::BufferEdited(_) => InvalidationStrategy::BufferEdited,
InlayHintRefreshReason::RefreshRequested {
server_id,
request_id,
} => InvalidationStrategy::RefreshRequested {
server_id: *server_id,
request_id: *request_id,
},
InlayHintRefreshReason::RefreshRequested(server_id) => {
InvalidationStrategy::RefreshRequested(*server_id)
}
};
match &mut self.inlay_hints {
@@ -720,29 +718,44 @@ impl Editor {
fn inlay_hints_for_buffer(
&mut self,
invalidate_cache: InvalidationStrategy,
ignore_previous_fetches: bool,
buffer_excerpts: VisibleExcerpts,
known_chunks: Option<(Global, HashSet<Range<BufferRow>>)>,
cx: &mut Context<Self>,
) -> Option<Vec<Task<(Range<BufferRow>, anyhow::Result<CacheInlayHints>)>>> {
let semantics_provider = self.semantics_provider()?;
let inlay_hints = self.inlay_hints.as_mut()?;
let buffer_id = buffer_excerpts.buffer.read(cx).remote_id();
let new_hint_tasks = semantics_provider
.inlay_hints(
invalidate_cache,
buffer_excerpts.buffer,
buffer_excerpts.ranges,
known_chunks,
inlay_hints
.hint_chunk_fetched
.get(&buffer_id)
.filter(|_| !ignore_previous_fetches && !invalidate_cache.should_invalidate())
.cloned(),
cx,
)
.unwrap_or_default();
let mut hint_tasks = None;
for (row_range, new_hints_task) in new_hint_tasks {
hint_tasks
.get_or_insert_with(Vec::new)
.push(cx.spawn(async move |_, _| (row_range, new_hints_task.await)));
let (known_version, known_chunks) =
inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default();
if buffer_excerpts.buffer_version.changed_since(known_version) {
known_chunks.clear();
*known_version = buffer_excerpts.buffer_version;
}
hint_tasks
let mut hint_tasks = Vec::new();
for (row_range, new_hints_task) in new_hint_tasks {
let inserted = known_chunks.insert(row_range.clone());
if inserted || ignore_previous_fetches || invalidate_cache.should_invalidate() {
hint_tasks.push(cx.spawn(async move |_, _| (row_range, new_hints_task.await)));
}
}
Some(hint_tasks)
}
fn apply_fetched_hints(
@@ -780,28 +793,20 @@ impl Editor {
let excerpts = self.buffer.read(cx).excerpt_ids();
let hints_to_insert = new_hints
.into_iter()
.filter_map(|(chunk_range, hints_result)| {
let chunks_fetched = inlay_hints.hint_chunk_fetching.get_mut(&buffer_id);
match hints_result {
Ok(new_hints) => {
if new_hints.is_empty() {
if let Some((_, chunks_fetched)) = chunks_fetched {
chunks_fetched.remove(&chunk_range);
}
.filter_map(|(chunk_range, hints_result)| match hints_result {
Ok(new_hints) => Some(new_hints),
Err(e) => {
log::error!(
"Failed to query inlays for buffer row range {chunk_range:?}, {e:#}"
);
if let Some((for_version, chunks_fetched)) =
inlay_hints.hint_chunk_fetched.get_mut(&buffer_id)
{
if for_version == &query_version {
chunks_fetched.remove(&chunk_range);
}
Some(new_hints)
}
Err(e) => {
log::error!(
"Failed to query inlays for buffer row range {chunk_range:?}, {e:#}"
);
if let Some((for_version, chunks_fetched)) = chunks_fetched {
if for_version == &query_version {
chunks_fetched.remove(&chunk_range);
}
}
None
}
None
}
})
.flat_map(|hints| hints.into_values())
@@ -851,10 +856,9 @@ struct VisibleExcerpts {
fn spawn_editor_hints_refresh(
buffer_id: BufferId,
invalidate_cache: InvalidationStrategy,
ignore_previous_fetches: bool,
debounce: Option<Duration>,
buffer_excerpts: VisibleExcerpts,
known_chunks: Option<(Global, HashSet<Range<BufferRow>>)>,
applicable_chunks: Vec<Range<BufferRow>>,
cx: &mut Context<'_, Editor>,
) -> Task<()> {
cx.spawn(async move |editor, cx| {
@@ -865,7 +869,12 @@ fn spawn_editor_hints_refresh(
let query_version = buffer_excerpts.buffer_version.clone();
let Some(hint_tasks) = editor
.update(cx, |editor, cx| {
editor.inlay_hints_for_buffer(invalidate_cache, buffer_excerpts, known_chunks, cx)
editor.inlay_hints_for_buffer(
invalidate_cache,
ignore_previous_fetches,
buffer_excerpts,
cx,
)
})
.ok()
else {
@@ -873,19 +882,6 @@ fn spawn_editor_hints_refresh(
};
let hint_tasks = hint_tasks.unwrap_or_default();
if hint_tasks.is_empty() {
editor
.update(cx, |editor, _| {
if let Some((_, hint_chunk_fetching)) = editor
.inlay_hints
.as_mut()
.and_then(|inlay_hints| inlay_hints.hint_chunk_fetching.get_mut(&buffer_id))
{
for applicable_chunks in &applicable_chunks {
hint_chunk_fetching.remove(applicable_chunks);
}
}
})
.ok();
return;
}
let new_hints = join_all(hint_tasks).await;
@@ -1106,10 +1102,7 @@ pub mod tests {
editor
.update(cx, |editor, _window, cx| {
editor.refresh_inlay_hints(
InlayHintRefreshReason::RefreshRequested {
server_id: fake_server.server.server_id(),
request_id: Some(1),
},
InlayHintRefreshReason::RefreshRequested(fake_server.server.server_id()),
cx,
);
})
@@ -1965,8 +1958,15 @@ pub mod tests {
async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
show_value_hints: Some(true),
enabled: Some(true),
..InlayHintSettingsContent::default()
edit_debounce_ms: Some(0),
scroll_debounce_ms: Some(0),
show_type_hints: Some(true),
show_parameter_hints: Some(true),
show_other_hints: Some(true),
show_background: Some(false),
toggle_on_modifiers_press: None,
})
});
@@ -2044,7 +2044,6 @@ pub mod tests {
cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
cx.executor().run_until_parked();
let _fake_server = fake_servers.next().await.unwrap();
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
let ranges = lsp_request_ranges
@@ -2130,7 +2129,6 @@ pub mod tests {
);
})
.unwrap();
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
editor.update(cx, |_, _, _| {
let ranges = lsp_request_ranges
@@ -2147,7 +2145,6 @@ pub mod tests {
editor.handle_input("++++more text++++", window, cx);
})
.unwrap();
cx.executor().advance_clock(Duration::from_secs(1));
cx.executor().run_until_parked();
editor.update(cx, |editor, _window, cx| {
let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
@@ -3890,10 +3887,7 @@ let c = 3;"#
editor
.update(cx, |editor, _, cx| {
editor.refresh_inlay_hints(
InlayHintRefreshReason::RefreshRequested {
server_id: fake_server.server.server_id(),
request_id: Some(1),
},
InlayHintRefreshReason::RefreshRequested(fake_server.server.server_id()),
cx,
);
})
@@ -4028,7 +4022,7 @@ let c = 3;"#
let mut all_fetched_hints = Vec::new();
for buffer in editor.buffer.read(cx).all_buffers() {
lsp_store.update(cx, |lsp_store, cx| {
let hints = lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
let mut label = hint.text().to_string();
if hint.padding_left {

View File

@@ -1593,12 +1593,7 @@ impl SearchableItem for Editor {
) {
self.unfold_ranges(&[matches[index].clone()], false, true, cx);
let range = self.range_for_match(&matches[index], collapse);
let autoscroll = if EditorSettings::get_global(cx).search.center_on_match {
Autoscroll::center()
} else {
Autoscroll::fit()
};
self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| {
self.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([range]);
})
}

View File

@@ -60,10 +60,8 @@ async fn lsp_task_context(
buffer: &Entity<Buffer>,
cx: &mut AsyncApp,
) -> Option<TaskContext> {
let (worktree_store, environment) = project
.read_with(cx, |project, _| {
(project.worktree_store(), project.environment().clone())
})
let worktree_store = project
.read_with(cx, |project, _| project.worktree_store())
.ok()?;
let worktree_abs_path = cx
@@ -76,9 +74,9 @@ async fn lsp_task_context(
})
.ok()?;
let project_env = environment
.update(cx, |environment, cx| {
environment.buffer_environment(buffer, &worktree_store, cx)
let project_env = project
.update(cx, |project, cx| {
project.buffer_environment(buffer, &worktree_store, cx)
})
.ok()?
.await;

View File

@@ -878,6 +878,7 @@ mod tests {
use gpui::{AppContext as _, font, px};
use language::Capability;
use project::{Project, project_settings::DiagnosticSeverity};
use rope::Rope;
use settings::SettingsStore;
use util::post_inc;
@@ -1024,22 +1025,22 @@ mod tests {
Inlay::edit_prediction(
post_inc(&mut id),
buffer_snapshot.anchor_before(offset),
"test",
Rope::from_str_small("test"),
),
Inlay::edit_prediction(
post_inc(&mut id),
buffer_snapshot.anchor_after(offset),
"test",
Rope::from_str_small("test"),
),
Inlay::mock_hint(
post_inc(&mut id),
buffer_snapshot.anchor_before(offset),
"test",
Rope::from_str_small("test"),
),
Inlay::mock_hint(
post_inc(&mut id),
buffer_snapshot.anchor_after(offset),
"test",
Rope::from_str_small("test"),
),
]
})

View File

@@ -603,7 +603,7 @@ impl Editor {
scroll_position
};
self.scroll_manager.set_scroll_position(
let editor_was_scrolled = self.scroll_manager.set_scroll_position(
adjusted_position,
&display_map,
local,
@@ -611,7 +611,22 @@ impl Editor {
workspace_id,
window,
cx,
)
);
self.post_scroll_update = cx.spawn_in(window, async move |editor, cx| {
cx.background_executor()
.timer(Duration::from_millis(50))
.await;
editor
.update_in(cx, |editor, window, cx| {
editor.register_visible_buffers(cx);
editor.refresh_colors_for_visible_range(None, window, cx);
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
})
.ok();
});
editor_was_scrolled
}
pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<ScrollOffset> {

View File

@@ -193,7 +193,7 @@ impl Editor {
if let Some(language) = language {
for signature in &mut signature_help.signatures {
let text = Rope::from(signature.label.as_ref());
let text = Rope::from_str_small(signature.label.as_ref());
let highlights = language
.highlight_text(&text, 0..signature.label.len())
.into_iter()

View File

@@ -13,6 +13,8 @@ path = "src/extension.rs"
[dependencies]
anyhow.workspace = true
async-compression.workspace = true
async-tar.workspace = true
async-trait.workspace = true
collections.workspace = true
dap.workspace = true

View File

@@ -3,7 +3,9 @@ use crate::{
parse_wasm_extension_version,
};
use anyhow::{Context as _, Result, bail};
use futures::AsyncReadExt;
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use futures::{AsyncReadExt, io::Cursor};
use heck::ToSnakeCase;
use http_client::{self, AsyncBody, HttpClient};
use serde::Deserialize;
@@ -415,48 +417,28 @@ impl ExtensionBuilder {
return Ok(clang_path);
}
let tar_out_dir = self.cache_dir.join("wasi-sdk-temp");
let mut tar_out_dir = wasi_sdk_dir.clone();
tar_out_dir.set_extension("archive");
fs::remove_dir_all(&wasi_sdk_dir).ok();
fs::remove_dir_all(&tar_out_dir).ok();
fs::create_dir_all(&tar_out_dir).context("failed to create extraction directory")?;
log::info!("downloading wasi-sdk to {}", wasi_sdk_dir.display());
let mut response = self.http.get(&url, AsyncBody::default(), true).await?;
let body = GzipDecoder::new({
// stream the entire request into memory at once as the artifact is quite big (100MB+)
let mut b = vec![];
response.body_mut().read_to_end(&mut b).await?;
Cursor::new(b)
});
let tar = Archive::new(body);
// Write the response to a temporary file
let tar_gz_path = self.cache_dir.join("wasi-sdk.tar.gz");
let mut tar_gz_file =
fs::File::create(&tar_gz_path).context("failed to create temporary tar.gz file")?;
let response_body = response.body_mut();
let mut body_bytes = Vec::new();
response_body.read_to_end(&mut body_bytes).await?;
std::io::Write::write_all(&mut tar_gz_file, &body_bytes)?;
drop(tar_gz_file);
log::info!("un-tarring wasi-sdk to {}", tar_out_dir.display());
// Shell out to tar to extract the archive
let tar_output = util::command::new_smol_command("tar")
.arg("-xzf")
.arg(&tar_gz_path)
.arg("-C")
.arg(&tar_out_dir)
.output()
log::info!("un-tarring wasi-sdk to {}", wasi_sdk_dir.display());
tar.unpack(&tar_out_dir)
.await
.context("failed to run tar")?;
if !tar_output.status.success() {
bail!(
"failed to extract wasi-sdk archive: {}",
String::from_utf8_lossy(&tar_output.stderr)
);
}
.context("failed to unpack wasi-sdk archive")?;
log::info!("finished downloading wasi-sdk");
// Clean up the temporary tar.gz file
fs::remove_file(&tar_gz_path).ok();
let inner_dir = fs::read_dir(&tar_out_dir)?
.next()
.context("no content")?

View File

@@ -164,19 +164,6 @@ pub struct AgentServerManifestEntry {
/// args = ["--serve"]
/// sha256 = "abc123..." # optional
/// ```
///
/// For Node.js-based agents, you can use "node" as the cmd to automatically
/// use Zed's managed Node.js runtime instead of relying on the user's PATH:
/// ```toml
/// [agent_servers.nodeagent.targets.darwin-aarch64]
/// archive = "https://example.com/nodeagent.zip"
/// cmd = "node"
/// args = ["index.js", "--port", "3000"]
/// ```
///
/// Note: All commands are executed with the archive extraction directory as the
/// working directory, so relative paths in args (like "index.js") will resolve
/// relative to the extracted archive contents.
pub targets: HashMap<String, TargetConfig>,
}

View File

@@ -145,10 +145,6 @@ fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet<ExtensionProvide
provides.insert(ExtensionProvides::ContextServers);
}
if !manifest.agent_servers.is_empty() {
provides.insert(ExtensionProvides::AgentServers);
}
if manifest.snippets.is_some() {
provides.insert(ExtensionProvides::Snippets);
}

View File

@@ -1468,6 +1468,7 @@ impl ExtensionStore {
let extensions_dir = self.installed_dir.clone();
let index_path = self.index_path.clone();
let proxy = self.proxy.clone();
let executor = cx.background_executor().clone();
cx.background_spawn(async move {
let start_time = Instant::now();
let mut index = ExtensionIndex::default();
@@ -1501,10 +1502,14 @@ impl ExtensionStore {
}
if let Ok(index_json) = serde_json::to_string_pretty(&index) {
fs.save(&index_path, &index_json.as_str().into(), Default::default())
.await
.context("failed to save extension index")
.log_err();
fs.save(
&index_path,
&Rope::from_str(&index_json, &executor),
Default::default(),
)
.await
.context("failed to save extension index")
.log_err();
}
log::info!("rebuilt extension index in {:?}", start_time.elapsed());
@@ -1671,7 +1676,7 @@ impl ExtensionStore {
let manifest_toml = toml::to_string(&loaded_extension.manifest)?;
fs.save(
&tmp_dir.join(EXTENSION_TOML),
&Rope::from(manifest_toml),
&Rope::from_str_small(&manifest_toml),
language::LineEnding::Unix,
)
.await?;

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