Compare commits

..

4 Commits

Author SHA1 Message Date
Lukas Wirth
f289ee7223 Drain tasks on exit 2025-12-17 15:23:03 +01:00
Lukas Wirth
f2d1fd091e quitting 2025-12-17 15:18:00 +01:00
Lukas Wirth
70855a0267 Better drop order in tests 2025-12-17 13:17:42 +01:00
Lukas Wirth
d0ec591b23 Detect leaked entities at the end of test runs 2025-12-17 12:28:46 +01:00
438 changed files with 7450 additions and 19699 deletions

View File

@@ -1,55 +0,0 @@
# Phase 2: Explore Repository
You are analyzing a codebase to understand its structure before reviewing documentation impact.
## Objective
Produce a structured overview of the repository to inform subsequent documentation analysis.
## Instructions
1. **Identify Primary Languages and Frameworks**
- Scan for Cargo.toml, package.json, or other manifest files
- Note the primary language(s) and key dependencies
2. **Map Documentation Structure**
- This project uses **mdBook** (https://rust-lang.github.io/mdBook/)
- Documentation is in `docs/src/`
- Table of contents: `docs/src/SUMMARY.md` (mdBook format: https://rust-lang.github.io/mdBook/format/summary.html)
- Style guide: `docs/.rules`
- Agent guidelines: `docs/AGENTS.md`
- Formatting: Prettier (config in `docs/.prettierrc`)
3. **Identify Build and Tooling**
- Note build systems (cargo, npm, etc.)
- Identify documentation tooling (mdbook, etc.)
4. **Output Format**
Produce a JSON summary:
```json
{
"primary_language": "Rust",
"frameworks": ["GPUI"],
"documentation": {
"system": "mdBook",
"location": "docs/src/",
"toc_file": "docs/src/SUMMARY.md",
"toc_format": "https://rust-lang.github.io/mdBook/format/summary.html",
"style_guide": "docs/.rules",
"agent_guidelines": "docs/AGENTS.md",
"formatter": "prettier",
"formatter_config": "docs/.prettierrc",
"custom_preprocessor": "docs_preprocessor (handles {#kb action::Name} syntax)"
},
"key_directories": {
"source": "crates/",
"docs": "docs/src/",
"extensions": "extensions/"
}
}
```
## Constraints
- Read-only: Do not modify any files
- Focus on structure, not content details
- Complete within 2 minutes

View File

@@ -1,57 +0,0 @@
# Phase 3: Analyze Changes
You are analyzing code changes to understand their nature and scope.
## Objective
Produce a clear, neutral summary of what changed in the codebase.
## Input
You will receive:
- List of changed files from the triggering commit/PR
- Repository structure from Phase 2
## Instructions
1. **Categorize Changed Files**
- Source code (which crates/modules)
- Configuration
- Tests
- Documentation (already existing)
- Other
2. **Analyze Each Change**
- Review diffs for files likely to impact documentation
- Focus on: public APIs, settings, keybindings, commands, user-visible behavior
3. **Identify What Did NOT Change**
- Note stable interfaces or behaviors
- Important for avoiding unnecessary documentation updates
4. **Output Format**
Produce a markdown summary:
```markdown
## Change Analysis
### Changed Files Summary
| Category | Files | Impact Level |
| --- | --- | --- |
| Source - [crate] | file1.rs, file2.rs | High/Medium/Low |
| Settings | settings.json | Medium |
| Tests | test_*.rs | None |
### Behavioral Changes
- **[Feature/Area]**: Description of what changed from user perspective
- **[Feature/Area]**: Description...
### Unchanged Areas
- [Area]: Confirmed no changes to [specific behavior]
### Files Requiring Deeper Review
- `path/to/file.rs`: Reason for deeper review
```
## Constraints
- Read-only: Do not modify any files
- Neutral tone: Describe what changed, not whether it's good/bad
- Do not propose documentation changes yet

View File

@@ -1,76 +0,0 @@
# Phase 4: Plan Documentation Impact
You are determining whether and how documentation should be updated based on code changes.
## Objective
Produce a structured documentation plan that will guide Phase 5 execution.
## Documentation System
This is an **mdBook** site (https://rust-lang.github.io/mdBook/):
- `docs/src/SUMMARY.md` defines book structure per https://rust-lang.github.io/mdBook/format/summary.html
- If adding new pages, they MUST be added to SUMMARY.md
- Use `{#kb action::ActionName}` syntax for keybindings (custom preprocessor expands these)
- Prettier formatting (80 char width) will be applied automatically
## Input
You will receive:
- Change analysis from Phase 3
- Repository structure from Phase 2
- Documentation guidelines from `docs/AGENTS.md`
## Instructions
1. **Review AGENTS.md**
- Load and apply all rules from `docs/AGENTS.md`
- Respect scope boundaries (in-scope vs out-of-scope)
2. **Evaluate Documentation Impact**
For each behavioral change from Phase 3:
- Does existing documentation cover this area?
- Is the documentation now inaccurate or incomplete?
- Classify per AGENTS.md "Change Classification" section
3. **Identify Specific Updates**
For each required update:
- Exact file path
- Specific section or heading
- Type of change (update existing, add new, deprecate)
- Description of the change
4. **Flag Uncertainty**
Explicitly mark:
- Assumptions you're making
- Areas where human confirmation is needed
- Ambiguous requirements
5. **Output Format**
Use the exact format specified in `docs/AGENTS.md` Phase 4 section:
```markdown
## Documentation Impact Assessment
### Summary
Brief description of code changes analyzed.
### Documentation Updates Required: [Yes/No]
### Planned Changes
#### 1. [File Path]
- **Section**: [Section name or "New section"]
- **Change Type**: [Update/Add/Deprecate]
- **Reason**: Why this change is needed
- **Description**: What will be added/modified
### Uncertainty Flags
- [ ] [Description of any assumptions or areas needing confirmation]
### No Changes Needed
- [List files reviewed but not requiring updates, with brief reason]
```
## Constraints
- Read-only: Do not modify any files
- Conservative: When uncertain, flag for human review rather than planning changes
- Scoped: Only plan changes that trace directly to code changes from Phase 3
- No scope expansion: Do not plan "improvements" unrelated to triggering changes

View File

@@ -1,67 +0,0 @@
# Phase 5: Apply Documentation Plan
You are executing a pre-approved documentation plan for an **mdBook** documentation site.
## Objective
Implement exactly the changes specified in the documentation plan from Phase 4.
## Documentation System
- **mdBook**: https://rust-lang.github.io/mdBook/
- **SUMMARY.md**: Follows mdBook format (https://rust-lang.github.io/mdBook/format/summary.html)
- **Prettier**: Will be run automatically after this phase (80 char line width)
- **Custom preprocessor**: Use `{#kb action::ActionName}` for keybindings instead of hardcoding
## Input
You will receive:
- Documentation plan from Phase 4
- Documentation guidelines from `docs/AGENTS.md`
- Style rules from `docs/.rules`
## Instructions
1. **Validate Plan**
- Confirm all planned files are within scope per AGENTS.md
- Verify no out-of-scope files are targeted
2. **Execute Each Planned Change**
For each item in "Planned Changes":
- Navigate to the specified file
- Locate the specified section
- Apply the described change
- Follow style rules from `docs/.rules`
3. **Style Compliance**
Every edit must follow `docs/.rules`:
- Second person, present tense
- No hedging words ("simply", "just", "easily")
- Proper keybinding format (`Cmd+Shift+P`)
- Settings Editor first, JSON second
- Correct terminology (folder not directory, etc.)
4. **Preserve Context**
- Maintain surrounding content structure
- Keep consistent heading levels
- Preserve existing cross-references
## Constraints
- Execute ONLY changes listed in the plan
- Do not discover new documentation targets
- Do not make stylistic improvements outside planned sections
- Do not expand scope beyond what Phase 4 specified
- If a planned change cannot be applied (file missing, section not found), skip and note it
## Output
After applying changes, output a summary:
```markdown
## Applied Changes
### Successfully Applied
- `path/to/file.md`: [Brief description of change]
### Skipped (Could Not Apply)
- `path/to/file.md`: [Reason - e.g., "Section not found"]
### Warnings
- [Any issues encountered during application]
```

View File

@@ -1,54 +0,0 @@
# Phase 6: Summarize Changes
You are generating a summary of documentation updates for PR review.
## Objective
Create a clear, reviewable summary of all documentation changes made.
## Input
You will receive:
- Applied changes report from Phase 5
- Original change analysis from Phase 3
- Git diff of documentation changes
## Instructions
1. **Gather Change Information**
- List all modified documentation files
- Identify the corresponding code changes that triggered each update
2. **Generate Summary**
Use the format specified in `docs/AGENTS.md` Phase 6 section:
```markdown
## Documentation Update Summary
### Changes Made
| File | Change | Related Code |
| --- | --- | --- |
| docs/src/path.md | Brief description | PR #123 or commit SHA |
### Rationale
Brief explanation of why these updates were made, linking back to the triggering code changes.
### Review Notes
- Items reviewers should pay special attention to
- Any uncertainty flags from Phase 4 that were addressed
- Assumptions made during documentation
```
3. **Add Context for Reviewers**
- Highlight any changes that might be controversial
- Note if any planned changes were skipped and why
- Flag areas where reviewer expertise is especially needed
## Output Format
The summary should be suitable for:
- PR description body
- Commit message (condensed version)
- Team communication
## Constraints
- Read-only (documentation changes already applied in Phase 5)
- Factual: Describe what was done, not justify why it's good
- Complete: Account for all changes, including skipped items

View File

@@ -1,67 +0,0 @@
# Phase 7: Commit and Open PR
You are creating a git branch, committing documentation changes, and opening a PR.
## Objective
Package documentation updates into a reviewable pull request.
## Input
You will receive:
- Summary from Phase 6
- List of modified files
## Instructions
1. **Create Branch**
```sh
git checkout -b docs/auto-update-{date}
```
Use format: `docs/auto-update-YYYY-MM-DD` or `docs/auto-update-{short-sha}`
2. **Stage and Commit**
- Stage only documentation files in `docs/src/`
- Do not stage any other files
Commit message format:
```
docs: auto-update documentation for [brief description]
[Summary from Phase 6, condensed]
Triggered by: [commit SHA or PR reference]
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
```
3. **Push Branch**
```sh
git push -u origin docs/auto-update-{date}
```
4. **Create Pull Request**
Use the Phase 6 summary as the PR body.
PR Title: `docs: [Brief description of documentation updates]`
Labels (if available): `documentation`, `automated`
Base branch: `main`
## Constraints
- Do NOT auto-merge
- Do NOT request specific reviewers (let CODEOWNERS handle it)
- Do NOT modify files outside `docs/src/`
- If no changes to commit, exit gracefully with message "No documentation changes to commit"
## Output
```markdown
## PR Created
- **Branch**: docs/auto-update-{date}
- **PR URL**: https://github.com/zed-industries/zed/pull/XXXX
- **Status**: Ready for review
### Commit
- SHA: {commit-sha}
- Files: {count} documentation files modified
```

View File

@@ -25,7 +25,6 @@ self-hosted-runner:
- namespace-profile-32x64-ubuntu-2204
# Namespace Ubuntu 24.04 (like ubuntu-latest)
- namespace-profile-2x4-ubuntu-2404
- namespace-profile-8x32-ubuntu-2404
# Namespace Limited Preview
- namespace-profile-8x16-ubuntu-2004-arm-m4
- namespace-profile-8x32-ubuntu-2004-arm-m4

View File

@@ -19,18 +19,6 @@ runs:
shell: bash -euxo pipefail {0}
run: ./script/linux
- name: Install mold linker
shell: bash -euxo pipefail {0}
run: ./script/install-mold
- name: Download WASI SDK
shell: bash -euxo pipefail {0}
run: ./script/download-wasi-sdk
- name: Generate action metadata
shell: bash -euxo pipefail {0}
run: ./script/generate-action-metadata
- name: Check for broken links (in MD)
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:

View File

@@ -54,10 +54,6 @@ jobs:
- name: autofix_pr::run_autofix::run_cargo_fmt
run: cargo fmt --all
shell: bash -euxo pipefail {0}
- name: autofix_pr::run_autofix::run_cargo_fix
if: ${{ inputs.run_clippy }}
run: cargo fix --workspace --release --all-targets --all-features --allow-dirty --allow-staged
shell: bash -euxo pipefail {0}
- name: autofix_pr::run_autofix::run_clippy_fix
if: ${{ inputs.run_clippy }}
run: cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged
@@ -94,7 +90,7 @@ jobs:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: get-app-token
name: steps::authenticate_as_zippy
name: autofix_pr::commit_changes::authenticate_as_zippy
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
@@ -127,6 +123,3 @@ jobs:
GIT_AUTHOR_NAME: Zed Zippy
GIT_AUTHOR_EMAIL: 234243425+zed-zippy[bot]@users.noreply.github.com
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
concurrency:
group: ${{ github.workflow }}-${{ inputs.pr_number }}
cancel-in-progress: true

View File

@@ -30,7 +30,7 @@ jobs:
with:
clean: false
- id: get-app-token
name: steps::authenticate_as_zippy
name: cherry_pick::run_cherry_pick::authenticate_as_zippy
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}

View File

@@ -34,7 +34,6 @@ jobs:
CharlesChen0823
chbk
cppcoffee
davidbarsky
davewa
ddoemonn
djsauble

View File

@@ -1,40 +1,29 @@
name: "Close Stale Issues"
on:
schedule:
- cron: "0 2 * * 5"
- cron: "0 8 31 DEC *"
workflow_dispatch:
inputs:
debug-only:
description: "Run in dry-run mode (no changes made)"
type: boolean
default: false
operations-per-run:
description: "Max number of issues to process (default: 1000)"
type: number
default: 1000
jobs:
stale:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: >
Hi there!
Zed development moves fast and a significant number of bugs become outdated.
If you can reproduce this bug on the latest stable Zed, please let us know by leaving a comment with the Zed version.
If the bug doesn't appear for you anymore, feel free to close the issue yourself; otherwise, the bot will close it in a couple of weeks.
Hi there! 👋
We're working to clean up our issue tracker by closing older bugs that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and it will be kept open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, it will close automatically in 14 days.
Thanks for your help!
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please leave a comment with your Zed version so that we can reopen the issue."
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
days-before-stale: 60
days-before-close: 14
only-issue-types: "Bug,Crash"
operations-per-run: ${{ inputs.operations-per-run || 1000 }}
operations-per-run: 1000
ascending: true
enable-statistics: true
debug-only: ${{ inputs.debug-only }}
stale-issue-label: "stale"
exempt-issue-labels: "never stale"

View File

@@ -1,264 +0,0 @@
name: Documentation Automation
on:
# push:
# branches: [main]
# paths:
# - 'crates/**'
# - 'extensions/**'
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to analyze (gets full PR diff)'
required: false
type: string
trigger_sha:
description: 'Commit SHA to analyze (ignored if pr_number is set)'
required: false
type: string
permissions:
contents: write
pull-requests: write
env:
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
DROID_MODEL: claude-opus-4-5-20251101
jobs:
docs-automation:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Droid CLI
id: install-droid
run: |
curl -fsSL https://app.factory.ai/cli | sh
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
echo "DROID_BIN=${HOME}/.local/bin/droid" >> "$GITHUB_ENV"
# Verify installation
"${HOME}/.local/bin/droid" --version
- name: Setup Node.js (for Prettier)
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Prettier
run: npm install -g prettier
- name: Get changed files
id: changed
run: |
if [ -n "${{ inputs.pr_number }}" ]; then
# Get full PR diff
echo "Analyzing PR #${{ inputs.pr_number }}"
echo "source=pr" >> "$GITHUB_OUTPUT"
echo "ref=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT"
gh pr diff "${{ inputs.pr_number }}" --name-only > /tmp/changed_files.txt
elif [ -n "${{ inputs.trigger_sha }}" ]; then
# Get single commit diff
SHA="${{ inputs.trigger_sha }}"
echo "Analyzing commit $SHA"
echo "source=commit" >> "$GITHUB_OUTPUT"
echo "ref=$SHA" >> "$GITHUB_OUTPUT"
git diff --name-only "${SHA}^" "$SHA" > /tmp/changed_files.txt
else
# Default to current commit
SHA="${{ github.sha }}"
echo "Analyzing commit $SHA"
echo "source=commit" >> "$GITHUB_OUTPUT"
echo "ref=$SHA" >> "$GITHUB_OUTPUT"
git diff --name-only "${SHA}^" "$SHA" > /tmp/changed_files.txt || git diff --name-only HEAD~1 HEAD > /tmp/changed_files.txt
fi
echo "Changed files:"
cat /tmp/changed_files.txt
env:
GH_TOKEN: ${{ github.token }}
# Phase 0: Guardrails are loaded via AGENTS.md in each phase
# Phase 2: Explore Repository (Read-Only - default)
- name: "Phase 2: Explore Repository"
id: phase2
run: |
"$DROID_BIN" exec \
-m "$DROID_MODEL" \
-f .factory/prompts/docs-automation/phase2-explore.md \
> /tmp/phase2-output.txt 2>&1 || true
echo "Repository exploration complete"
cat /tmp/phase2-output.txt
# Phase 3: Analyze Changes (Read-Only - default)
- name: "Phase 3: Analyze Changes"
id: phase3
run: |
CHANGED_FILES=$(tr '\n' ' ' < /tmp/changed_files.txt)
echo "Analyzing changes in: $CHANGED_FILES"
# Build prompt with context
cat > /tmp/phase3-prompt.md << 'EOF'
$(cat .factory/prompts/docs-automation/phase3-analyze.md)
## Context
### Changed Files
$CHANGED_FILES
### Phase 2 Output
$(cat /tmp/phase2-output.txt)
EOF
"$DROID_BIN" exec \
-m "$DROID_MODEL" \
"$(cat .factory/prompts/docs-automation/phase3-analyze.md)
Changed files: $CHANGED_FILES" \
> /tmp/phase3-output.md 2>&1 || true
echo "Change analysis complete"
cat /tmp/phase3-output.md
# Phase 4: Plan Documentation Impact (Read-Only - default)
- name: "Phase 4: Plan Documentation Impact"
id: phase4
run: |
"$DROID_BIN" exec \
-m "$DROID_MODEL" \
-f .factory/prompts/docs-automation/phase4-plan.md \
> /tmp/phase4-plan.md 2>&1 || true
echo "Documentation plan complete"
cat /tmp/phase4-plan.md
# Check if updates are required
if grep -q "NO_UPDATES_REQUIRED" /tmp/phase4-plan.md; then
echo "updates_required=false" >> "$GITHUB_OUTPUT"
else
echo "updates_required=true" >> "$GITHUB_OUTPUT"
fi
# Phase 5: Apply Plan (Write-Enabled with --auto medium)
- name: "Phase 5: Apply Documentation Plan"
id: phase5
if: steps.phase4.outputs.updates_required == 'true'
run: |
"$DROID_BIN" exec \
-m "$DROID_MODEL" \
--auto medium \
-f .factory/prompts/docs-automation/phase5-apply.md \
> /tmp/phase5-report.md 2>&1 || true
echo "Documentation updates applied"
cat /tmp/phase5-report.md
# Phase 5b: Format with Prettier
- name: "Phase 5b: Format with Prettier"
id: phase5b
if: steps.phase4.outputs.updates_required == 'true'
run: |
echo "Formatting documentation with Prettier..."
cd docs && prettier --write src/
echo "Verifying Prettier formatting passes..."
cd docs && prettier --check src/
echo "Prettier formatting complete"
# Phase 6: Summarize Changes (Read-Only - default)
- name: "Phase 6: Summarize Changes"
id: phase6
if: steps.phase4.outputs.updates_required == 'true'
run: |
# Get git diff of docs
git diff docs/src/ > /tmp/docs-diff.txt || true
"$DROID_BIN" exec \
-m "$DROID_MODEL" \
-f .factory/prompts/docs-automation/phase6-summarize.md \
> /tmp/phase6-summary.md 2>&1 || true
echo "Summary generated"
cat /tmp/phase6-summary.md
# Phase 7: Commit and Open PR
- name: "Phase 7: Create PR"
id: phase7
if: steps.phase4.outputs.updates_required == 'true'
run: |
# Check if there are actual changes
if git diff --quiet docs/src/; then
echo "No documentation changes detected"
exit 0
fi
# Configure git
git config user.name "factory-droid[bot]"
git config user.email "138933559+factory-droid[bot]@users.noreply.github.com"
# Daily batch branch - one branch per day, multiple commits accumulate
BRANCH_NAME="docs/auto-update-$(date +%Y-%m-%d)"
# Stash local changes from phase 5
git stash push -m "docs-automation-changes" -- docs/src/
# Check if branch already exists on remote
if git ls-remote --exit-code --heads origin "$BRANCH_NAME" > /dev/null 2>&1; then
echo "Branch $BRANCH_NAME exists, checking out and updating..."
git fetch origin "$BRANCH_NAME"
git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME"
else
echo "Creating new branch $BRANCH_NAME..."
git checkout -b "$BRANCH_NAME"
fi
# Apply stashed changes
git stash pop || true
# Stage and commit
git add docs/src/
SUMMARY=$(head -50 < /tmp/phase6-summary.md)
git commit -m "docs: auto-update documentation
${SUMMARY}
Triggered by: ${{ steps.changed.outputs.source }} ${{ steps.changed.outputs.ref }}
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>"
# Push
git push -u origin "$BRANCH_NAME"
# Check if PR already exists for this branch
EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number --jq '.[0].number' || echo "")
if [ -n "$EXISTING_PR" ]; then
echo "PR #$EXISTING_PR already exists for branch $BRANCH_NAME, updated with new commit"
else
# Create new PR
gh pr create \
--title "docs: automated documentation update ($(date +%Y-%m-%d))" \
--body-file /tmp/phase6-summary.md \
--base main || true
echo "PR created on branch: $BRANCH_NAME"
fi
env:
GH_TOKEN: ${{ github.token }}
# Summary output
- name: "Summary"
if: always()
run: |
echo "## Documentation Automation Summary" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
if [ "${{ steps.phase4.outputs.updates_required }}" == "false" ]; then
echo "No documentation updates required for this change." >> "$GITHUB_STEP_SUMMARY"
elif [ -f /tmp/phase6-summary.md ]; then
cat /tmp/phase6-summary.md >> "$GITHUB_STEP_SUMMARY"
else
echo "Workflow completed. Check individual phase outputs for details." >> "$GITHUB_STEP_SUMMARY"
fi

View File

@@ -51,7 +51,7 @@ jobs:
needs:
- orchestrate
if: needs.orchestrate.outputs.check_rust == 'true'
runs-on: namespace-profile-4x8-ubuntu-2204
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -79,7 +79,7 @@ jobs:
needs:
- orchestrate
if: needs.orchestrate.outputs.check_extension == 'true'
runs-on: namespace-profile-8x32-ubuntu-2404
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

View File

@@ -472,17 +472,11 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: get-app-token
name: steps::authenticate_as_zippy
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
- name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
notify_on_failure:
needs:
- upload_release_assets

View File

@@ -74,12 +74,9 @@ jobs:
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
version: '9'
- name: steps::prettier
- name: ./script/prettier
run: ./script/prettier
shell: bash -euxo pipefail {0}
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: ./script/check-todos
run: ./script/check-todos
shell: bash -euxo pipefail {0}
@@ -90,6 +87,9 @@ jobs:
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
with:
config: ./typos.toml
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
timeout-minutes: 60
run_tests_windows:
needs:
@@ -353,9 +353,6 @@ jobs:
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/generate-action-metadata
run: ./script/generate-action-metadata
shell: bash -euxo pipefail {0}
- name: run_tests::check_docs::install_mdbook
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08
with:

1
.gitignore vendored
View File

@@ -36,7 +36,6 @@
DerivedData/
Packages
xcuserdata/
crates/docs_preprocessor/actions.json
# Don't commit any secrets to the repo.
.env

View File

@@ -141,9 +141,6 @@ Uladzislau Kaminski <i@uladkaminski.com>
Uladzislau Kaminski <i@uladkaminski.com> <uladzislau_kaminski@epam.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com>
Vitaly Slobodin <vitaliy.slobodin@gmail.com> <vitaly_slobodin@fastmail.com>
Yara <davidsk@zed.dev>
Yara <git@davidsk.dev>
Yara <git@yara.blue>
Will Bradley <williambbradley@gmail.com>
Will Bradley <williambbradley@gmail.com> <will@zed.dev>
WindSoilder <WindSoilder@outlook.com>

View File

@@ -23,6 +23,7 @@ In particular we love PRs that are:
If you're looking for concrete ideas:
- [Curated board of issues](https://github.com/orgs/zed-industries/projects/69) suitable for everyone from first-time contributors to seasoned community champions.
- [Triaged bugs with confirmed steps to reproduce](https://github.com/zed-industries/zed/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug%20label%3Astate%3Areproducible).
- [Area labels](https://github.com/zed-industries/zed/labels?q=area%3A*) to browse bugs in a specific part of the product you care about (after clicking on an area label, add type:Bug to the search).

215
Cargo.lock generated
View File

@@ -226,9 +226,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.9.2"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
dependencies = [
"agent-client-protocol-schema",
"anyhow",
@@ -243,9 +243,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol-schema"
version = "0.10.5"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6"
dependencies = [
"anyhow",
"derive_more 2.0.1",
@@ -793,7 +793,7 @@ dependencies = [
"url",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols 0.32.9",
"zbus",
]
@@ -1441,9 +1441,9 @@ dependencies = [
[[package]]
name = "aws-config"
version = "1.8.10"
version = "1.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1856b1b48b65f71a4dd940b1c0931f9a7b646d4a924b9828ffefc1454714668a"
checksum = "37cf2b6af2a95a20e266782b4f76f1a5e12bf412a9db2de9c1e9123b9d8c0ad8"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1507,9 +1507,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
version = "1.5.13"
version = "1.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f2402da1a5e16868ba98725e5d73f26b8116eaa892e56f2cd0bf5eec7985f70"
checksum = "bfa006bb32360ed90ac51203feafb9d02e3d21046e1fd3a450a404b90ea73e5d"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@@ -1532,9 +1532,9 @@ dependencies = [
[[package]]
name = "aws-sdk-bedrockruntime"
version = "1.112.0"
version = "1.109.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06c037e6823696d752702ec2bad758d3cf95d1b92b712c8ac7e93824b5e2391"
checksum = "fbfdfd941dcb253c17bf70baddbf1e5b22f19e29d313d2e049bad4b1dadb2011"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1614,9 +1614,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.88.0"
version = "1.86.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05b276777560aa9a196dbba2e3aada4d8006d3d7eeb3ba7fe0c317227d933c4"
checksum = "4a0abbfab841446cce6e87af853a3ba2cc1bc9afcd3f3550dd556c43d434c86d"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1636,9 +1636,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.90.0"
version = "1.88.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9be14d6d9cd761fac3fd234a0f47f7ed6c0df62d83c0eeb7012750e4732879b"
checksum = "9a68d675582afea0e94d38b6ca9c5aaae4ca14f1d36faa6edb19b42e687e70d7"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1658,9 +1658,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.90.0"
version = "1.88.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98a862d704c817d865c8740b62d8bbeb5adcb30965e93b471df8a5bcefa20a80"
checksum = "d30990923f4f675523c51eb1c0dec9b752fb267b36a61e83cbc219c9d86da715"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -1681,9 +1681,9 @@ dependencies = [
[[package]]
name = "aws-sigv4"
version = "1.3.6"
version = "1.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c35452ec3f001e1f2f6db107b6373f1f48f05ec63ba2c5c9fa91f07dad32af11"
checksum = "bffc03068fbb9c8dd5ce1c6fb240678a5cffb86fb2b7b1985c999c4b83c8df68"
dependencies = [
"aws-credential-types",
"aws-smithy-eventstream",
@@ -1740,9 +1740,9 @@ dependencies = [
[[package]]
name = "aws-smithy-eventstream"
version = "0.60.13"
version = "0.60.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e29a304f8319781a39808847efb39561351b1bb76e933da7aa90232673638658"
checksum = "9656b85088f8d9dc7ad40f9a6c7228e1e8447cdf4b046c87e152e0805dea02fa"
dependencies = [
"aws-smithy-types",
"bytes 1.10.1",
@@ -1751,9 +1751,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http"
version = "0.62.5"
version = "0.62.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445d5d720c99eed0b4aa674ed00d835d9b1427dd73e04adaf2f94c6b2d6f9fca"
checksum = "3feafd437c763db26aa04e0cc7591185d0961e64c61885bece0fb9d50ceac671"
dependencies = [
"aws-smithy-eventstream",
"aws-smithy-runtime-api",
@@ -1761,7 +1761,6 @@ dependencies = [
"bytes 1.10.1",
"bytes-utils",
"futures-core",
"futures-util",
"http 0.2.12",
"http 1.3.1",
"http-body 0.4.6",
@@ -1773,9 +1772,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http-client"
version = "1.1.4"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "623254723e8dfd535f566ee7b2381645f8981da086b5c4aa26c0c41582bb1d2c"
checksum = "1053b5e587e6fa40ce5a79ea27957b04ba660baa02b28b7436f64850152234f1"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -1803,9 +1802,9 @@ dependencies = [
[[package]]
name = "aws-smithy-json"
version = "0.61.7"
version = "0.61.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2db31f727935fc63c6eeae8b37b438847639ec330a9161ece694efba257e0c54"
checksum = "cff418fc8ec5cadf8173b10125f05c2e7e1d46771406187b2c878557d4503390"
dependencies = [
"aws-smithy-types",
]
@@ -1831,9 +1830,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
version = "1.9.4"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bbe9d018d646b96c7be063dd07987849862b0e6d07c778aad7d93d1be6c1ef0"
checksum = "40ab99739082da5347660c556689256438defae3bcefd66c52b095905730e404"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -1855,9 +1854,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.9.2"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7204f9fd94749a7c53b26da1b961b4ac36bf070ef1e0b94bb09f79d4f6c193"
checksum = "3683c5b152d2ad753607179ed71988e8cfd52964443b4f74fd8e552d0bbfeb46"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
@@ -1872,9 +1871,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "1.3.4"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f535879a207fce0db74b679cfc3e91a3159c8144d717d55f5832aea9eef46e"
checksum = "9f5b3a7486f6690ba25952cabf1e7d75e34d69eaff5081904a47bc79074d6457"
dependencies = [
"base64-simd",
"bytes 1.10.1",
@@ -1898,18 +1897,18 @@ dependencies = [
[[package]]
name = "aws-smithy-xml"
version = "0.60.12"
version = "0.60.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab77cdd036b11056d2a30a7af7b775789fb024bf216acc13884c6c97752ae56"
checksum = "e9c34127e8c624bc2999f3b657e749c1393bedc9cd97b92a804db8ced4d2e163"
dependencies = [
"xmlparser",
]
[[package]]
name = "aws-types"
version = "1.3.10"
version = "1.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d79fb68e3d7fe5d4833ea34dc87d2e97d26d3086cb3da660bb6b1f76d98680b6"
checksum = "e2fd329bf0e901ff3f60425691410c69094dc2a1f34b331f37bfc4e9ac1565a1"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
@@ -2667,9 +2666,9 @@ dependencies = [
[[package]]
name = "cap-fs-ext"
version = "3.4.4"
version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e41cc18551193fe8fa6f15c1e3c799bc5ec9e2cfbfaa8ed46f37013e3e6c173c"
checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654"
dependencies = [
"cap-primitives",
"cap-std",
@@ -2679,9 +2678,9 @@ dependencies = [
[[package]]
name = "cap-net-ext"
version = "3.4.4"
version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f83833816c66c986e913b22ac887cec216ea09301802054316fc5301809702c"
checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7"
dependencies = [
"cap-primitives",
"cap-std",
@@ -2691,9 +2690,9 @@ dependencies = [
[[package]]
name = "cap-primitives"
version = "3.4.4"
version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a1e394ed14f39f8bc26f59d4c0c010dbe7f0a1b9bafff451b1f98b67c8af62a"
checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a"
dependencies = [
"ambient-authority",
"fs-set-times",
@@ -2709,9 +2708,9 @@ dependencies = [
[[package]]
name = "cap-rand"
version = "3.4.4"
version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0acb89ccf798a28683f00089d0630dfaceec087234eae0d308c05ddeaa941b40"
checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d"
dependencies = [
"ambient-authority",
"rand 0.8.5",
@@ -2719,9 +2718,9 @@ dependencies = [
[[package]]
name = "cap-std"
version = "3.4.4"
version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07c0355ca583dd58f176c3c12489d684163861ede3c9efa6fd8bba314c984189"
checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a"
dependencies = [
"cap-primitives",
"io-extras",
@@ -2731,9 +2730,9 @@ dependencies = [
[[package]]
name = "cap-time-ext"
version = "3.4.4"
version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "491af520b8770085daa0466978c75db90368c71896523f2464214e38359b1a5b"
checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80"
dependencies = [
"ambient-authority",
"cap-primitives",
@@ -2896,17 +2895,6 @@ dependencies = [
"util",
]
[[package]]
name = "chardetng"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
dependencies = [
"cfg-if",
"encoding_rs",
"memchr",
]
[[package]]
name = "chrono"
version = "0.4.42"
@@ -3525,33 +3513,6 @@ dependencies = [
"theme",
]
[[package]]
name = "component_preview"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"component",
"db",
"fs",
"gpui",
"language",
"log",
"node_runtime",
"notifications",
"project",
"release_channel",
"reqwest_client",
"session",
"settings",
"theme",
"ui",
"ui_input",
"uuid",
"workspace",
]
[[package]]
name = "compression-codecs"
version = "0.4.31"
@@ -5048,6 +5009,8 @@ name = "docs_preprocessor"
version = "0.1.0"
dependencies = [
"anyhow",
"command_palette",
"gpui",
"mdbook",
"regex",
"serde",
@@ -5056,6 +5019,7 @@ dependencies = [
"task",
"theme",
"util",
"zed",
"zlog",
]
@@ -5212,7 +5176,6 @@ dependencies = [
"anyhow",
"arrayvec",
"brotli",
"buffer_diff",
"client",
"clock",
"cloud_api_types",
@@ -5250,9 +5213,7 @@ dependencies = [
"strum 0.27.2",
"telemetry",
"telemetry_events",
"text",
"thiserror 2.0.17",
"time",
"ui",
"util",
"uuid",
@@ -5357,10 +5318,8 @@ dependencies = [
"anyhow",
"buffer_diff",
"client",
"clock",
"cloud_llm_client",
"codestral",
"collections",
"command_palette_hooks",
"copilot",
"edit_prediction",
@@ -5369,20 +5328,18 @@ dependencies = [
"feature_flags",
"fs",
"futures 0.3.31",
"git",
"gpui",
"indoc",
"language",
"language_model",
"log",
"lsp",
"markdown",
"menu",
"multi_buffer",
"paths",
"pretty_assertions",
"project",
"regex",
"release_channel",
"semver",
"serde_json",
"settings",
"supermaven",
@@ -5395,7 +5352,6 @@ dependencies = [
"workspace",
"zed_actions",
"zeta_prompt",
"zlog",
]
[[package]]
@@ -7402,7 +7358,7 @@ dependencies = [
"wayland-backend",
"wayland-client",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols 0.31.2",
"wayland-protocols-plasma",
"wayland-protocols-wlr",
"windows 0.61.3",
@@ -8653,7 +8609,6 @@ dependencies = [
"extension",
"gpui",
"language",
"lsp",
"paths",
"project",
"schemars",
@@ -8841,7 +8796,6 @@ dependencies = [
"ctor",
"diffy",
"ec4rs",
"encoding_rs",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -8860,7 +8814,6 @@ dependencies = [
"regex",
"rpc",
"schemars",
"semver",
"serde",
"serde_json",
"settings",
@@ -8965,8 +8918,6 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
"extension",
"extension_host",
"fs",
"futures 0.3.31",
"google_ai",
@@ -9097,7 +9048,6 @@ dependencies = [
"regex",
"rope",
"rust-embed",
"semver",
"serde",
"serde_json",
"serde_json_lenient",
@@ -12512,7 +12462,6 @@ dependencies = [
"dap",
"dap_adapters",
"db",
"encoding_rs",
"extension",
"fancy-regex",
"fs",
@@ -12606,7 +12555,6 @@ dependencies = [
"gpui",
"language",
"menu",
"notifications",
"pretty_assertions",
"project",
"rayon",
@@ -12684,8 +12632,6 @@ dependencies = [
"paths",
"rope",
"serde",
"strum 0.27.2",
"tempfile",
"text",
"util",
"uuid",
@@ -18965,6 +18911,18 @@ dependencies = [
"xcursor",
]
[[package]]
name = "wayland-protocols"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.32.9"
@@ -18979,14 +18937,14 @@ dependencies = [
[[package]]
name = "wayland-protocols-plasma"
version = "0.3.9"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols 0.31.2",
"wayland-scanner",
]
@@ -18999,7 +18957,7 @@ dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols 0.32.9",
"wayland-scanner",
]
@@ -19159,20 +19117,6 @@ dependencies = [
"winsafe",
]
[[package]]
name = "which_key"
version = "0.1.0"
dependencies = [
"command_palette",
"gpui",
"serde",
"settings",
"theme",
"ui",
"util",
"workspace",
]
[[package]]
name = "whoami"
version = "1.6.1"
@@ -20270,10 +20214,8 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-lock 2.8.0",
"chardetng",
"clock",
"collections",
"encoding_rs",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -20301,16 +20243,6 @@ dependencies = [
"zlog",
]
[[package]]
name = "worktree_benchmarks"
version = "0.1.0"
dependencies = [
"fs",
"gpui",
"settings",
"worktree",
]
[[package]]
name = "writeable"
version = "0.6.1"
@@ -20679,7 +20611,6 @@ dependencies = [
"collections",
"command_palette",
"component",
"component_preview",
"copilot",
"crashes",
"dap",
@@ -20785,6 +20716,7 @@ dependencies = [
"tree-sitter-md",
"tree-sitter-rust",
"ui",
"ui_input",
"ui_prompt",
"url",
"urlencoding",
@@ -20795,7 +20727,6 @@ dependencies = [
"watch",
"web_search",
"web_search_providers",
"which_key",
"windows 0.61.3",
"winresource",
"workspace",
@@ -20978,7 +20909,7 @@ dependencies = [
[[package]]
name = "zed_proto"
version = "0.3.1"
version = "0.3.0"
dependencies = [
"zed_extension_api 0.7.0",
]

View File

@@ -39,7 +39,6 @@ members = [
"crates/command_palette",
"crates/command_palette_hooks",
"crates/component",
"crates/component_preview",
"crates/context_server",
"crates/copilot",
"crates/crashes",
@@ -193,13 +192,11 @@ members = [
"crates/vercel",
"crates/vim",
"crates/vim_mode_setting",
"crates/which_key",
"crates/watch",
"crates/web_search",
"crates/web_search_providers",
"crates/workspace",
"crates/worktree",
"crates/worktree_benchmarks",
"crates/x_ai",
"crates/zed",
"crates/zed_actions",
@@ -276,7 +273,6 @@ collections = { path = "crates/collections", version = "0.1.0" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
component = { path = "crates/component" }
component_preview = { path = "crates/component_preview" }
context_server = { path = "crates/context_server" }
copilot = { path = "crates/copilot" }
crashes = { path = "crates/crashes" }
@@ -419,7 +415,6 @@ util_macros = { path = "crates/util_macros" }
vercel = { path = "crates/vercel" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
which_key = { path = "crates/which_key" }
watch = { path = "crates/watch" }
web_search = { path = "crates/web_search" }
@@ -441,7 +436,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
# External crates
#
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = "0.25.1-rc1"
any_vec = "0.14"
@@ -460,15 +455,15 @@ async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.31.0"
async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] }
aws-config = { version = "1.8.10", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.8", features = [
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.2", features = [
"hardcoded-credentials",
] }
aws-sdk-bedrockruntime = { version = "1.112.0", features = [
aws-sdk-bedrockruntime = { version = "1.80.0", features = [
"behavior-version-latest",
] }
aws-smithy-runtime-api = { version = "1.9.2", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.4", features = ["http-body-1-x"] }
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
backtrace = "0.3"
base64 = "0.22"
bincode = "1.2.1"
@@ -481,7 +476,6 @@ bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.21"
cfg-if = "1.0.3"
chardetng = "0.1"
chrono = { version = "0.4", features = ["serde"] }
ciborium = "0.2"
circular-buffer = "1.0"
@@ -505,7 +499,6 @@ dotenvy = "0.15.0"
ec4rs = "1.1"
emojis = "0.6.1"
env_logger = "0.11"
encoding_rs = "0.8"
exec = "0.3.1"
fancy-regex = "0.16.0"
fork = "0.4.0"

View File

@@ -20,6 +20,7 @@ Other platforms are not yet available:
- [Building Zed for macOS](./docs/src/development/macos.md)
- [Building Zed for Linux](./docs/src/development/linux.md)
- [Building Zed for Windows](./docs/src/development/windows.md)
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
### Contributing

View File

@@ -28,7 +28,7 @@ ai
= @rtfeldman
audio
= @yara-blue
= @dvdsk
crashes
= @p1n3appl3
@@ -53,7 +53,7 @@ extension
git
= @cole-miller
= @danilo-leal
= @yara-blue
= @dvdsk
= @kubkon
= @Anthony-Eid
= @cameron1024
@@ -76,7 +76,7 @@ languages
linux
= @cole-miller
= @yara-blue
= @dvdsk
= @p1n3appl3
= @probably-neb
= @smitbarmase
@@ -92,7 +92,7 @@ multi_buffer
= @SomeoneToIgnore
pickers
= @yara-blue
= @dvdsk
= @p1n3appl3
= @SomeoneToIgnore

View File

@@ -227,7 +227,6 @@
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-k l": "agent::OpenRulesLibrary",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -241,7 +240,6 @@
"ctrl-alt-l": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "agent::ToggleModelSelector",
"alt-tab": "agent::CycleFavoriteModels",
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
@@ -254,6 +252,7 @@
"ctrl-y": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"ctrl-alt-z": "agent::RejectOnce",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -265,9 +264,9 @@
{
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-insert": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown",
"copy": "markdown::Copy",
"ctrl-insert": "markdown::Copy",
"ctrl-c": "markdown::Copy",
},
},
{
@@ -285,6 +284,36 @@
"ctrl-alt-t": "agent::NewThread",
},
},
{
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
"bindings": {
"enter": "agent::Chat",
"ctrl-enter": "agent::ChatWithFollow",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
},
},
{
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
"bindings": {
"ctrl-enter": "agent::Chat",
"enter": "editor::Newline",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
},
},
{
"context": "EditMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline",
},
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"bindings": {
@@ -299,25 +328,14 @@
"ctrl-enter": "menu::Confirm",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::ChatWithFollow",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
},
},
{
@@ -325,7 +343,11 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::Chat",
"enter": "editor::Newline",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -792,7 +814,7 @@
},
},
{
"context": "InlineAssistant",
"context": "PromptEditor",
"bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist",
@@ -883,8 +905,8 @@
"bindings": {
"left": "git_panel::CollapseSelectedEntry",
"right": "git_panel::ExpandSelectedEntry",
"up": "git_panel::PreviousEntry",
"down": "git_panel::NextEntry",
"up": "menu::SelectPrevious",
"down": "menu::SelectNext",
"enter": "menu::Confirm",
"alt-y": "git::StageFile",
"alt-shift-y": "git::UnstageFile",

View File

@@ -267,7 +267,6 @@
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-k l": "agent::OpenRulesLibrary",
"alt-tab": "agent::CycleFavoriteModels",
"cmd-shift-v": "agent::PasteRaw",
},
},
{
@@ -282,7 +281,6 @@
"cmd-alt-p": "agent::ManageProfiles",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "agent::ToggleModelSelector",
"alt-tab": "agent::CycleFavoriteModels",
"cmd-shift-j": "agent::ToggleNavigationMenu",
"cmd-alt-m": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
@@ -295,6 +293,7 @@
"cmd-y": "agent::AllowOnce",
"cmd-alt-y": "agent::AllowAlways",
"cmd-alt-z": "agent::RejectOnce",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -307,7 +306,7 @@
"context": "AgentPanel > Markdown",
"use_key_equivalents": true,
"bindings": {
"cmd-c": "markdown::CopyAsMarkdown",
"cmd-c": "markdown::Copy",
},
},
{
@@ -326,6 +325,39 @@
"cmd-alt-t": "agent::NewThread",
},
},
{
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"cmd-enter": "agent::ChatWithFollow",
"cmd-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
},
},
{
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "agent::Chat",
"enter": "editor::Newline",
"cmd-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
},
},
{
"context": "EditMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline",
},
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"use_key_equivalents": true,
@@ -347,25 +379,16 @@
"cmd-enter": "menu::Confirm",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"cmd-enter": "agent::ChatWithFollow",
"cmd-shift-v": "agent::PasteRaw",
"cmd-i": "agent::ToggleProfileSelector",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -373,7 +396,11 @@
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "agent::Chat",
"enter": "editor::Newline",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -853,7 +880,7 @@
},
},
{
"context": "InlineAssistant > Editor",
"context": "PromptEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-alt-/": "agent::ToggleModelSelector",
@@ -954,12 +981,12 @@
"context": "GitPanel && ChangesList",
"use_key_equivalents": true,
"bindings": {
"up": "git_panel::PreviousEntry",
"down": "git_panel::NextEntry",
"cmd-up": "git_panel::FirstEntry",
"cmd-down": "git_panel::LastEntry",
"left": "git_panel::CollapseSelectedEntry",
"right": "git_panel::ExpandSelectedEntry",
"up": "menu::SelectPrevious",
"down": "menu::SelectNext",
"cmd-up": "menu::SelectFirst",
"cmd-down": "menu::SelectLast",
"enter": "menu::Confirm",
"cmd-alt-y": "git::ToggleStaged",
"space": "git::ToggleStaged",

View File

@@ -227,7 +227,6 @@
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-k l": "agent::OpenRulesLibrary",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -241,7 +240,6 @@
"shift-alt-l": "agent::OpenRulesLibrary",
"shift-alt-p": "agent::ManageProfiles",
"ctrl-i": "agent::ToggleProfileSelector",
"alt-tab": "agent::CycleFavoriteModels",
"shift-alt-/": "agent::ToggleModelSelector",
"shift-alt-j": "agent::ToggleNavigationMenu",
"shift-alt-i": "agent::ToggleOptionsMenu",
@@ -255,6 +253,7 @@
"shift-alt-a": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"shift-alt-z": "agent::RejectOnce",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -268,7 +267,7 @@
"context": "AgentPanel > Markdown",
"use_key_equivalents": true,
"bindings": {
"ctrl-c": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::Copy",
},
},
{
@@ -287,6 +286,39 @@
"ctrl-alt-t": "agent::NewThread",
},
},
{
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"ctrl-enter": "agent::ChatWithFollow",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
},
},
{
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::Chat",
"enter": "editor::Newline",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
},
},
{
"context": "EditMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline",
},
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"use_key_equivalents": true,
@@ -302,25 +334,16 @@
"ctrl-enter": "menu::Confirm",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::ChatWithFollow",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -328,7 +351,11 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::Chat",
"enter": "editor::Newline",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -796,7 +823,7 @@
},
},
{
"context": "InlineAssistant",
"context": "PromptEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist",
@@ -881,10 +908,10 @@
"context": "GitPanel && ChangesList",
"use_key_equivalents": true,
"bindings": {
"up": "git_panel::PreviousEntry",
"down": "git_panel::NextEntry",
"left": "git_panel::CollapseSelectedEntry",
"right": "git_panel::ExpandSelectedEntry",
"up": "menu::SelectPrevious",
"down": "menu::SelectNext",
"enter": "menu::Confirm",
"alt-y": "git::StageFile",
"shift-alt-y": "git::UnstageFile",

View File

@@ -24,7 +24,7 @@
},
},
{
"context": "InlineAssistant > Editor",
"context": "InlineAssistEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-backspace": "editor::Cancel",

View File

@@ -24,7 +24,7 @@
},
},
{
"context": "InlineAssistant > Editor",
"context": "InlineAssistEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-backspace": "editor::Cancel",

View File

@@ -14,6 +14,7 @@ The section you'll need to rewrite is marked with <rewrite_this></rewrite_this>
The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
{{/if}}
{{#if rewrite_section}}
And here's the section to rewrite based on that prompt again for reference:
<rewrite_this>
@@ -32,6 +33,8 @@ Below are the diagnostic errors visible to the user. If the user requests probl
{{/each}}
{{/if}}
{{/if}}
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
Start at the indentation level in the original file in the rewritten {{content_type}}.

View File

@@ -1178,10 +1178,6 @@
"remove_trailing_whitespace_on_save": true,
// Whether to start a new line with a comment when a previous line is a comment as well.
"extend_comment_on_newline": true,
// Whether to continue markdown lists when pressing enter.
"extend_list_on_newline": true,
// Whether to indent list items when pressing tab after a list marker.
"indent_list_on_tab": true,
// Removes any lines containing only whitespace at the end of the file and
// ensures just one newline at the end.
"ensure_final_newline_on_save": true,
@@ -1325,14 +1321,6 @@
"hidden_files": ["**/.*"],
// Git gutter behavior configuration.
"git": {
// Global switch to enable or disable all git integration features.
// If set to true, disables all git integration features.
// If set to false, individual git integration features below will be independently enabled or disabled.
"disable_git": false,
// Whether to enable git status tracking.
"enable_status": true,
// Whether to enable git diff display.
"enable_diff": true,
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"
@@ -1717,12 +1705,7 @@
// }
//
"file_types": {
"JSONC": [
"**/.zed/*.json",
"**/.vscode/**/*.json",
"**/{zed,Zed}/{settings,keymap,tasks,debug}.json",
"tsconfig*.json",
],
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
"Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
"Shell Script": [".env.*"],
},
@@ -2169,13 +2152,6 @@
// The shape can be one of the following: "block", "bar", "underline", "hollow".
"cursor_shape": {},
},
// Which-key popup settings
"which_key": {
// Whether to show the which-key popup when holding down key combinations.
"enabled": false,
// Delay in milliseconds before showing the which-key popup.
"delay_ms": 1000,
},
// The server to connect to. If the environment variable
// ZED_SERVER_URL is set, it will override this setting.
"server_url": "https://zed.dev",

View File

@@ -11,7 +11,6 @@ use language::language_settings::FormatOnSave;
pub use mention::*;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
use serde::{Deserialize, Serialize};
use serde_json::to_string_pretty;
use settings::Settings as _;
use task::{Shell, ShellBuilder};
pub use terminal::*;
@@ -193,7 +192,6 @@ pub struct ToolCall {
pub locations: Vec<acp::ToolCallLocation>,
pub resolved_locations: Vec<Option<AgentLocation>>,
pub raw_input: Option<serde_json::Value>,
pub raw_input_markdown: Option<Entity<Markdown>>,
pub raw_output: Option<serde_json::Value>,
}
@@ -224,11 +222,6 @@ impl ToolCall {
}
}
let raw_input_markdown = tool_call
.raw_input
.as_ref()
.and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
let result = Self {
id: tool_call.tool_call_id,
label: cx
@@ -239,7 +232,6 @@ impl ToolCall {
resolved_locations: Vec::default(),
status,
raw_input: tool_call.raw_input,
raw_input_markdown,
raw_output: tool_call.raw_output,
};
Ok(result)
@@ -315,7 +307,6 @@ impl ToolCall {
}
if let Some(raw_input) = raw_input {
self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
self.raw_input = Some(raw_input);
}
@@ -1364,7 +1355,6 @@ impl AcpThread {
locations: Vec::new(),
resolved_locations: Vec::new(),
raw_input: None,
raw_input_markdown: None,
raw_output: None,
};
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
@@ -1993,42 +1983,37 @@ impl AcpThread {
fn update_last_checkpoint(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let git_store = self.project.read(cx).git_store().clone();
let Some((_, message)) = self.last_user_message() else {
let old_checkpoint = if let Some((_, message)) = self.last_user_message() {
if let Some(checkpoint) = message.checkpoint.as_ref() {
checkpoint.git_checkpoint.clone()
} else {
return Task::ready(Ok(()));
}
} else {
return Task::ready(Ok(()));
};
let Some(user_message_id) = message.id.clone() else {
return Task::ready(Ok(()));
};
let Some(checkpoint) = message.checkpoint.as_ref() else {
return Task::ready(Ok(()));
};
let old_checkpoint = checkpoint.git_checkpoint.clone();
let new_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx));
cx.spawn(async move |this, cx| {
let Some(new_checkpoint) = new_checkpoint
let new_checkpoint = new_checkpoint
.await
.context("failed to get new checkpoint")
.log_err()
else {
return Ok(());
};
let equal = git_store
.update(cx, |git, cx| {
git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
})?
.await
.unwrap_or(true);
this.update(cx, |this, cx| {
if let Some((ix, message)) = this.user_message_mut(&user_message_id) {
if let Some(checkpoint) = message.checkpoint.as_mut() {
checkpoint.show = !equal;
cx.emit(AcpThreadEvent::EntryUpdated(ix));
}
}
})?;
.log_err();
if let Some(new_checkpoint) = new_checkpoint {
let equal = git_store
.update(cx, |git, cx| {
git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
})?
.await
.unwrap_or(true);
this.update(cx, |this, cx| {
let (ix, message) = this.last_user_message().context("no user message")?;
let checkpoint = message.checkpoint.as_mut().context("no checkpoint")?;
checkpoint.show = !equal;
cx.emit(AcpThreadEvent::EntryUpdated(ix));
anyhow::Ok(())
})??;
}
Ok(())
})
@@ -2428,10 +2413,8 @@ fn markdown_for_raw_output(
)
})),
value => Some(cx.new(|cx| {
let pretty_json = to_string_pretty(value).unwrap_or_else(|_| value.to_string());
Markdown::new(
format!("```json\n{}\n```", pretty_json).into(),
format!("```json\n{}\n```", value).into(),
Some(language_registry.clone()),
None,
cx,
@@ -4074,67 +4057,4 @@ mod tests {
"Should have exactly 2 terminals (the completed ones from before checkpoint)"
);
}
/// Tests that update_last_checkpoint correctly updates the original message's checkpoint
/// even when a new user message is added while the async checkpoint comparison is in progress.
///
/// This is a regression test for a bug where update_last_checkpoint would fail with
/// "no checkpoint" if a new user message (without a checkpoint) was added between when
/// update_last_checkpoint started and when its async closure ran.
#[gpui::test]
async fn test_update_last_checkpoint_with_new_message_added(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/test"), json!({".git": {}, "file.txt": "content"}))
.await;
let project = Project::test(fs.clone(), [Path::new(path!("/test"))], cx).await;
let handler_done = Arc::new(AtomicBool::new(false));
let handler_done_clone = handler_done.clone();
let connection = Rc::new(FakeAgentConnection::new().on_user_message(
move |_, _thread, _cx| {
handler_done_clone.store(true, SeqCst);
async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) }.boxed_local()
},
));
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
.await
.unwrap();
let send_future = thread.update(cx, |thread, cx| thread.send_raw("First message", cx));
let send_task = cx.background_executor.spawn(send_future);
// Tick until handler completes, then a few more to let update_last_checkpoint start
while !handler_done.load(SeqCst) {
cx.executor().tick();
}
for _ in 0..5 {
cx.executor().tick();
}
thread.update(cx, |thread, cx| {
thread.push_entry(
AgentThreadEntry::UserMessage(UserMessage {
id: Some(UserMessageId::new()),
content: ContentBlock::Empty,
chunks: vec!["Injected message (no checkpoint)".into()],
checkpoint: None,
indented: false,
}),
cx,
);
});
cx.run_until_parked();
let result = send_task.await;
assert!(
result.is_ok(),
"send should succeed even when new message added during update_last_checkpoint: {:?}",
result.err()
);
}
}

View File

@@ -202,15 +202,12 @@ pub trait AgentModelSelector: 'static {
fn should_render_footer(&self) -> bool {
false
}
}
/// Icon for a model in the model selector.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AgentModelIcon {
/// A built-in icon from Zed's icon set.
Named(IconName),
/// Path to a custom SVG icon file.
Path(SharedString),
/// Whether this selector supports the favorites feature.
/// Only the native agent uses the model ID format that maps to settings.
fn supports_favorites(&self) -> bool {
false
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -218,7 +215,7 @@ pub struct AgentModelInfo {
pub id: acp::ModelId,
pub name: SharedString,
pub description: Option<SharedString>,
pub icon: Option<AgentModelIcon>,
pub icon: Option<IconName>,
}
impl From<acp::ModelInfo> for AgentModelInfo {

View File

@@ -6,7 +6,7 @@ use futures::{FutureExt, StreamExt, channel::mpsc};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
};
use language::{Anchor, Buffer, BufferEvent, Point, ToPoint};
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
use text::{Edit, Patch, Rope};
@@ -150,7 +150,7 @@ impl ActionLog {
if buffer
.read(cx)
.file()
.is_some_and(|file| file.disk_state().is_deleted())
.is_some_and(|file| file.disk_state() == DiskState::Deleted)
{
// If the buffer had been edited by a tool, but it got
// deleted externally, we want to stop tracking it.
@@ -162,7 +162,7 @@ impl ActionLog {
if buffer
.read(cx)
.file()
.is_some_and(|file| !file.disk_state().is_deleted())
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
{
// If the buffer had been deleted by a tool, but it got
// resurrected externally, we want to clear the edits we
@@ -769,7 +769,7 @@ impl ActionLog {
tracked.version != buffer.version
&& buffer
.file()
.is_some_and(|file| !file.disk_state().is_deleted())
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
})
.map(|(buffer, _)| buffer)
}

View File

@@ -30,7 +30,7 @@ use futures::{StreamExt, future};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
};
use language_model::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{
ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
@@ -93,7 +93,7 @@ impl LanguageModels {
fn refresh_list(&mut self, cx: &App) {
let providers = LanguageModelRegistry::global(cx)
.read(cx)
.visible_providers()
.providers()
.into_iter()
.filter(|provider| provider.is_authenticated(cx))
.collect::<Vec<_>>();
@@ -153,10 +153,7 @@ impl LanguageModels {
id: Self::model_id(model),
name: model.name().0,
description: None,
icon: Some(match provider.icon() {
IconOrSvg::Svg(path) => acp_thread::AgentModelIcon::Path(path),
IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),
}),
icon: Some(provider.icon()),
}
}
@@ -167,7 +164,7 @@ impl LanguageModels {
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.visible_providers()
.providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
@@ -429,7 +426,7 @@ impl NativeAgent {
.into_iter()
.flat_map(|(contents, prompt_metadata)| match contents {
Ok(contents) => Some(UserRulesContext {
uuid: prompt_metadata.id.as_user()?,
uuid: prompt_metadata.id.user_id()?,
title: prompt_metadata.title.map(|title| title.to_string()),
contents,
}),
@@ -1167,6 +1164,10 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
fn should_render_footer(&self) -> bool {
true
}
fn supports_favorites(&self) -> bool {
true
}
}
impl acp_thread::AgentConnection for NativeAgentConnection {
@@ -1629,9 +1630,7 @@ mod internal_tests {
id: acp::ModelId::new("fake/fake"),
name: "Fake".into(),
description: None,
icon: Some(acp_thread::AgentModelIcon::Named(
ui::IconName::ZedAssistant
)),
icon: Some(ui::IconName::ZedAssistant),
}]
)])
);

View File

@@ -216,10 +216,14 @@ impl HistoryStore {
}
pub fn reload(&self, cx: &mut Context<Self>) {
let database_connection = ThreadsDatabase::connect(cx);
let database_future = ThreadsDatabase::connect(cx);
cx.spawn(async move |this, cx| {
let database = database_connection.await;
let threads = database.map_err(|err| anyhow!(err))?.list_threads().await?;
let threads = database_future
.await
.map_err(|err| anyhow!(err))?
.list_threads()
.await?;
this.update(cx, |this, cx| {
if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES {
for thread in threads
@@ -340,8 +344,7 @@ impl HistoryStore {
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
cx.background_spawn(async move {
if cfg!(any(feature = "test-support", test)) {
log::warn!("history store does not persist in tests");
return Ok(VecDeque::new());
anyhow::bail!("history store does not persist in tests");
}
let json = KEY_VALUE_STORE
.read_kvp(RECENTLY_OPENED_THREADS_KEY)?

View File

@@ -1,14 +1,10 @@
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
use agent_client_protocol as acp;
use agent_servers::{AgentServer, AgentServerDelegate};
use agent_settings::AgentSettings;
use anyhow::Result;
use collections::HashSet;
use fs::Fs;
use gpui::{App, Entity, SharedString, Task};
use prompt_store::PromptStore;
use settings::{LanguageModelSelection, Settings as _, update_settings_file};
use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates};
@@ -75,38 +71,6 @@ impl AgentServer for NativeAgentServer {
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
AgentSettings::get_global(cx).favorite_model_ids()
}
fn toggle_favorite_model(
&self,
model_id: acp::ModelId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
let selection = model_id_to_selection(&model_id);
update_settings_file(fs, cx, move |settings, _| {
let agent = settings.agent.get_or_insert_default();
if should_be_favorite {
agent.add_favorite_model(selection.clone());
} else {
agent.remove_favorite_model(&selection);
}
});
}
}
/// Convert a ModelId (e.g. "anthropic/claude-3-5-sonnet") to a LanguageModelSelection.
fn model_id_to_selection(model_id: &acp::ModelId) -> LanguageModelSelection {
let id = model_id.0.as_ref();
let (provider, model) = id.split_once('/').unwrap_or(("", id));
LanguageModelSelection {
provider: provider.to_owned().into(),
model: model.to_owned(),
}
}
#[cfg(test)]

View File

@@ -1725,10 +1725,6 @@ impl Thread {
self.pending_summary_generation.is_some()
}
pub fn is_generating_title(&self) -> bool {
self.pending_title_generation.is_some()
}
pub fn summary(&mut self, cx: &mut Context<Self>) -> Shared<Task<Option<SharedString>>> {
if let Some(summary) = self.summary.as_ref() {
return Task::ready(Some(summary.clone())).shared();
@@ -1796,7 +1792,7 @@ impl Thread {
task
}
pub fn generate_title(&mut self, cx: &mut Context<Self>) {
fn generate_title(&mut self, cx: &mut Context<Self>) {
let Some(model) = self.summarization_model.clone() else {
return;
};

View File

@@ -4,8 +4,6 @@ mod codex;
mod custom;
mod gemini;
use collections::HashSet;
#[cfg(any(test, feature = "test-support"))]
pub mod e2e_tests;
@@ -58,19 +56,9 @@ impl AgentServerDelegate {
pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName;
fn name(&self) -> SharedString;
fn connect(
&self,
root_dir: Option<&Path>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
None
}
fn set_default_mode(
&self,
_mode_id: Option<agent_client_protocol::SessionModeId>,
@@ -91,18 +79,14 @@ pub trait AgentServer: Send {
) {
}
fn favorite_model_ids(&self, _cx: &mut App) -> HashSet<agent_client_protocol::ModelId> {
HashSet::default()
}
fn toggle_favorite_model(
fn connect(
&self,
_model_id: agent_client_protocol::ModelId,
_should_be_favorite: bool,
_fs: Arc<dyn Fs>,
_cx: &App,
) {
}
root_dir: Option<&Path>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
}
impl dyn AgentServer {

View File

@@ -1,5 +1,4 @@
use agent_client_protocol as acp;
use collections::HashSet;
use fs::Fs;
use settings::{SettingsStore, update_settings_file};
use std::path::Path;
@@ -73,48 +72,6 @@ impl AgentServer for ClaudeCode {
});
}
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
settings
.as_ref()
.map(|s| {
s.favorite_models
.iter()
.map(|id| acp::ModelId::new(id.clone()))
.collect()
})
.unwrap_or_default()
}
fn toggle_favorite_model(
&self,
model_id: acp::ModelId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
update_settings_file(fs, cx, move |settings, _| {
let favorite_models = &mut settings
.agent_servers
.get_or_insert_default()
.claude
.get_or_insert_default()
.favorite_models;
let model_id_str = model_id.to_string();
if should_be_favorite {
if !favorite_models.contains(&model_id_str) {
favorite_models.push(model_id_str);
}
} else {
favorite_models.retain(|id| id != &model_id_str);
}
});
}
fn connect(
&self,
root_dir: Option<&Path>,

View File

@@ -5,7 +5,6 @@ use std::{any::Any, path::Path};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use collections::HashSet;
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
@@ -74,48 +73,6 @@ impl AgentServer for Codex {
});
}
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.map(|s| {
s.favorite_models
.iter()
.map(|id| acp::ModelId::new(id.clone()))
.collect()
})
.unwrap_or_default()
}
fn toggle_favorite_model(
&self,
model_id: acp::ModelId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
update_settings_file(fs, cx, move |settings, _| {
let favorite_models = &mut settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.favorite_models;
let model_id_str = model_id.to_string();
if should_be_favorite {
if !favorite_models.contains(&model_id_str) {
favorite_models.push(model_id_str);
}
} else {
favorite_models.retain(|id| id != &model_id_str);
}
});
}
fn connect(
&self,
root_dir: Option<&Path>,

View File

@@ -2,7 +2,6 @@ use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use collections::HashSet;
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName};
@@ -55,7 +54,6 @@ impl AgentServer for CustomAgentServer {
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
default_model: None,
default_mode: None,
favorite_models: Vec::new(),
});
match settings {
@@ -92,7 +90,6 @@ impl AgentServer for CustomAgentServer {
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
default_model: None,
default_mode: None,
favorite_models: Vec::new(),
});
match settings {
@@ -104,66 +101,6 @@ impl AgentServer for CustomAgentServer {
});
}
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.custom
.get(&self.name())
.cloned()
});
settings
.as_ref()
.map(|s| {
s.favorite_models()
.iter()
.map(|id| acp::ModelId::new(id.clone()))
.collect()
})
.unwrap_or_default()
}
fn toggle_favorite_model(
&self,
model_id: acp::ModelId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
let name = self.name();
update_settings_file(fs, cx, move |settings, _| {
let settings = settings
.agent_servers
.get_or_insert_default()
.custom
.entry(name.clone())
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
default_model: None,
default_mode: None,
favorite_models: Vec::new(),
});
let favorite_models = match settings {
settings::CustomAgentServerSettings::Custom {
favorite_models, ..
}
| settings::CustomAgentServerSettings::Extension {
favorite_models, ..
} => favorite_models,
};
let model_id_str = model_id.to_string();
if should_be_favorite {
if !favorite_models.contains(&model_id_str) {
favorite_models.push(model_id_str);
}
} else {
favorite_models.retain(|id| id != &model_id_str);
}
});
}
fn connect(
&self,
root_dir: Option<&Path>,

View File

@@ -460,7 +460,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
ignore_system_version: None,
default_mode: None,
default_model: None,
favorite_models: vec![],
}),
gemini: Some(crate::gemini::tests::local_command().into()),
codex: Some(BuiltinAgentServerSettings {
@@ -470,7 +469,6 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
ignore_system_version: None,
default_mode: None,
default_model: None,
favorite_models: vec![],
}),
custom: collections::HashMap::default(),
},

View File

@@ -13,7 +13,7 @@ path = "src/agent_ui.rs"
doctest = false
[features]
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support", "agent/test-support"]
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support"]
unit-eval = []
[dependencies]

View File

@@ -31,10 +31,10 @@ use rope::Point;
use settings::Settings;
use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc};
use theme::ThemeSettings;
use ui::{ContextMenu, prelude::*};
use ui::prelude::*;
use util::{ResultExt, debug_panic};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, PasteRaw};
use zed_actions::agent::Chat;
pub struct MessageEditor {
mention_set: Entity<MentionSet>,
@@ -132,21 +132,6 @@ impl MessageEditor {
placement: Some(ContextMenuPlacement::Above),
});
editor.register_addon(MessageEditorAddon::new());
editor.set_custom_context_menu(|editor, _point, window, cx| {
let has_selection = editor.has_non_empty_selection(&editor.display_snapshot(cx));
Some(ContextMenu::build(window, cx, |menu, _, _| {
menu.action("Cut", Box::new(editor::actions::Cut))
.action_disabled_when(
!has_selection,
"Copy",
Box::new(editor::actions::Copy),
)
.action("Paste", Box::new(editor::actions::Paste))
}))
});
editor
});
let mention_set =
@@ -558,9 +543,6 @@ impl MessageEditor {
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let editor_clipboard_selections = cx
.read_from_clipboard()
.and_then(|item| item.entries().first().cloned())
@@ -571,127 +553,133 @@ impl MessageEditor {
_ => None,
});
// Insert creases for pasted clipboard selections that:
// 1. Contain exactly one selection
// 2. Have an associated file path
// 3. Span multiple lines (not single-line selections)
// 4. Belong to a file that exists in the current project
let should_insert_creases = util::maybe!({
let selections = editor_clipboard_selections.as_ref()?;
if selections.len() > 1 {
return Some(false);
}
let selection = selections.first()?;
let file_path = selection.file_path.as_ref()?;
let line_range = selection.line_range.as_ref()?;
let has_file_context = editor_clipboard_selections
.as_ref()
.is_some_and(|selections| {
selections
.iter()
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
});
if line_range.start() == line_range.end() {
return Some(false);
}
Some(
workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some(),
)
})
.unwrap_or(false);
if should_insert_creases && let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
let insertion_target = self
.editor
.read(cx)
.selections
.newest_anchor()
.start
.text_anchor;
let project = workspace.read(cx).project().clone();
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let crease_text =
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
let mention_uri = MentionUri::Selection {
abs_path: Some(file_path.clone()),
line_range: line_range.clone(),
};
let mention_text = mention_uri.as_link().to_string();
let (excerpt_id, text_anchor, content_len) =
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx);
let snapshot = buffer.snapshot(cx);
let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
editor.insert(&mention_text, window, cx);
editor.insert(" ", window, cx);
(*excerpt_id, text_anchor, mention_text.len())
});
let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
crease_text.into(),
mention_uri.icon_path(cx),
None,
self.editor.clone(),
window,
cx,
) else {
continue;
};
drop(tx);
let mention_task = cx
.spawn({
let project = project.clone();
async move |_, cx| {
let project_path = project
.update(cx, |project, cx| {
project.project_path_for_absolute_path(&file_path, cx)
})
.map_err(|e| e.to_string())?
.ok_or_else(|| "project path not found".to_string())?;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?;
buffer
.update(cx, |buffer, cx| {
let start = Point::new(*line_range.start(), 0)
.min(buffer.max_point());
let end = Point::new(*line_range.end() + 1, 0)
.min(buffer.max_point());
let content = buffer.text_for_range(start..end).collect();
Mention::Text {
content,
tracked_buffers: vec![cx.entity()],
}
})
.map_err(|e| e.to_string())
}
})
.shared();
self.mention_set.update(cx, |mention_set, _cx| {
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
});
if has_file_context {
if let Some((workspace, selections)) =
self.workspace.upgrade().zip(editor_clipboard_selections)
{
let Some(first_selection) = selections.first() else {
return;
};
if let Some(file_path) = &first_selection.file_path {
// In case someone pastes selections from another window
// with a different project, we don't want to insert the
// crease (containing the absolute path) since the agent
// cannot access files outside the project.
let is_in_project = workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some();
if !is_in_project {
return;
}
}
cx.stop_propagation();
let insertion_target = self
.editor
.read(cx)
.selections
.newest_anchor()
.start
.text_anchor;
let project = workspace.read(cx).project().clone();
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let crease_text =
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
let mention_uri = MentionUri::Selection {
abs_path: Some(file_path.clone()),
line_range: line_range.clone(),
};
let mention_text = mention_uri.as_link().to_string();
let (excerpt_id, text_anchor, content_len) =
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx);
let snapshot = buffer.snapshot(cx);
let (excerpt_id, _, buffer_snapshot) =
snapshot.as_singleton().unwrap();
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
editor.insert(&mention_text, window, cx);
editor.insert(" ", window, cx);
(*excerpt_id, text_anchor, mention_text.len())
});
let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
crease_text.into(),
mention_uri.icon_path(cx),
None,
self.editor.clone(),
window,
cx,
) else {
continue;
};
drop(tx);
let mention_task = cx
.spawn({
let project = project.clone();
async move |_, cx| {
let project_path = project
.update(cx, |project, cx| {
project.project_path_for_absolute_path(&file_path, cx)
})
.map_err(|e| e.to_string())?
.ok_or_else(|| "project path not found".to_string())?;
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path, cx)
})
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?;
buffer
.update(cx, |buffer, cx| {
let start = Point::new(*line_range.start(), 0)
.min(buffer.max_point());
let end = Point::new(*line_range.end() + 1, 0)
.min(buffer.max_point());
let content =
buffer.text_for_range(start..end).collect();
Mention::Text {
content,
tracked_buffers: vec![cx.entity()],
}
})
.map_err(|e| e.to_string())
}
})
.shared();
self.mention_set.update(cx, |mention_set, _cx| {
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
});
}
}
return;
}
return;
}
if self.prompt_capabilities.borrow().image
@@ -702,13 +690,6 @@ impl MessageEditor {
}
}
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
let editor = self.editor.clone();
window.defer(cx, move |window, cx| {
editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
});
}
pub fn insert_dragged_files(
&mut self,
paths: Vec<project::ProjectPath>,
@@ -986,7 +967,6 @@ impl Render for MessageEditor {
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::chat_with_follow))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::paste_raw))
.capture_action(cx.listener(Self::paste))
.flex_1()
.child({
@@ -1385,7 +1365,7 @@ mod tests {
cx,
);
});
message_editor.read(cx).focus_handle(cx).focus(window, cx);
message_editor.read(cx).focus_handle(cx).focus(window);
message_editor.read(cx).editor().clone()
});
@@ -1607,7 +1587,7 @@ mod tests {
cx,
);
});
message_editor.read(cx).focus_handle(cx).focus(window, cx);
message_editor.read(cx).focus_handle(cx).focus(window);
let editor = message_editor.read(cx).editor().clone();
(message_editor, editor)
});
@@ -2335,7 +2315,7 @@ mod tests {
cx,
);
});
message_editor.read(cx).focus_handle(cx).focus(window, cx);
message_editor.read(cx).focus_handle(cx).focus(window);
let editor = message_editor.read(cx).editor().clone();
(message_editor, editor)
});

View File

@@ -186,17 +186,6 @@ impl Render for ModeSelector {
move |_window, cx| {
v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Toggle Mode Menu"))
.child(KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
cx,
)),
)
.child(
h_flex()
.pb_1()
@@ -211,6 +200,17 @@ impl Render for ModeSelector {
cx,
)),
)
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Toggle Mode Menu"))
.child(KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
cx,
)),
)
.into_any()
}
}),

View File

@@ -1,21 +1,21 @@
use std::{cmp::Reverse, rc::Rc, sync::Arc};
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
use agent_client_protocol::ModelId;
use agent_servers::AgentServer;
use agent_settings::AgentSettings;
use anyhow::Result;
use collections::{HashSet, IndexMap};
use fs::Fs;
use futures::FutureExt;
use fuzzy::{StringMatchCandidate, match_strings};
use gpui::{
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
WeakEntity,
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use settings::SettingsStore;
use settings::Settings;
use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, prelude::*};
use util::ResultExt;
use zed_actions::agent::OpenSettings;
@@ -54,9 +54,7 @@ pub struct AcpModelPickerDelegate {
selected_index: usize,
selected_description: Option<(usize, SharedString, bool)>,
selected_model: Option<AgentModelInfo>,
favorites: HashSet<ModelId>,
_refresh_models_task: Task<()>,
_settings_subscription: Subscription,
focus_handle: FocusHandle,
}
@@ -104,19 +102,6 @@ impl AcpModelPickerDelegate {
})
};
let agent_server_for_subscription = agent_server.clone();
let settings_subscription =
cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
// Only refresh if the favorites actually changed to avoid redundant work
// when other settings are modified (e.g., user editing settings.json)
let new_favorites = agent_server_for_subscription.favorite_model_ids(cx);
if new_favorites != picker.delegate.favorites {
picker.delegate.favorites = new_favorites;
picker.refresh(window, cx);
}
});
let favorites = agent_server.favorite_model_ids(cx);
Self {
selector,
agent_server,
@@ -126,9 +111,7 @@ impl AcpModelPickerDelegate {
selected_model: None,
selected_index: 0,
selected_description: None,
favorites,
_refresh_models_task: refresh_models_task,
_settings_subscription: settings_subscription,
focus_handle,
}
}
@@ -137,37 +120,40 @@ impl AcpModelPickerDelegate {
self.selected_model.as_ref()
}
pub fn favorites_count(&self) -> usize {
self.favorites.len()
}
pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if self.favorites.is_empty() {
if !self.selector.supports_favorites() {
return;
}
let Some(models) = &self.models else {
let favorites = AgentSettings::get_global(cx).favorite_model_ids();
if favorites.is_empty() {
return;
}
let Some(models) = self.models.clone() else {
return;
};
let all_models: Vec<&AgentModelInfo> = match models {
AgentModelList::Flat(list) => list.iter().collect(),
AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
let all_models: Vec<AgentModelInfo> = match models {
AgentModelList::Flat(list) => list,
AgentModelList::Grouped(index_map) => index_map
.into_values()
.flatten()
.collect::<Vec<AgentModelInfo>>(),
};
let favorite_models: Vec<_> = all_models
.into_iter()
.filter(|model| self.favorites.contains(&model.id))
let favorite_models = all_models
.iter()
.filter(|model| favorites.contains(&model.id))
.unique_by(|model| &model.id)
.collect();
.cloned()
.collect::<Vec<_>>();
if favorite_models.is_empty() {
return;
}
let current_id = self.selected_model.as_ref().map(|m| &m.id);
let current_id = self.selected_model.as_ref().map(|m| m.id.clone());
let current_index_in_favorites = current_id
.as_ref()
.and_then(|id| favorite_models.iter().position(|m| &m.id == id))
.unwrap_or(usize::MAX);
@@ -234,7 +220,11 @@ impl PickerDelegate for AcpModelPickerDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let favorites = self.favorites.clone();
let favorites = if self.selector.supports_favorites() {
Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
} else {
Default::default()
};
cx.spawn_in(window, async move |this, cx| {
let filtered_models = match this
@@ -252,7 +242,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries =
info_list_to_picker_entries(filtered_models, &favorites);
info_list_to_picker_entries(filtered_models, favorites);
// Finds the currently selected model in the list
let new_index = this
.delegate
@@ -327,20 +317,21 @@ impl PickerDelegate for AcpModelPickerDelegate {
let default_model = self.agent_server.default_model(cx);
let is_default = default_model.as_ref() == Some(&model_info.id);
let supports_favorites = self.selector.supports_favorites();
let is_favorite = *is_favorite;
let handle_action_click = {
let model_id = model_info.id.clone();
let fs = self.fs.clone();
let agent_server = self.agent_server.clone();
cx.listener(move |_, _, _, cx| {
agent_server.toggle_favorite_model(
move |cx: &App| {
crate::favorite_models::toggle_model_id_in_settings(
model_id.clone(),
!is_favorite,
fs.clone(),
cx,
);
})
}
};
Some(
@@ -359,15 +350,13 @@ impl PickerDelegate for AcpModelPickerDelegate {
})
.child(
ModelSelectorListItem::new(ix, model_info.name.clone())
.map(|this| match &model_info.icon {
Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),
Some(AgentModelIcon::Named(icon)) => this.icon(*icon),
None => this,
})
.when_some(model_info.icon, |this, icon| this.icon(icon))
.is_selected(is_selected)
.is_focused(selected)
.is_favorite(is_favorite)
.on_toggle_favorite(handle_action_click),
.when(supports_favorites, |this| {
this.is_favorite(is_favorite)
.on_toggle_favorite(handle_action_click)
}),
)
.into_any_element(),
)
@@ -417,7 +406,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
fn info_list_to_picker_entries(
model_list: AgentModelList,
favorites: &HashSet<ModelId>,
favorites: Arc<HashSet<ModelId>>,
) -> Vec<AcpModelPickerEntry> {
let mut entries = Vec::new();
@@ -583,11 +572,13 @@ mod tests {
}
}
fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
models
.into_iter()
.map(|m| ModelId::new(m.to_string()))
.collect()
fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
Arc::new(
models
.into_iter()
.map(|m| ModelId::new(m.to_string()))
.collect(),
)
}
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
@@ -610,6 +601,141 @@ mod tests {
.collect()
}
#[gpui::test]
fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
));
let model_ids = get_entry_model_ids(&entries);
assert_eq!(model_ids[0], "zed/gemini");
}
#[gpui::test]
fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
let favorites = create_favorites(vec![]);
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "zed"
));
}
#[gpui::test]
fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, favorites);
for entry in &entries {
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
if info.id.0.as_ref() == "zed/claude" {
assert!(is_favorite, "zed/claude should be a favorite");
} else {
assert!(!is_favorite, "{} should not be a favorite", info.id.0);
}
}
}
}
#[gpui::test]
fn test_favorites_appear_in_both_sections(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5", "openai/gpt-4"]),
]);
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
let entries = info_list_to_picker_entries(models, favorites);
let model_ids = get_entry_model_ids(&entries);
assert_eq!(model_ids[0], "zed/gemini");
assert_eq!(model_ids[1], "openai/gpt-5");
assert!(model_ids[2..].contains(&"zed/gemini"));
assert!(model_ids[2..].contains(&"openai/gpt-5"));
}
#[gpui::test]
fn test_favorites_are_not_duplicated_when_repeated_in_other_sections(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("Recommended", vec!["zed/claude", "anthropic/claude"]),
("Zed", vec!["zed/claude", "zed/gpt-5"]),
("Antropic", vec!["anthropic/claude"]),
("OpenAI", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, favorites);
let labels = get_entry_labels(&entries);
assert_eq!(
labels,
vec![
"Favorite",
"zed/claude",
"Recommended",
"zed/claude",
"anthropic/claude",
"Zed",
"zed/claude",
"zed/gpt-5",
"Antropic",
"anthropic/claude",
"OpenAI",
"openai/gpt-5"
]
);
}
#[gpui::test]
fn test_flat_model_list_with_favorites(_cx: &mut TestAppContext) {
let models = AgentModelList::Flat(vec![
acp_thread::AgentModelInfo {
id: acp::ModelId::new("zed/claude".to_string()),
name: "Claude".into(),
description: None,
icon: None,
},
acp_thread::AgentModelInfo {
id: acp::ModelId::new("zed/gemini".to_string()),
name: "Gemini".into(),
description: None,
icon: None,
},
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
));
assert!(entries.iter().any(|e| matches!(
e,
AcpModelPickerEntry::Separator(s) if s == "All"
)));
}
#[gpui::test]
async fn test_fuzzy_match(cx: &mut TestAppContext) {
let models = create_model_list(vec![
@@ -649,185 +775,4 @@ mod tests {
],
);
}
#[gpui::test]
fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, &favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
));
let model_ids = get_entry_model_ids(&entries);
assert_eq!(model_ids[0], "zed/gemini");
}
#[gpui::test]
fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
let favorites = create_favorites(vec![]);
let entries = info_list_to_picker_entries(models, &favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "zed"
));
}
#[gpui::test]
fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, &favorites);
for entry in &entries {
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
if info.id.0.as_ref() == "zed/claude" {
assert!(is_favorite, "zed/claude should be a favorite");
} else {
assert!(!is_favorite, "{} should not be a favorite", info.id.0);
}
}
}
}
#[gpui::test]
fn test_favorites_appear_in_both_sections(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5", "openai/gpt-4"]),
]);
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
let entries = info_list_to_picker_entries(models, &favorites);
let model_ids = get_entry_model_ids(&entries);
assert_eq!(model_ids[0], "zed/gemini");
assert_eq!(model_ids[1], "openai/gpt-5");
assert!(model_ids[2..].contains(&"zed/gemini"));
assert!(model_ids[2..].contains(&"openai/gpt-5"));
}
#[gpui::test]
fn test_favorites_are_not_duplicated_when_repeated_in_other_sections(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("Recommended", vec!["zed/claude", "anthropic/claude"]),
("Zed", vec!["zed/claude", "zed/gpt-5"]),
("Antropic", vec!["anthropic/claude"]),
("OpenAI", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, &favorites);
let labels = get_entry_labels(&entries);
assert_eq!(
labels,
vec![
"Favorite",
"zed/claude",
"Recommended",
"zed/claude",
"anthropic/claude",
"Zed",
"zed/claude",
"zed/gpt-5",
"Antropic",
"anthropic/claude",
"OpenAI",
"openai/gpt-5"
]
);
}
#[gpui::test]
fn test_flat_model_list_with_favorites(_cx: &mut TestAppContext) {
let models = AgentModelList::Flat(vec![
acp_thread::AgentModelInfo {
id: acp::ModelId::new("zed/claude".to_string()),
name: "Claude".into(),
description: None,
icon: None,
},
acp_thread::AgentModelInfo {
id: acp::ModelId::new("zed/gemini".to_string()),
name: "Gemini".into(),
description: None,
icon: None,
},
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, &favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
));
assert!(entries.iter().any(|e| matches!(
e,
AcpModelPickerEntry::Separator(s) if s == "All"
)));
}
#[gpui::test]
fn test_favorites_count_returns_correct_count(_cx: &mut TestAppContext) {
let empty_favorites: HashSet<ModelId> = HashSet::default();
assert_eq!(empty_favorites.len(), 0);
let one_favorite = create_favorites(vec!["model-a"]);
assert_eq!(one_favorite.len(), 1);
let multiple_favorites = create_favorites(vec!["model-a", "model-b", "model-c"]);
assert_eq!(multiple_favorites.len(), 3);
let with_duplicates = create_favorites(vec!["model-a", "model-a", "model-b"]);
assert_eq!(with_duplicates.len(), 2);
}
#[gpui::test]
fn test_is_favorite_flag_set_correctly_in_entries(_cx: &mut TestAppContext) {
let models = AgentModelList::Flat(vec![
acp_thread::AgentModelInfo {
id: acp::ModelId::new("favorite-model".to_string()),
name: "Favorite".into(),
description: None,
icon: None,
},
acp_thread::AgentModelInfo {
id: acp::ModelId::new("regular-model".to_string()),
name: "Regular".into(),
description: None,
icon: None,
},
]);
let favorites = create_favorites(vec!["favorite-model"]);
let entries = info_list_to_picker_entries(models, &favorites);
for entry in &entries {
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
if info.id.0.as_ref() == "favorite-model" {
assert!(*is_favorite, "favorite-model should have is_favorite=true");
} else if info.id.0.as_ref() == "regular-model" {
assert!(!*is_favorite, "regular-model should have is_favorite=false");
}
}
}
}
}

View File

@@ -1,14 +1,18 @@
use std::rc::Rc;
use std::sync::Arc;
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
use acp_thread::{AgentModelInfo, AgentModelSelector};
use agent_servers::AgentServer;
use agent_settings::AgentSettings;
use fs::Fs;
use gpui::{Entity, FocusHandle};
use picker::popover_menu::PickerPopoverMenu;
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
use settings::Settings as _;
use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
use zed_actions::agent::ToggleModelSelector;
use crate::CycleFavoriteModels;
use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
use crate::ui::ModelSelectorTooltip;
pub struct AcpModelSelectorPopover {
selector: Entity<AcpModelSelector>,
@@ -19,7 +23,7 @@ pub struct AcpModelSelectorPopover {
impl AcpModelSelectorPopover {
pub(crate) fn new(
selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn agent_servers::AgentServer>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<AcpModelSelector>,
focus_handle: FocusHandle,
@@ -60,14 +64,13 @@ impl AcpModelSelectorPopover {
impl Render for AcpModelSelectorPopover {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let selector = self.selector.read(cx);
let model = selector.delegate.active_model();
let model = self.selector.read(cx).delegate.active_model();
let model_name = model
.as_ref()
.map(|model| model.name.clone())
.unwrap_or_else(|| SharedString::from("Select a Model"));
let model_icon = model.as_ref().and_then(|model| model.icon.clone());
let model_icon = model.as_ref().and_then(|model| model.icon);
let focus_handle = self.focus_handle.clone();
@@ -77,13 +80,43 @@ impl Render for AcpModelSelectorPopover {
(Color::Muted, IconName::ChevronDown)
};
let show_cycle_row = selector.delegate.favorites_count() > 1;
let tooltip = Tooltip::element({
move |_, _cx| {
ModelSelectorTooltip::new(focus_handle.clone())
.show_cycle_row(show_cycle_row)
.into_any_element()
move |_, cx| {
let focus_handle = focus_handle.clone();
let should_show_cycle_row = !AgentSettings::get_global(cx)
.favorite_model_ids()
.is_empty();
v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Change Model"))
.child(KeyBinding::for_action_in(
&ToggleModelSelector,
&focus_handle,
cx,
)),
)
.when(should_show_cycle_row, |this| {
this.child(
h_flex()
.pt_1()
.gap_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.justify_between()
.child(Label::new("Cycle Favorited Models"))
.child(KeyBinding::for_action_in(
&CycleFavoriteModels,
&focus_handle,
cx,
)),
)
})
.into_any()
}
});
@@ -92,14 +125,7 @@ impl Render for AcpModelSelectorPopover {
ButtonLike::new("active-model")
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.when_some(model_icon, |this, icon| {
this.child(
match icon {
AgentModelIcon::Path(path) => Icon::from_external_svg(path),
AgentModelIcon::Named(icon_name) => Icon::new(icon_name),
}
.color(color)
.size(IconSize::XSmall),
)
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
})
.child(
Label::new(model_name)

View File

@@ -1,7 +1,7 @@
use crate::acp::AcpThreadView;
use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
use agent::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate;
use gpui::{
@@ -402,22 +402,7 @@ impl AcpThreadHistory {
let selected = ix == self.selected_index;
let hovered = Some(ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp();
let display_text = match format {
EntryTimeFormat::DateAndTime => {
let entry_time = entry.updated_at();
let now = Utc::now();
let duration = now.signed_duration_since(entry_time);
let days = duration.num_days();
format!("{}d", days)
}
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
};
let title = entry.title().clone();
let full_date =
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
h_flex()
.w_full()
@@ -438,14 +423,11 @@ impl AcpThreadHistory {
.truncate(),
)
.child(
Label::new(display_text)
Label::new(thread_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.tooltip(move |_, cx| {
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
})
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = Some(ix);

View File

@@ -34,7 +34,7 @@ use language::Buffer;
use language_model::LanguageModelRegistry;
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
use project::{Project, ProjectEntryId};
use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
@@ -47,9 +47,8 @@ use terminal_view::terminal_panel::TerminalPanel;
use text::Anchor;
use theme::{AgentFontSize, ThemeSettings};
use ui::{
Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider, DividerColor,
ElevationIndex, KeyBinding, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar,
prelude::*, right_click_menu,
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, NewTerminal, Workspace};
@@ -254,14 +253,13 @@ impl ThreadFeedbackState {
editor
});
editor.read(cx).focus_handle(cx).focus(window, cx);
editor.read(cx).focus_handle(cx).focus(window);
editor
}
}
pub struct AcpThreadView {
agent: Rc<dyn AgentServer>,
agent_server_store: Entity<AgentServerStore>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
thread_state: ThreadState,
@@ -339,13 +337,7 @@ impl AcpThreadView {
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let available_commands = Rc::new(RefCell::new(vec![]));
let agent_server_store = project.read(cx).agent_server_store().clone();
let agent_display_name = agent_server_store
.read(cx)
.agent_display_name(&ExternalAgentServerName(agent.name()))
.unwrap_or_else(|| agent.name());
let placeholder = placeholder_text(agent_display_name.as_ref(), false);
let placeholder = placeholder_text(agent.name().as_ref(), false);
let message_editor = cx.new(|cx| {
let mut editor = MessageEditor::new(
@@ -384,6 +376,7 @@ impl AcpThreadView {
)
});
let agent_server_store = project.read(cx).agent_server_store().clone();
let subscriptions = [
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
@@ -413,7 +406,6 @@ impl AcpThreadView {
Self {
agent: agent.clone(),
agent_server_store,
workspace: workspace.clone(),
project: project.clone(),
entry_view_state,
@@ -690,7 +682,7 @@ impl AcpThreadView {
})
});
this.message_editor.focus_handle(cx).focus(window, cx);
this.message_editor.focus_handle(cx).focus(window);
cx.notify();
}
@@ -745,7 +737,7 @@ impl AcpThreadView {
cx: &mut App,
) {
let agent_name = agent.name();
let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
let registry = LanguageModelRegistry::global(cx);
let sub = window.subscribe(&registry, cx, {
@@ -787,11 +779,12 @@ impl AcpThreadView {
configuration_view,
description: err
.description
.clone()
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
_subscription: subscription,
};
if this.message_editor.focus_handle(cx).is_focused(window) {
this.focus_handle.focus(window, cx)
this.focus_handle.focus(window)
}
cx.notify();
})
@@ -811,7 +804,7 @@ impl AcpThreadView {
ThreadState::LoadError(LoadError::Other(format!("{:#}", err).into()))
}
if self.message_editor.focus_handle(cx).is_focused(window) {
self.focus_handle.focus(window, cx)
self.focus_handle.focus(window)
}
cx.notify();
}
@@ -1095,7 +1088,10 @@ impl AcpThreadView {
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired::new(),
AuthRequired {
description: None,
provider_id: None,
},
agent,
connection,
window,
@@ -1274,7 +1270,7 @@ impl AcpThreadView {
}
})
};
self.focus_handle(cx).focus(window, cx);
self.focus_handle(cx).focus(window);
cx.notify();
}
@@ -1326,7 +1322,7 @@ impl AcpThreadView {
.await?;
this.update_in(cx, |this, window, cx| {
this.send_impl(message_editor, window, cx);
this.focus_handle(cx).focus(window, cx);
this.focus_handle(cx).focus(window);
})?;
anyhow::Ok(())
})
@@ -1469,7 +1465,7 @@ impl AcpThreadView {
self.thread_retry_status.take();
self.thread_state = ThreadState::LoadError(error.clone());
if self.message_editor.focus_handle(cx).is_focused(window) {
self.focus_handle.focus(window, cx)
self.focus_handle.focus(window)
}
}
AcpThreadEvent::TitleUpdated => {
@@ -1504,13 +1500,7 @@ impl AcpThreadView {
let has_commands = !available_commands.is_empty();
self.available_commands.replace(available_commands);
let agent_display_name = self
.agent_server_store
.read(cx)
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
.unwrap_or_else(|| self.agent.name());
let new_placeholder = placeholder_text(agent_display_name.as_ref(), has_commands);
let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands);
self.message_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(&new_placeholder, window, cx);
@@ -1673,6 +1663,44 @@ impl AcpThreadView {
});
return;
}
} else if method.0.as_ref() == "anthropic-api-key" {
let registry = LanguageModelRegistry::global(cx);
let provider = registry
.read(cx)
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
.unwrap();
let this = cx.weak_entity();
let agent = self.agent.clone();
let connection = connection.clone();
window.defer(cx, move |window, cx| {
if !provider.is_authenticated(cx) {
Self::handle_auth_required(
this,
AuthRequired {
description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
},
agent,
connection,
window,
cx,
);
} else {
this.update(cx, |this, cx| {
this.thread_state = Self::initial_state(
agent,
None,
this.workspace.clone(),
this.project.clone(),
true,
window,
cx,
)
})
.ok();
}
});
return;
} else if method.0.as_ref() == "vertex-ai"
&& std::env::var("GOOGLE_API_KEY").is_err()
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
@@ -1870,17 +1898,6 @@ impl AcpThreadView {
})
}
pub fn has_user_submitted_prompt(&self, cx: &App) -> bool {
self.thread().is_some_and(|thread| {
thread.read(cx).entries().iter().any(|entry| {
matches!(
entry,
AgentThreadEntry::UserMessage(user_message) if user_message.id.is_some()
)
})
})
}
fn authorize_tool_call(
&mut self,
tool_call_id: acp::ToolCallId,
@@ -2039,7 +2056,7 @@ impl AcpThreadView {
}
})
.text_xs()
.child(editor.clone().into_any_element())
.child(editor.clone().into_any_element()),
)
.when(editor_focus, |this| {
let base_container = h_flex()
@@ -2125,7 +2142,6 @@ impl AcpThreadView {
chunks,
indented: _,
}) => {
let mut is_blank = true;
let is_last = entry_ix + 1 == total_entries;
let style = default_markdown_style(false, false, window, cx);
@@ -2135,54 +2151,36 @@ impl AcpThreadView {
.children(chunks.iter().enumerate().filter_map(
|(chunk_ix, chunk)| match chunk {
AssistantMessageChunk::Message { block } => {
block.markdown().and_then(|md| {
let this_is_blank = md.read(cx).source().trim().is_empty();
is_blank = is_blank && this_is_blank;
if this_is_blank {
return None;
}
Some(
self.render_markdown(md.clone(), style.clone())
.into_any_element(),
)
block.markdown().map(|md| {
self.render_markdown(md.clone(), style.clone())
.into_any_element()
})
}
AssistantMessageChunk::Thought { block } => {
block.markdown().and_then(|md| {
let this_is_blank = md.read(cx).source().trim().is_empty();
is_blank = is_blank && this_is_blank;
if this_is_blank {
return None;
}
Some(
self.render_thinking_block(
entry_ix,
chunk_ix,
md.clone(),
window,
cx,
)
.into_any_element(),
block.markdown().map(|md| {
self.render_thinking_block(
entry_ix,
chunk_ix,
md.clone(),
window,
cx,
)
.into_any_element()
})
}
},
))
.into_any();
if is_blank {
Empty.into_any()
} else {
v_flex()
.px_5()
.py_1p5()
.when(is_last, |this| this.pb_4())
.w_full()
.text_ui(cx)
.child(self.render_message_context_menu(entry_ix, message_body, cx))
.into_any()
}
v_flex()
.px_5()
.py_1p5()
.when(is_first_indented, |this| this.pt_0p5())
.when(is_last, |this| this.pb_4())
.w_full()
.text_ui(cx)
.child(message_body)
.into_any()
}
AgentThreadEntry::ToolCall(tool_call) => {
let has_terminals = tool_call.terminals().next().is_some();
@@ -2214,7 +2212,7 @@ impl AcpThreadView {
div()
.relative()
.w_full()
.pl_5()
.pl(rems_from_px(20.0))
.bg(cx.theme().colors().panel_background.opacity(0.2))
.child(
div()
@@ -2287,70 +2285,6 @@ impl AcpThreadView {
}
}
fn render_message_context_menu(
&self,
entry_ix: usize,
message_body: AnyElement,
cx: &Context<Self>,
) -> AnyElement {
let entity = cx.entity();
let workspace = self.workspace.clone();
right_click_menu(format!("agent_context_menu-{}", entry_ix))
.trigger(move |_, _, _| message_body)
.menu(move |window, cx| {
let focus = window.focused(cx);
let entity = entity.clone();
let workspace = workspace.clone();
ContextMenu::build(window, cx, move |menu, _, cx| {
let is_at_top = entity.read(cx).list_state.logical_scroll_top().item_ix == 0;
let scroll_item = if is_at_top {
ContextMenuEntry::new("Scroll to Bottom").handler({
let entity = entity.clone();
move |_, cx| {
entity.update(cx, |this, cx| {
this.scroll_to_bottom(cx);
});
}
})
} else {
ContextMenuEntry::new("Scroll to Top").handler({
let entity = entity.clone();
move |_, cx| {
entity.update(cx, |this, cx| {
this.scroll_to_top(cx);
});
}
})
};
let open_thread_as_markdown = ContextMenuEntry::new("Open Thread as Markdown")
.handler({
let entity = entity.clone();
let workspace = workspace.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
entity
.update(cx, |this, cx| {
this.open_thread_as_markdown(workspace, window, cx)
})
.detach_and_log_err(cx);
}
}
});
menu.when_some(focus, |menu, focus| menu.context(focus))
.action("Copy", Box::new(markdown::CopyAsMarkdown))
.separator()
.item(scroll_item)
.item(open_thread_as_markdown)
})
})
.into_any_element()
}
fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
cx.theme()
.colors()
@@ -2495,12 +2429,6 @@ impl AcpThreadView {
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
let input_output_header = |label: SharedString| {
Label::new(label)
.size(LabelSize::XSmall)
.color(Color::Muted)
.buffer_font(cx)
};
let tool_output_display =
if is_open {
@@ -2542,40 +2470,18 @@ impl AcpThreadView {
| ToolCallStatus::Completed
| ToolCallStatus::Failed
| ToolCallStatus::Canceled => v_flex()
.when(!is_edit && !is_terminal_tool, |this| {
this.mt_1p5().w_full().child(
v_flex()
.ml(rems(0.4))
.px_3p5()
.pb_1()
.gap_1()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
.child(input_output_header("Raw Input:".into()))
.children(tool_call.raw_input_markdown.clone().map(|input| {
div().id(("tool-call-raw-input-markdown", entry_ix)).child(
self.render_markdown(
input,
default_markdown_style(false, false, window, cx),
),
)
}))
.child(input_output_header("Output:".into())),
)
})
.w_full()
.children(tool_call.content.iter().enumerate().map(
|(content_ix, content)| {
div().id(("tool-call-output", entry_ix)).child(
self.render_tool_call_content(
entry_ix,
content,
content_ix,
tool_call,
use_card_layout,
window,
cx,
),
)
div().child(self.render_tool_call_content(
entry_ix,
content,
content_ix,
tool_call,
use_card_layout,
window,
cx,
))
},
))
.into_any(),
@@ -2663,7 +2569,7 @@ impl AcpThreadView {
.gap_px()
.when(is_collapsible, |this| {
this.child(
Disclosure::new(("expand-output", entry_ix), is_open)
Disclosure::new(("expand", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.visible_on_hover(&card_header_id)
@@ -2786,7 +2692,7 @@ impl AcpThreadView {
..default_markdown_style(false, true, window, cx)
},
))
.tooltip(Tooltip::text("Go to File"))
.tooltip(Tooltip::text("Jump to File"))
.on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx);
}))
@@ -2849,20 +2755,20 @@ impl AcpThreadView {
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
v_flex()
.mt_1p5()
.gap_2()
.map(|this| {
if card_layout {
this.when(context_ix > 0, |this| {
this.pt_2()
.border_t_1()
.border_color(self.tool_card_border_color(cx))
})
} else {
this.ml(rems(0.4))
.px_3p5()
.border_l_1()
.when(!card_layout, |this| {
this.ml(rems(0.4))
.px_3p5()
.border_l_1()
.border_color(self.tool_card_border_color(cx))
})
.when(card_layout, |this| {
this.px_2().pb_2().when(context_ix > 0, |this| {
this.border_t_1()
.pt_2()
.border_color(self.tool_card_border_color(cx))
}
})
})
.text_xs()
.text_color(cx.theme().colors().text_muted)
@@ -3583,119 +3489,138 @@ impl AcpThreadView {
pending_auth_method: Option<&acp::AuthMethodId>,
window: &mut Window,
cx: &Context<Self>,
) -> impl IntoElement {
) -> Div {
let show_description =
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
let auth_methods = connection.auth_methods();
let agent_display_name = self
.agent_server_store
.read(cx)
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
.unwrap_or_else(|| self.agent.name());
let show_fallback_description = auth_methods.len() > 1
&& configuration_view.is_none()
&& description.is_none()
&& pending_auth_method.is_none();
let auth_buttons = || {
h_flex().justify_end().flex_wrap().gap_1().children(
connection
.auth_methods()
.iter()
.enumerate()
.rev()
.map(|(ix, method)| {
let (method_id, name) = if self.project.read(cx).is_via_remote_server()
&& method.id.0.as_ref() == "oauth-personal"
&& method.name == "Log in with Google"
{
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
} else {
(method.id.0.clone(), method.name.clone())
};
let agent_telemetry_id = connection.telemetry_id();
Button::new(method_id.clone(), name)
.label_size(LabelSize::Small)
.map(|this| {
if ix == 0 {
this.style(ButtonStyle::Tinted(TintColor::Accent))
} else {
this.style(ButtonStyle::Outlined)
}
})
.when_some(method.description.clone(), |this, description| {
this.tooltip(Tooltip::text(description))
})
.on_click({
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = agent_telemetry_id,
method = method_id
);
this.authenticate(
acp::AuthMethodId::new(method_id.clone()),
window,
cx,
)
})
})
}),
)
};
if pending_auth_method.is_some() {
return Callout::new()
.icon(IconName::Info)
.title(format!("Authenticating to {}", agent_display_name))
.actions_slot(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_rotate_animation(2)
.into_any_element(),
v_flex().flex_1().size_full().justify_end().child(
v_flex()
.p_2()
.pr_3()
.w_full()
.gap_1()
.border_t_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().status().warning.opacity(0.04))
.child(
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::Warning)
.color(Color::Warning)
.size(IconSize::Small),
)
.child(Label::new("Authentication Required").size(LabelSize::Small)),
)
.into_any_element();
}
.children(description.map(|desc| {
div().text_ui(cx).child(self.render_markdown(
desc.clone(),
default_markdown_style(false, false, window, cx),
))
}))
.children(
configuration_view
.cloned()
.map(|view| div().w_full().child(view)),
)
.when(show_description, |el| {
el.child(
Label::new(format!(
"You are not currently authenticated with {}.{}",
self.agent.name(),
if auth_methods.len() > 1 {
" Please choose one of the following options:"
} else {
""
}
))
.size(LabelSize::Small)
.color(Color::Muted)
.mb_1()
.ml_5(),
)
})
.when_some(pending_auth_method, |el, _| {
el.child(
h_flex()
.py_4()
.w_full()
.justify_center()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_rotate_animation(2),
)
.child(Label::new("Authenticating…").size(LabelSize::Small)),
)
})
.when(!auth_methods.is_empty(), |this| {
this.child(
h_flex()
.justify_end()
.flex_wrap()
.gap_1()
.when(!show_description, |this| {
this.border_t_1()
.mt_1()
.pt_2()
.border_color(cx.theme().colors().border.opacity(0.8))
})
.children(connection.auth_methods().iter().enumerate().rev().map(
|(ix, method)| {
let (method_id, name) = if self
.project
.read(cx)
.is_via_remote_server()
&& method.id.0.as_ref() == "oauth-personal"
&& method.name == "Log in with Google"
{
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
} else {
(method.id.0.clone(), method.name.clone())
};
Callout::new()
.icon(IconName::Info)
.title(format!("Authenticate to {}", agent_display_name))
.when(auth_methods.len() == 1, |this| {
this.actions_slot(auth_buttons())
})
.description_slot(
v_flex()
.text_ui(cx)
.map(|this| {
if show_fallback_description {
this.child(
Label::new("Choose one of the following authentication options:")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else {
this.children(
configuration_view
.cloned()
.map(|view| div().w_full().child(view)),
)
.children(description.map(|desc| {
self.render_markdown(
desc.clone(),
default_markdown_style(false, false, window, cx),
)
}))
}
})
.when(auth_methods.len() > 1, |this| {
this.gap_1().child(auth_buttons())
}),
)
.into_any_element()
let agent_telemetry_id = connection.telemetry_id();
Button::new(method_id.clone(), name)
.label_size(LabelSize::Small)
.map(|this| {
if ix == 0 {
this.style(ButtonStyle::Tinted(TintColor::Warning))
} else {
this.style(ButtonStyle::Outlined)
}
})
.when_some(
method.description.clone(),
|this, description| {
this.tooltip(Tooltip::text(description))
},
)
.on_click({
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = agent_telemetry_id,
method = method_id
);
this.authenticate(
acp::AuthMethodId::new(method_id.clone()),
window,
cx,
)
})
})
},
)),
)
}),
)
}
fn render_load_error(
@@ -4185,8 +4110,6 @@ impl AcpThreadView {
.ml_1p5()
});
let full_path = path.display(path_style).to_string();
let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
@@ -4220,6 +4143,7 @@ impl AcpThreadView {
.relative()
.pr_8()
.w_full()
.overflow_x_scroll()
.child(
h_flex()
.id(("file-name-path", index))
@@ -4231,14 +4155,7 @@ impl AcpThreadView {
.child(file_icon)
.children(file_name)
.children(file_path)
.tooltip(move |_, cx| {
Tooltip::with_meta(
"Go to File",
None,
full_path.clone(),
cx,
)
})
.tooltip(Tooltip::text("Go to File"))
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
@@ -4352,6 +4269,37 @@ impl AcpThreadView {
v_flex()
.on_action(cx.listener(Self::expand_message_editor))
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
if let Some(profile_selector) = this.profile_selector.as_ref() {
profile_selector.read(cx).menu_handle().toggle(window, cx);
} else if let Some(mode_selector) = this.mode_selector() {
mode_selector.read(cx).menu_handle().toggle(window, cx);
}
}))
.on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
if let Some(profile_selector) = this.profile_selector.as_ref() {
profile_selector.update(cx, |profile_selector, cx| {
profile_selector.cycle_profile(cx);
});
} else if let Some(mode_selector) = this.mode_selector() {
mode_selector.update(cx, |mode_selector, cx| {
mode_selector.cycle_mode(window, cx);
});
}
}))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
if let Some(model_selector) = this.model_selector.as_ref() {
model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
}
}))
.on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
if let Some(model_selector) = this.model_selector.as_ref() {
model_selector.update(cx, |model_selector, cx| {
model_selector.cycle_favorite_models(window, cx);
});
}
}))
.p_2()
.gap_2()
.border_t_1()
@@ -5913,6 +5861,10 @@ impl AcpThreadView {
};
let connection = thread.read(cx).connection().clone();
let err = AuthRequired {
description: None,
provider_id: None,
};
this.clear_thread_error(cx);
if let Some(message) = this.in_flight_prompt.take() {
this.message_editor.update(cx, |editor, cx| {
@@ -5921,14 +5873,7 @@ impl AcpThreadView {
}
let this = cx.weak_entity();
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired::new(),
agent,
connection,
window,
cx,
);
Self::handle_auth_required(this, err, agent, connection, window, cx);
})
}
}))
@@ -5941,10 +5886,14 @@ impl AcpThreadView {
};
let connection = thread.read(cx).connection().clone();
let err = AuthRequired {
description: None,
provider_id: None,
};
self.clear_thread_error(cx);
let this = cx.weak_entity();
window.defer(cx, |window, cx| {
Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx);
Self::handle_auth_required(this, err, agent, connection, window, cx);
})
}
@@ -6038,37 +5987,6 @@ impl Render for AcpThreadView {
.on_action(cx.listener(Self::allow_always))
.on_action(cx.listener(Self::allow_once))
.on_action(cx.listener(Self::reject_once))
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
if let Some(profile_selector) = this.profile_selector.as_ref() {
profile_selector.read(cx).menu_handle().toggle(window, cx);
} else if let Some(mode_selector) = this.mode_selector() {
mode_selector.read(cx).menu_handle().toggle(window, cx);
}
}))
.on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
if let Some(profile_selector) = this.profile_selector.as_ref() {
profile_selector.update(cx, |profile_selector, cx| {
profile_selector.cycle_profile(cx);
});
} else if let Some(mode_selector) = this.mode_selector() {
mode_selector.update(cx, |mode_selector, cx| {
mode_selector.cycle_mode(window, cx);
});
}
}))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
if let Some(model_selector) = this.model_selector.as_ref() {
model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
}
}))
.on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
if let Some(model_selector) = this.model_selector.as_ref() {
model_selector.update(cx, |model_selector, cx| {
model_selector.cycle_favorite_models(window, cx);
});
}
}))
.track_focus(&self.focus_handle)
.bg(cx.theme().colors().panel_background)
.child(match &self.thread_state {
@@ -6078,19 +5996,16 @@ impl Render for AcpThreadView {
configuration_view,
pending_auth_method,
..
} => v_flex()
.flex_1()
.size_full()
.justify_end()
.child(self.render_auth_required_state(
} => self
.render_auth_required_state(
connection,
description.as_ref(),
configuration_view.as_ref(),
pending_auth_method.as_ref(),
window,
cx,
))
.into_any_element(),
)
.into_any(),
ThreadState::Loading { .. } => v_flex()
.flex_1()
.child(self.render_recent_history(cx))

View File

@@ -22,8 +22,7 @@ use gpui::{
};
use language::LanguageRegistry;
use language_model::{
IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
ZED_CLOUD_PROVIDER_ID,
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
use language_models::AllLanguageModelSettings;
use notifications::status_toast::{StatusToast, ToastIcon};
@@ -118,7 +117,7 @@ impl AgentConfiguration {
}
fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
let providers = LanguageModelRegistry::read_global(cx).providers();
for provider in providers {
self.add_provider_configuration_view(&provider, window, cx);
}
@@ -262,12 +261,9 @@ impl AgentConfiguration {
.w_full()
.gap_1p5()
.child(
match provider.icon() {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.size(IconSize::Small)
.color(Color::Muted),
Icon::new(provider.icon())
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
@@ -420,7 +416,7 @@ impl AgentConfiguration {
&mut self,
cx: &mut Context<Self>,
) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
let providers = LanguageModelRegistry::read_global(cx).providers();
let popover_menu = PopoverMenu::new("add-provider-popover")
.trigger(
@@ -1370,7 +1366,6 @@ async fn open_new_agent_servers_entry_in_settings_editor(
env: Some(HashMap::default()),
default_mode: None,
default_model: None,
favorite_models: vec![],
},
);
}

View File

@@ -446,17 +446,17 @@ impl AddLlmProviderModal {
})
}
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
window.focus_next(cx);
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
}
fn on_tab_prev(
&mut self,
_: &menu::SelectPrevious,
window: &mut Window,
cx: &mut Context<Self>,
_: &mut Context<Self>,
) {
window.focus_prev(cx);
window.focus_prev();
}
}
@@ -493,7 +493,7 @@ impl Render for AddLlmProviderModal {
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window, cx);
this.focus_handle(cx).focus(window);
}))
.child(
Modal::new("configure-context-server", None)

View File

@@ -831,7 +831,7 @@ impl Render for ConfigureContextServerModal {
}),
)
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window, cx);
this.focus_handle(cx).focus(window);
}))
.child(
Modal::new("configure-context-server", None)

View File

@@ -156,7 +156,7 @@ impl ManageProfilesModal {
cx.observe_global_in::<SettingsStore>(window, |this, window, cx| {
if matches!(this.mode, Mode::ChooseProfile(_)) {
this.mode = Mode::choose_profile(window, cx);
this.focus_handle(cx).focus(window, cx);
this.focus_handle(cx).focus(window);
cx.notify();
}
});
@@ -173,7 +173,7 @@ impl ManageProfilesModal {
fn choose_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.mode = Mode::choose_profile(window, cx);
self.focus_handle(cx).focus(window, cx);
self.focus_handle(cx).focus(window);
}
fn new_profile(
@@ -191,7 +191,7 @@ impl ManageProfilesModal {
name_editor,
base_profile_id,
});
self.focus_handle(cx).focus(window, cx);
self.focus_handle(cx).focus(window);
}
pub fn view_profile(
@@ -209,7 +209,7 @@ impl ManageProfilesModal {
delete_profile: NavigableEntry::focusable(cx),
cancel_item: NavigableEntry::focusable(cx),
});
self.focus_handle(cx).focus(window, cx);
self.focus_handle(cx).focus(window);
}
fn configure_default_model(
@@ -300,7 +300,7 @@ impl ManageProfilesModal {
model_picker,
_subscription: dismiss_subscription,
};
self.focus_handle(cx).focus(window, cx);
self.focus_handle(cx).focus(window);
}
fn configure_mcp_tools(
@@ -336,7 +336,7 @@ impl ManageProfilesModal {
tool_picker,
_subscription: dismiss_subscription,
};
self.focus_handle(cx).focus(window, cx);
self.focus_handle(cx).focus(window);
}
fn configure_builtin_tools(
@@ -377,7 +377,7 @@ impl ManageProfilesModal {
tool_picker,
_subscription: dismiss_subscription,
};
self.focus_handle(cx).focus(window, cx);
self.focus_handle(cx).focus(window);
}
fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -951,7 +951,7 @@ impl Render for ManageProfilesModal {
.on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window, cx);
this.focus_handle(cx).focus(window);
}))
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
.child(match &self.mode {

View File

@@ -17,7 +17,7 @@ use gpui::{
Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
};
use language::{Buffer, Capability, OffsetRangeExt, Point};
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
use multi_buffer::PathKey;
use project::{Project, ProjectItem, ProjectPath};
use settings::{Settings, SettingsStore};
@@ -192,7 +192,7 @@ impl AgentDiffPane {
&& buffer
.read(cx)
.file()
.is_some_and(|file| file.disk_state().is_deleted())
.is_some_and(|file| file.disk_state() == DiskState::Deleted)
{
editor.fold_buffer(snapshot.text.remote_id(), cx)
}
@@ -212,10 +212,10 @@ impl AgentDiffPane {
.focus_handle(cx)
.contains_focused(window, cx)
{
self.focus_handle.focus(window, cx);
self.focus_handle.focus(window);
} else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
self.editor.update(cx, |editor, cx| {
editor.focus_handle(cx).focus(window, cx);
editor.focus_handle(cx).focus(window);
});
}
}
@@ -874,12 +874,12 @@ impl AgentDiffToolbar {
match active_item {
AgentDiffToolbarItem::Pane(agent_diff) => {
if let Some(agent_diff) = agent_diff.upgrade() {
agent_diff.focus_handle(cx).focus(window, cx);
agent_diff.focus_handle(cx).focus(window);
}
}
AgentDiffToolbarItem::Editor { editor, .. } => {
if let Some(editor) = editor.upgrade() {
editor.read(cx).focus_handle(cx).focus(window, cx);
editor.read(cx).focus_handle(cx).focus(window);
}
}
}

View File

@@ -1,15 +1,14 @@
use crate::{
ModelUsageContext,
language_model_selector::{LanguageModelSelector, language_model_selector},
ui::ModelSelectorTooltip,
};
use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use language_model::IconOrSvg;
use picker::popover_menu::PickerPopoverMenu;
use settings::update_settings_file;
use std::sync::Arc;
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
use zed_actions::agent::ToggleModelSelector;
pub struct AgentModelSelector {
selector: Entity<LanguageModelSelector>,
@@ -81,12 +80,6 @@ impl AgentModelSelector {
pub fn active_model(&self, cx: &App) -> Option<language_model::ConfiguredModel> {
self.selector.read(cx).delegate.active_model(cx)
}
pub fn cycle_favorite_models(&self, window: &mut Window, cx: &mut Context<Self>) {
self.selector.update(cx, |selector, cx| {
selector.delegate.cycle_favorite_models(window, cx);
});
}
}
impl Render for AgentModelSelector {
@@ -104,30 +97,13 @@ impl Render for AgentModelSelector {
Color::Muted
};
let show_cycle_row = self.selector.read(cx).delegate.favorites_count() > 1;
let focus_handle = self.focus_handle.clone();
let tooltip = Tooltip::element({
move |_, _cx| {
ModelSelectorTooltip::new(focus_handle.clone())
.show_cycle_row(show_cycle_row)
.into_any_element()
}
});
PickerPopoverMenu::new(
self.selector.clone(),
ButtonLike::new("active-model")
.when_some(provider_icon, |this, icon| {
this.child(
match icon {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.color(color)
.size(IconSize::XSmall),
)
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
})
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.child(
@@ -139,9 +115,11 @@ impl Render for AgentModelSelector {
.child(
Icon::new(IconName::ChevronDown)
.color(color)
.size(IconSize::XSmall),
.size(IconSize::Small),
),
tooltip,
move |_window, cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
},
gpui::Corner::TopRight,
cx,
)

View File

@@ -7,6 +7,7 @@ use db::kvp::{Dismissable, KEY_VALUE_STORE};
use project::{
ExternalAgentServerName,
agent_server_store::{CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, wait_for_workspace_trust},
};
use serde::{Deserialize, Serialize};
use settings::{
@@ -263,6 +264,17 @@ impl AgentType {
Self::Custom { .. } => Some(IconName::Sparkle),
}
}
fn is_mcp(&self) -> bool {
match self {
Self::NativeAgent => false,
Self::TextThread => false,
Self::Custom { .. } => false,
Self::Gemini => true,
Self::ClaudeCode => true,
Self::Codex => true,
}
}
}
impl From<ExternalAgent> for AgentType {
@@ -443,7 +455,9 @@ pub struct AgentPanel {
pending_serialization: Option<Task<Result<()>>>,
onboarding: Entity<AgentPanelOnboarding>,
selected_agent: AgentType,
new_agent_thread_task: Task<()>,
show_trust_workspace_message: bool,
_worktree_trust_subscription: Option<Subscription>,
}
impl AgentPanel {
@@ -667,6 +681,48 @@ impl AgentPanel {
None
};
let mut show_trust_workspace_message = false;
let worktree_trust_subscription =
TrustedWorktrees::try_get_global(cx).and_then(|trusted_worktrees| {
let has_global_trust = trusted_worktrees.update(cx, |trusted_worktrees, cx| {
trusted_worktrees.can_trust_workspace(
project
.read(cx)
.remote_connection_options(cx)
.map(RemoteHostLocation::from),
cx,
)
});
if has_global_trust {
None
} else {
show_trust_workspace_message = true;
let project = project.clone();
Some(cx.subscribe(
&trusted_worktrees,
move |agent_panel, trusted_worktrees, _, cx| {
let new_show_trust_workspace_message =
!trusted_worktrees.update(cx, |trusted_worktrees, cx| {
trusted_worktrees.can_trust_workspace(
project
.read(cx)
.remote_connection_options(cx)
.map(RemoteHostLocation::from),
cx,
)
});
if new_show_trust_workspace_message
!= agent_panel.show_trust_workspace_message
{
agent_panel.show_trust_workspace_message =
new_show_trust_workspace_message;
cx.notify();
};
},
))
}
});
let mut panel = Self {
active_view,
workspace,
@@ -689,12 +745,14 @@ impl AgentPanel {
height: None,
zoomed: false,
pending_serialization: None,
new_agent_thread_task: Task::ready(()),
onboarding,
acp_history,
history_store,
selected_agent: AgentType::default(),
loading: false,
show_trust_workspace_message: false,
show_trust_workspace_message,
_worktree_trust_subscription: worktree_trust_subscription,
};
// Initial sync of agent servers from extensions
@@ -822,7 +880,7 @@ impl AgentPanel {
window,
cx,
);
text_thread_editor.focus_handle(cx).focus(window, cx);
text_thread_editor.focus_handle(cx).focus(window);
}
fn external_thread(
@@ -887,6 +945,47 @@ impl AgentPanel {
}
};
if ext_agent.is_mcp() {
let wait_task = this.update(cx, |agent_panel, cx| {
agent_panel.project.update(cx, |project, cx| {
wait_for_workspace_trust(
project.remote_connection_options(cx),
"context servers",
cx,
)
})
})?;
if let Some(wait_task) = wait_task {
this.update_in(cx, |agent_panel, window, cx| {
agent_panel.show_trust_workspace_message = true;
cx.notify();
agent_panel.new_agent_thread_task =
cx.spawn_in(window, async move |agent_panel, cx| {
wait_task.await;
let server = ext_agent.server(fs, history);
agent_panel
.update_in(cx, |agent_panel, window, cx| {
agent_panel.show_trust_workspace_message = false;
cx.notify();
agent_panel._external_thread(
server,
resume_thread,
summarize_thread,
workspace,
project,
loading,
ext_agent,
window,
cx,
);
})
.ok();
});
})?;
return Ok(());
}
}
let server = ext_agent.server(fs, history);
this.update_in(cx, |agent_panel, window, cx| {
agent_panel._external_thread(
@@ -935,7 +1034,7 @@ impl AgentPanel {
if let Some(thread_view) = self.active_thread_view() {
thread_view.update(cx, |view, cx| {
view.expand_message_editor(&ExpandMessageEditor, window, cx);
view.focus_handle(cx).focus(window, cx);
view.focus_handle(cx).focus(window);
});
}
}
@@ -1016,12 +1115,12 @@ impl AgentPanel {
match &self.active_view {
ActiveView::ExternalAgentThread { thread_view } => {
thread_view.focus_handle(cx).focus(window, cx);
thread_view.focus_handle(cx).focus(window);
}
ActiveView::TextThread {
text_thread_editor, ..
} => {
text_thread_editor.focus_handle(cx).focus(window, cx);
text_thread_editor.focus_handle(cx).focus(window);
}
ActiveView::History | ActiveView::Configuration => {}
}
@@ -1169,7 +1268,7 @@ impl AgentPanel {
Self::handle_agent_configuration_event,
));
configuration.focus_handle(cx).focus(window, cx);
configuration.focus_handle(cx).focus(window);
}
}
@@ -1305,7 +1404,7 @@ impl AgentPanel {
}
if focus {
self.focus_handle(cx).focus(window, cx);
self.focus_handle(cx).focus(window);
}
}
@@ -1411,6 +1510,36 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
let wait_task = if agent.is_mcp() {
self.project.update(cx, |project, cx| {
wait_for_workspace_trust(
project.remote_connection_options(cx),
"context servers",
cx,
)
})
} else {
None
};
if let Some(wait_task) = wait_task {
self.show_trust_workspace_message = true;
cx.notify();
self.new_agent_thread_task = cx.spawn_in(window, async move |agent_panel, cx| {
wait_task.await;
agent_panel
.update_in(cx, |agent_panel, window, cx| {
agent_panel.show_trust_workspace_message = false;
cx.notify();
agent_panel._new_agent_thread(agent, window, cx);
})
.ok();
});
} else {
self._new_agent_thread(agent, window, cx);
}
}
fn _new_agent_thread(&mut self, agent: AgentType, window: &mut Window, cx: &mut Context<Self>) {
match agent {
AgentType::TextThread => {
window.dispatch_action(NewTextThread.boxed_clone(), cx);
@@ -1620,19 +1749,14 @@ impl AgentPanel {
let content = match &self.active_view {
ActiveView::ExternalAgentThread { thread_view } => {
let is_generating_title = thread_view
.read(cx)
.as_native_thread(cx)
.map_or(false, |t| t.read(cx).is_generating_title());
if let Some(title_editor) = thread_view.read(cx).title_editor() {
let container = div()
div()
.w_full()
.on_action({
let thread_view = thread_view.downgrade();
move |_: &menu::Confirm, window, cx| {
if let Some(thread_view) = thread_view.upgrade() {
thread_view.focus_handle(cx).focus(window, cx);
thread_view.focus_handle(cx).focus(window);
}
}
})
@@ -1640,25 +1764,12 @@ impl AgentPanel {
let thread_view = thread_view.downgrade();
move |_: &editor::actions::Cancel, window, cx| {
if let Some(thread_view) = thread_view.upgrade() {
thread_view.focus_handle(cx).focus(window, cx);
thread_view.focus_handle(cx).focus(window);
}
}
})
.child(title_editor);
if is_generating_title {
container
.with_animation(
"generating_title",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|div, delta| div.opacity(delta),
)
.into_any_element()
} else {
container.into_any_element()
}
.child(title_editor)
.into_any_element()
} else {
Label::new(thread_view.read(cx).title(cx))
.color(Color::Muted)
@@ -1688,13 +1799,6 @@ impl AgentPanel {
Label::new(LOADING_SUMMARY_PLACEHOLDER)
.truncate()
.color(Color::Muted)
.with_animation(
"generating_title",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
)
.into_any_element()
}
}
@@ -1738,25 +1842,6 @@ impl AgentPanel {
.into_any()
}
fn handle_regenerate_thread_title(thread_view: Entity<AcpThreadView>, cx: &mut App) {
thread_view.update(cx, |thread_view, cx| {
if let Some(thread) = thread_view.as_native_thread(cx) {
thread.update(cx, |thread, cx| {
thread.generate_title(cx);
});
}
});
}
fn handle_regenerate_text_thread_title(
text_thread_editor: Entity<TextThreadEditor>,
cx: &mut App,
) {
text_thread_editor.update(cx, |text_thread_editor, cx| {
text_thread_editor.regenerate_summary(cx);
});
}
fn render_panel_options_menu(
&self,
window: &mut Window,
@@ -1776,35 +1861,6 @@ impl AgentPanel {
let selected_agent = self.selected_agent.clone();
let text_thread_view = match &self.active_view {
ActiveView::TextThread {
text_thread_editor, ..
} => Some(text_thread_editor.clone()),
_ => None,
};
let text_thread_with_messages = match &self.active_view {
ActiveView::TextThread {
text_thread_editor, ..
} => text_thread_editor
.read(cx)
.text_thread()
.read(cx)
.messages(cx)
.any(|message| message.role == language_model::Role::Assistant),
_ => false,
};
let thread_view = match &self.active_view {
ActiveView::ExternalAgentThread { thread_view } => Some(thread_view.clone()),
_ => None,
};
let thread_with_messages = match &self.active_view {
ActiveView::ExternalAgentThread { thread_view } => {
thread_view.read(cx).has_user_submitted_prompt(cx)
}
_ => false,
};
PopoverMenu::new("agent-options-menu")
.trigger_with_tooltip(
IconButton::new("agent-options-menu", IconName::Ellipsis)
@@ -1827,7 +1883,6 @@ impl AgentPanel {
move |window, cx| {
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
menu = menu.context(focus_handle.clone());
if let Some(usage) = usage {
menu = menu
.header_with_link("Prompt Usage", "Manage", account_url.clone())
@@ -1865,38 +1920,6 @@ impl AgentPanel {
.separator()
}
if thread_with_messages | text_thread_with_messages {
menu = menu.header("Current Thread");
if let Some(text_thread_view) = text_thread_view.as_ref() {
menu = menu
.entry("Regenerate Thread Title", None, {
let text_thread_view = text_thread_view.clone();
move |_, cx| {
Self::handle_regenerate_text_thread_title(
text_thread_view.clone(),
cx,
);
}
})
.separator();
}
if let Some(thread_view) = thread_view.as_ref() {
menu = menu
.entry("Regenerate Thread Title", None, {
let thread_view = thread_view.clone();
move |_, cx| {
Self::handle_regenerate_thread_title(
thread_view.clone(),
cx,
);
}
})
.separator();
}
}
menu = menu
.header("MCP Servers")
.action(
@@ -2428,7 +2451,7 @@ impl AgentPanel {
let history_is_empty = self.history_store.read(cx).is_empty(cx);
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
.visible_providers()
.providers()
.iter()
.any(|provider| {
provider.is_authenticated(cx)

View File

@@ -174,6 +174,16 @@ impl ExternalAgent {
Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
}
}
pub fn is_mcp(&self) -> bool {
match self {
Self::Gemini => true,
Self::ClaudeCode => true,
Self::Codex => true,
Self::NativeAgent => false,
Self::Custom { .. } => false,
}
}
}
/// Opens the profile management interface for configuring agent tools and settings.
@@ -348,8 +358,7 @@ fn init_language_model_settings(cx: &mut App) {
|_, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
| language_model::Event::RemovedProvider(_) => {
update_active_language_model_from_settings(cx);
}
_ => {}

View File

@@ -75,9 +75,6 @@ pub struct BufferCodegen {
session_id: Uuid,
}
pub const REWRITE_SECTION_TOOL_NAME: &str = "rewrite_section";
pub const FAILURE_MESSAGE_TOOL_NAME: &str = "failure_message";
impl BufferCodegen {
pub fn new(
buffer: Entity<MultiBuffer>,
@@ -525,12 +522,12 @@ impl CodegenAlternative {
let tools = vec![
LanguageModelRequestTool {
name: REWRITE_SECTION_TOOL_NAME.to_string(),
name: "rewrite_section".to_string(),
description: "Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
input_schema: language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
},
LanguageModelRequestTool {
name: FAILURE_MESSAGE_TOOL_NAME.to_string(),
name: "failure_message".to_string(),
description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
input_schema: language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
},
@@ -1170,7 +1167,7 @@ impl CodegenAlternative {
let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option<ToolUseOutput> {
let mut chars_read_so_far = chars_read_so_far.lock();
match tool_use.name.as_ref() {
REWRITE_SECTION_TOOL_NAME => {
"rewrite_section" => {
let Ok(input) =
serde_json::from_value::<RewriteSectionInput>(tool_use.input)
else {
@@ -1183,7 +1180,7 @@ impl CodegenAlternative {
description: None,
})
}
FAILURE_MESSAGE_TOOL_NAME => {
"failure_message" => {
let Ok(mut input) =
serde_json::from_value::<FailureMessageInput>(tool_use.input)
else {
@@ -1496,10 +1493,7 @@ mod tests {
use indoc::indoc;
use language::{Buffer, Point};
use language_model::fake_provider::FakeLanguageModel;
use language_model::{
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRegistry,
LanguageModelToolUse, StopReason, TokenUsage,
};
use language_model::{LanguageModelRegistry, TokenUsage};
use languages::rust_lang;
use rand::prelude::*;
use settings::SettingsStore;
@@ -1811,51 +1805,6 @@ mod tests {
);
}
// When not streaming tool calls, we strip backticks as part of parsing the model's
// plain text response. This is a regression test for a bug where we stripped
// backticks incorrectly.
#[gpui::test]
async fn test_allows_model_to_output_backticks(cx: &mut TestAppContext) {
init_test(cx);
let text = "- Improved; `cmd+click` behavior. Now requires `cmd` to be pressed before the click starts or it doesn't run. ([#44579](https://github.com/zed-industries/zed/pull/44579); thanks [Zachiah](https://github.com/Zachiah))";
let buffer = cx.new(|cx| Buffer::local("", cx));
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let range = buffer.read_with(cx, |buffer, cx| {
let snapshot = buffer.snapshot(cx);
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(0, 0))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
prompt_builder,
Uuid::new_v4(),
cx,
)
});
let events_tx = simulate_tool_based_completion(&codegen, cx);
let chunk_len = text.find('`').unwrap();
events_tx
.unbounded_send(rewrite_tool_use("tool_1", &text[..chunk_len], false))
.unwrap();
events_tx
.unbounded_send(rewrite_tool_use("tool_2", &text, true))
.unwrap();
events_tx
.unbounded_send(LanguageModelCompletionEvent::Stop(StopReason::EndTurn))
.unwrap();
drop(events_tx);
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
text
);
}
#[gpui::test]
async fn test_strip_invalid_spans_from_codeblock() {
assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await;
@@ -1921,39 +1870,4 @@ mod tests {
});
chunks_tx
}
fn simulate_tool_based_completion(
codegen: &Entity<CodegenAlternative>,
cx: &mut TestAppContext,
) -> mpsc::UnboundedSender<LanguageModelCompletionEvent> {
let (events_tx, events_rx) = mpsc::unbounded();
let model = Arc::new(FakeLanguageModel::default());
codegen.update(cx, |codegen, cx| {
let completion_stream = Task::ready(Ok(events_rx.map(Ok).boxed()
as BoxStream<
'static,
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
>));
codegen.generation = codegen.handle_completion(model, completion_stream, cx);
});
events_tx
}
fn rewrite_tool_use(
id: &str,
replacement_text: &str,
is_complete: bool,
) -> LanguageModelCompletionEvent {
let input = RewriteSectionInput {
replacement_text: replacement_text.into(),
};
LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
id: id.into(),
name: REWRITE_SECTION_TOOL_NAME.into(),
raw_input: serde_json::to_string(&input).unwrap(),
input: serde_json::to_value(&input).unwrap(),
is_input_complete: is_complete,
thought_signature: None,
})
}
}

View File

@@ -1586,7 +1586,7 @@ pub(crate) fn search_rules(
None
} else {
Some(RulesContextEntry {
prompt_id: metadata.id.as_user()?,
prompt_id: metadata.id.user_id()?,
title: metadata.title?,
})
}

View File

@@ -1,5 +1,6 @@
use std::sync::Arc;
use agent_client_protocol::ModelId;
use fs::Fs;
use language_model::LanguageModel;
use settings::{LanguageModelSelection, update_settings_file};
@@ -12,11 +13,20 @@ fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelS
}
}
fn model_id_to_selection(model_id: &ModelId) -> LanguageModelSelection {
let id = model_id.0.as_ref();
let (provider, model) = id.split_once('/').unwrap_or(("", id));
LanguageModelSelection {
provider: provider.to_owned().into(),
model: model.to_owned(),
}
}
pub fn toggle_in_settings(
model: Arc<dyn LanguageModel>,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &mut App,
cx: &App,
) {
let selection = language_model_to_selection(&model);
update_settings_file(fs, cx, move |settings, _| {
@@ -28,3 +38,20 @@ pub fn toggle_in_settings(
}
});
}
pub fn toggle_model_id_in_settings(
model_id: ModelId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
let selection = model_id_to_selection(&model_id);
update_settings_file(fs, cx, move |settings, _| {
let agent = settings.agent.get_or_insert_default();
if should_be_favorite {
agent.add_favorite_model(selection.clone());
} else {
agent.remove_favorite_model(&selection);
}
});
}

View File

@@ -1197,7 +1197,7 @@ impl InlineAssistant {
assist
.editor
.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
.ok();
}
@@ -1209,7 +1209,7 @@ impl InlineAssistant {
if let Some(decorations) = assist.decorations.as_ref() {
decorations.prompt_editor.update(cx, |prompt_editor, cx| {
prompt_editor.editor.update(cx, |editor, cx| {
window.focus(&editor.focus_handle(cx), cx);
window.focus(&editor.focus_handle(cx));
editor.select_all(&SelectAll, window, cx);
})
});
@@ -1259,26 +1259,28 @@ impl InlineAssistant {
let bottom = top + 1.0;
(top, bottom)
});
let height_in_lines = editor.visible_line_count().unwrap_or(0.);
let vertical_scroll_margin = editor.vertical_scroll_margin() as ScrollOffset;
let scroll_target_top = (scroll_target_range.0 - vertical_scroll_margin)
// Don't scroll up too far in the case of a large vertical_scroll_margin.
.max(scroll_target_range.0 - height_in_lines / 2.0);
let scroll_target_bottom = (scroll_target_range.1 + vertical_scroll_margin)
// Don't scroll down past where the top would still be visible.
.min(scroll_target_top + height_in_lines);
let mut scroll_target_top = scroll_target_range.0;
let mut scroll_target_bottom = scroll_target_range.1;
scroll_target_top -= editor.vertical_scroll_margin() as ScrollOffset;
scroll_target_bottom += editor.vertical_scroll_margin() as ScrollOffset;
let height_in_lines = editor.visible_line_count().unwrap_or(0.);
let scroll_top = editor.scroll_position(cx).y;
let scroll_bottom = scroll_top + height_in_lines;
if scroll_target_top < scroll_top {
editor.set_scroll_position(point(0., scroll_target_top), window, cx);
} else if scroll_target_bottom > scroll_bottom {
editor.set_scroll_position(
point(0., scroll_target_bottom - height_in_lines),
window,
cx,
);
if (scroll_target_bottom - scroll_target_top) <= height_in_lines {
editor.set_scroll_position(
point(0., scroll_target_bottom - height_in_lines),
window,
cx,
);
} else {
editor.set_scroll_position(point(0., scroll_target_top), window, cx);
}
}
});
}
@@ -2269,36 +2271,6 @@ pub mod evals {
);
}
#[test]
#[cfg_attr(not(feature = "unit-eval"), ignore)]
fn eval_empty_buffer() {
run_eval(
20,
1.0,
"Write a Python hello, world program".to_string(),
"ˇ".to_string(),
|output| match output {
InlineAssistantOutput::Success {
full_buffer_text, ..
} => {
if full_buffer_text.is_empty() {
EvalOutput::failed("expected some output".to_string())
} else {
EvalOutput::passed(format!("Produced {full_buffer_text}"))
}
}
o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!(
"Assistant output does not match expected output: {:?}",
o
)),
o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!(
"Assistant output does not match expected output: {:?}",
o
)),
},
);
}
fn run_eval(
iterations: usize,
expected_pass_ratio: f32,

View File

@@ -40,9 +40,7 @@ use crate::completion_provider::{
use crate::mention_set::paste_images_as_context;
use crate::mention_set::{MentionSet, crease_for_mention};
use crate::terminal_codegen::TerminalCodegen;
use crate::{
CycleFavoriteModels, CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext,
};
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
@@ -150,7 +148,7 @@ impl<T: 'static> Render for PromptEditor<T> {
.into_any_element();
v_flex()
.key_context("InlineAssistant")
.key_context("PromptEditor")
.capture_action(cx.listener(Self::paste))
.block_mouse_except_scroll()
.size_full()
@@ -164,6 +162,10 @@ impl<T: 'static> Render for PromptEditor<T> {
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
}))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
@@ -172,15 +174,6 @@ impl<T: 'static> Render for PromptEditor<T> {
.on_action(cx.listener(Self::thumbs_down))
.capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
}))
.on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
this.model_selector.update(cx, |model_selector, cx| {
model_selector.cycle_favorite_models(window, cx);
});
}))
.child(
WithRemSize::new(ui_font_size)
.h_full()
@@ -364,7 +357,7 @@ impl<T: 'static> PromptEditor<T> {
creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
if focus {
window.focus(&editor.focus_handle(cx), cx);
window.focus(&editor.focus_handle(cx));
}
editor
});
@@ -862,7 +855,7 @@ impl<T: 'static> PromptEditor<T> {
.map(|this| {
if rated {
this.disabled(true)
.icon_color(Color::Disabled)
.icon_color(Color::Ignored)
.tooltip(move |_, cx| {
Tooltip::with_meta(
"Good Result",
@@ -872,15 +865,8 @@ impl<T: 'static> PromptEditor<T> {
)
})
} else {
this.icon_color(Color::Muted).tooltip(
move |_, cx| {
Tooltip::for_action(
"Good Result",
&ThumbsUpResult,
cx,
)
},
)
this.icon_color(Color::Muted)
.tooltip(Tooltip::text("Good Result"))
}
})
.on_click(cx.listener(|this, _, window, cx| {
@@ -893,7 +879,7 @@ impl<T: 'static> PromptEditor<T> {
.map(|this| {
if rated {
this.disabled(true)
.icon_color(Color::Disabled)
.icon_color(Color::Ignored)
.tooltip(move |_, cx| {
Tooltip::with_meta(
"Bad Result",
@@ -903,15 +889,8 @@ impl<T: 'static> PromptEditor<T> {
)
})
} else {
this.icon_color(Color::Muted).tooltip(
move |_, cx| {
Tooltip::for_action(
"Bad Result",
&ThumbsDownResult,
cx,
)
},
)
this.icon_color(Color::Muted)
.tooltip(Tooltip::text("Bad Result"))
}
})
.on_click(cx.listener(|this, _, window, cx| {
@@ -1109,6 +1088,7 @@ impl<T: 'static> PromptEditor<T> {
let colors = cx.theme().colors();
div()
.key_context("InlineAssistEditor")
.size_full()
.p_2()
.pl_1()

View File

@@ -7,8 +7,8 @@ use gpui::{
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
};
use language_model::{
AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProvider,
LanguageModelProviderId, LanguageModelRegistry,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
@@ -20,14 +20,14 @@ use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem}
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static>;
type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &App) + 'static>;
pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
pub fn language_model_selector(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
popover_styles: bool,
focus_handle: FocusHandle,
window: &mut Window,
@@ -55,7 +55,7 @@ pub fn language_model_selector(
fn all_models(cx: &App) -> GroupedModels {
let lm_registry = LanguageModelRegistry::global(cx).read(cx);
let providers = lm_registry.visible_providers();
let providers = lm_registry.providers();
let mut favorites_index = FavoritesIndex::default();
@@ -94,7 +94,7 @@ type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>
#[derive(Clone)]
struct ModelInfo {
model: Arc<dyn LanguageModel>,
icon: IconOrSvg,
icon: IconName,
is_favorite: bool,
}
@@ -133,7 +133,7 @@ impl LanguageModelPickerDelegate {
fn new(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
popover_styles: bool,
focus_handle: FocusHandle,
window: &mut Window,
@@ -203,7 +203,7 @@ impl LanguageModelPickerDelegate {
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.visible_providers()
.providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
@@ -250,10 +250,6 @@ impl LanguageModelPickerDelegate {
(self.get_active_model)(cx)
}
pub fn favorites_count(&self) -> usize {
self.all_models.favorites.len()
}
pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if self.all_models.favorites.is_empty() {
return;
@@ -478,7 +474,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let configured_providers = language_model_registry
.read(cx)
.visible_providers()
.providers()
.into_iter()
.filter(|provider| provider.is_authenticated(cx))
.collect::<Vec<_>>();
@@ -565,18 +561,12 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let handle_action_click = {
let model = model_info.model.clone();
let on_toggle_favorite = self.on_toggle_favorite.clone();
cx.listener(move |picker, _, window, cx| {
on_toggle_favorite(model.clone(), !is_favorite, cx);
picker.refresh(window, cx);
})
move |cx: &App| on_toggle_favorite(model.clone(), !is_favorite, cx)
};
Some(
ModelSelectorListItem::new(ix, model_info.model.name().0)
.map(|this| match &model_info.icon {
IconOrSvg::Icon(icon_name) => this.icon(*icon_name),
IconOrSvg::Svg(icon_path) => this.icon_path(icon_path.clone()),
})
.icon(model_info.icon)
.is_selected(is_selected)
.is_focused(selected)
.is_favorite(is_favorite)
@@ -712,7 +702,7 @@ mod tests {
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
ModelInfo {
model: Arc::new(TestLanguageModel::new(name, provider)),
icon: IconOrSvg::Icon(IconName::Ai),
icon: IconName::Ai,
is_favorite,
}
})

View File

@@ -191,9 +191,6 @@ impl Render for ProfileSelector {
let container = || h_flex().gap_1().justify_between();
v_flex()
.gap_1()
.child(container().child(Label::new("Toggle Profile Menu")).child(
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
))
.child(
container()
.pb_1()
@@ -206,6 +203,9 @@ impl Render for ProfileSelector {
cx,
)),
)
.child(container().child(Label::new("Toggle Profile Menu")).child(
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
))
.into_any()
}
}),

View File

@@ -127,7 +127,7 @@ impl TerminalInlineAssistant {
if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
prompt_editor.update(cx, |this, cx| {
this.editor.update(cx, |editor, cx| {
window.focus(&editor.focus_handle(cx), cx);
window.focus(&editor.focus_handle(cx));
editor.select_all(&SelectAll, window, cx);
});
});
@@ -292,7 +292,7 @@ impl TerminalInlineAssistant {
.terminal
.update(cx, |this, cx| {
this.clear_block_below_cursor(cx);
this.focus_handle(cx).focus(window, cx);
this.focus_handle(cx).focus(window);
})
.log_err();
@@ -369,7 +369,7 @@ impl TerminalInlineAssistant {
.terminal
.update(cx, |this, cx| {
this.clear_block_below_cursor(cx);
this.focus_handle(cx).focus(window, cx);
this.focus_handle(cx).focus(window);
})
.is_ok()
}

View File

@@ -1,8 +1,8 @@
use crate::{
language_model_selector::{LanguageModelSelector, language_model_selector},
ui::{BurnModeTooltip, ModelSelectorTooltip},
ui::BurnModeTooltip,
};
use agent_settings::CompletionMode;
use agent_settings::{AgentSettings, CompletionMode};
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
@@ -33,8 +33,7 @@ use language::{
language_settings::{SoftWrap, all_language_settings},
};
use language_model::{
ConfigurationError, IconOrSvg, LanguageModelExt, LanguageModelImage, LanguageModelRegistry,
Role,
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
};
use multi_buffer::MultiBufferRow;
use picker::{Picker, popover_menu::PickerPopoverMenu};
@@ -72,7 +71,7 @@ use workspace::{
pane,
searchable::{SearchEvent, SearchableItem},
};
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
use crate::CycleFavoriteModels;
@@ -1342,7 +1341,7 @@ impl TextThreadEditor {
if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
active_editor_view.update(cx, |editor, cx| {
editor.insert(&text, window, cx);
editor.focus_handle(cx).focus(window, cx);
editor.focus_handle(cx).focus(window);
})
}
}
@@ -1699,9 +1698,6 @@ impl TextThreadEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let editor_clipboard_selections = cx
.read_from_clipboard()
.and_then(|item| item.entries().first().cloned())
@@ -1712,101 +1708,84 @@ impl TextThreadEditor {
_ => None,
});
// Insert creases for pasted clipboard selections that:
// 1. Contain exactly one selection
// 2. Have an associated file path
// 3. Span multiple lines (not single-line selections)
// 4. Belong to a file that exists in the current project
let should_insert_creases = util::maybe!({
let selections = editor_clipboard_selections.as_ref()?;
if selections.len() > 1 {
return Some(false);
}
let selection = selections.first()?;
let file_path = selection.file_path.as_ref()?;
let line_range = selection.line_range.as_ref()?;
let has_file_context = editor_clipboard_selections
.as_ref()
.is_some_and(|selections| {
selections
.iter()
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
});
if line_range.start() == line_range.end() {
return Some(false);
}
if has_file_context {
if let Some(clipboard_item) = cx.read_from_clipboard() {
if let Some(ClipboardEntry::String(clipboard_text)) =
clipboard_item.entries().first()
{
if let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
Some(
workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some(),
)
})
.unwrap_or(false);
let text = clipboard_text.text();
self.editor.update(cx, |editor, cx| {
let mut current_offset = 0;
let weak_editor = cx.entity().downgrade();
if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
if let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let selected_text =
&text[current_offset..current_offset + selection.len];
let fence = assistant_slash_commands::codeblock_fence_for_path(
file_path.to_str(),
Some(line_range.clone()),
);
let formatted_text = format!("{fence}{selected_text}\n```");
let text = clipboard_text.text();
self.editor.update(cx, |editor, cx| {
let mut current_offset = 0;
let weak_editor = cx.entity().downgrade();
let insert_point = editor
.selections
.newest::<Point>(&editor.display_snapshot(cx))
.head();
let start_row = MultiBufferRow(insert_point.row);
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let selected_text =
&text[current_offset..current_offset + selection.len];
let fence = assistant_slash_commands::codeblock_fence_for_path(
file_path.to_str(),
Some(line_range.clone()),
);
let formatted_text = format!("{fence}{selected_text}\n```");
editor.insert(&formatted_text, window, cx);
let insert_point = editor
.selections
.newest::<Point>(&editor.display_snapshot(cx))
.head();
let start_row = MultiBufferRow(insert_point.row);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_before = snapshot.anchor_after(insert_point);
let anchor_after = editor
.selections
.newest_anchor()
.head()
.bias_left(&snapshot);
editor.insert(&formatted_text, window, cx);
editor.insert("\n", window, cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_before = snapshot.anchor_after(insert_point);
let anchor_after = editor
.selections
.newest_anchor()
.head()
.bias_left(&snapshot);
let crease_text = acp_thread::selection_name(
Some(file_path.as_ref()),
&line_range,
);
editor.insert("\n", window, cx);
let fold_placeholder = quote_selection_fold_placeholder(
crease_text,
weak_editor.clone(),
);
let crease = Crease::inline(
anchor_before..anchor_after,
fold_placeholder,
render_quote_selection_output_toggle,
|_, _, _, _| Empty.into_any(),
);
editor.insert_creases(vec![crease], cx);
editor.fold_at(start_row, window, cx);
let crease_text = acp_thread::selection_name(
Some(file_path.as_ref()),
&line_range,
);
let fold_placeholder = quote_selection_fold_placeholder(
crease_text,
weak_editor.clone(),
);
let crease = Crease::inline(
anchor_before..anchor_after,
fold_placeholder,
render_quote_selection_output_toggle,
|_, _, _, _| Empty.into_any(),
);
editor.insert_creases(vec![crease], cx);
editor.fold_at(start_row, window, cx);
current_offset += selection.len;
if !selection.is_entire_line && current_offset < text.len() {
current_offset += 1;
current_offset += selection.len;
if !selection.is_entire_line && current_offset < text.len() {
current_offset += 1;
}
}
}
}
});
return;
});
return;
}
}
}
}
@@ -1965,12 +1944,6 @@ impl TextThreadEditor {
}
}
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.paste(&editor::actions::Paste, window, cx);
});
}
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
@@ -2232,10 +2205,10 @@ impl TextThreadEditor {
.default_model()
.map(|default| default.provider);
let provider_icon = active_provider
.as_ref()
.map(|p| p.icon())
.unwrap_or(IconOrSvg::Icon(IconName::Ai));
let provider_icon = match active_provider {
Some(provider) => provider.icon(),
None => IconName::Ai,
};
let focus_handle = self.editor().focus_handle(cx);
@@ -2245,25 +2218,43 @@ impl TextThreadEditor {
(Color::Muted, IconName::ChevronDown)
};
let provider_icon_element = match provider_icon {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.color(color)
.size(IconSize::XSmall);
let show_cycle_row = self
.language_model_selector
.read(cx)
.delegate
.favorites_count()
> 1;
let tooltip = Tooltip::element({
move |_, _cx| {
ModelSelectorTooltip::new(focus_handle.clone())
.show_cycle_row(show_cycle_row)
.into_any_element()
move |_, cx| {
let focus_handle = focus_handle.clone();
let should_show_cycle_row = !AgentSettings::get_global(cx)
.favorite_model_ids()
.is_empty();
v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Change Model"))
.child(KeyBinding::for_action_in(
&ToggleModelSelector,
&focus_handle,
cx,
)),
)
.when(should_show_cycle_row, |this| {
this.child(
h_flex()
.pt_1()
.gap_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.justify_between()
.child(Label::new("Cycle Favorited Models"))
.child(KeyBinding::for_action_in(
&CycleFavoriteModels,
&focus_handle,
cx,
)),
)
})
.into_any()
}
});
@@ -2274,7 +2265,7 @@ impl TextThreadEditor {
.child(
h_flex()
.gap_0p5()
.child(provider_icon_element)
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
.child(
Label::new(model_name)
.color(color)
@@ -2636,7 +2627,6 @@ impl Render for TextThreadEditor {
.capture_action(cx.listener(TextThreadEditor::copy))
.capture_action(cx.listener(TextThreadEditor::cut))
.capture_action(cx.listener(TextThreadEditor::paste))
.on_action(cx.listener(TextThreadEditor::paste_raw))
.capture_action(cx.listener(TextThreadEditor::cycle_message_role))
.capture_action(cx.listener(TextThreadEditor::confirm_command))
.on_action(cx.listener(TextThreadEditor::assist))

View File

@@ -222,8 +222,8 @@ impl Render for AcpOnboardingModal {
acp_onboarding_event!("Canceled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
this.focus_handle.focus(window, cx);
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(illustration)
.child(

View File

@@ -230,8 +230,8 @@ impl Render for ClaudeCodeOnboardingModal {
claude_code_onboarding_event!("Canceled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
this.focus_handle.focus(window, cx);
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(illustration)
.child(

View File

@@ -27,7 +27,7 @@ impl RenderOnce for HoldForDefault {
PlatformStyle::platform(),
None,
Some(TextSize::Default.rems(cx).into()),
false,
true,
)))
.child(div().map(|this| {
if self.is_default {

View File

@@ -1,13 +1,5 @@
use gpui::{Action, ClickEvent, FocusHandle, prelude::*};
use gpui::{Action, FocusHandle, prelude::*};
use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
use zed_actions::agent::ToggleModelSelector;
use crate::CycleFavoriteModels;
enum ModelIcon {
Name(IconName),
Path(SharedString),
}
#[derive(IntoElement)]
pub struct ModelSelectorHeader {
@@ -47,11 +39,11 @@ impl RenderOnce for ModelSelectorHeader {
pub struct ModelSelectorListItem {
index: usize,
title: SharedString,
icon: Option<ModelIcon>,
icon: Option<IconName>,
is_selected: bool,
is_focused: bool,
is_favorite: bool,
on_toggle_favorite: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
on_toggle_favorite: Option<Box<dyn Fn(&App) + 'static>>,
}
impl ModelSelectorListItem {
@@ -68,12 +60,7 @@ impl ModelSelectorListItem {
}
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(ModelIcon::Name(icon));
self
}
pub fn icon_path(mut self, path: SharedString) -> Self {
self.icon = Some(ModelIcon::Path(path));
self.icon = Some(icon);
self
}
@@ -92,10 +79,7 @@ impl ModelSelectorListItem {
self
}
pub fn on_toggle_favorite(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
pub fn on_toggle_favorite(mut self, handler: impl Fn(&App) + 'static) -> Self {
self.on_toggle_favorite = Some(Box::new(handler));
self
}
@@ -121,12 +105,9 @@ impl RenderOnce for ModelSelectorListItem {
.gap_1p5()
.when_some(self.icon, |this, icon| {
this.child(
match icon {
ModelIcon::Name(icon_name) => Icon::new(icon_name),
ModelIcon::Path(icon_path) => Icon::from_external_svg(icon_path),
}
.color(model_icon_color)
.size(IconSize::Small),
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small),
)
})
.child(Label::new(self.title).truncate()),
@@ -147,7 +128,7 @@ impl RenderOnce for ModelSelectorListItem {
.icon_color(color)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text(tooltip))
.on_click(move |event, window, cx| (handle_click)(event, window, cx)),
.on_click(move |_, _, cx| (handle_click)(cx)),
)
}
}))
@@ -193,57 +174,3 @@ impl RenderOnce for ModelSelectorFooter {
)
}
}
#[derive(IntoElement)]
pub struct ModelSelectorTooltip {
focus_handle: FocusHandle,
show_cycle_row: bool,
}
impl ModelSelectorTooltip {
pub fn new(focus_handle: FocusHandle) -> Self {
Self {
focus_handle,
show_cycle_row: true,
}
}
pub fn show_cycle_row(mut self, show: bool) -> Self {
self.show_cycle_row = show;
self
}
}
impl RenderOnce for ModelSelectorTooltip {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Change Model"))
.child(KeyBinding::for_action_in(
&ToggleModelSelector,
&self.focus_handle,
cx,
)),
)
.when(self.show_cycle_row, |this| {
this.child(
h_flex()
.pt_1()
.gap_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.justify_between()
.child(Label::new("Cycle Favorited Models"))
.child(KeyBinding::for_action_in(
&CycleFavoriteModels,
&self.focus_handle,
cx,
)),
)
})
}
}

View File

@@ -83,8 +83,8 @@ impl Render for AgentOnboardingModal {
agent_onboarding_event!("Canceled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
this.focus_handle.focus(window, cx);
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(
div()

View File

@@ -12,10 +12,6 @@ workspace = true
path = "src/agent_ui_v2.rs"
doctest = false
[features]
test-support = ["agent/test-support"]
[dependencies]
agent.workspace = true
agent_servers.workspace = true
@@ -42,6 +38,3 @@ time_format.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
[dev-dependencies]
agent = { workspace = true, features = ["test-support"] }

View File

@@ -1 +1 @@
../../LICENSE-GPL
LICENSE-GPL

View File

@@ -1,5 +1,5 @@
use agent::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate;
use gpui::{
@@ -411,22 +411,7 @@ impl AcpThreadHistory {
let selected = ix == self.selected_index;
let hovered = Some(ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp();
let display_text = match format {
EntryTimeFormat::DateAndTime => {
let entry_time = entry.updated_at();
let now = Utc::now();
let duration = now.signed_duration_since(entry_time);
let days = duration.num_days();
format!("{}d", days)
}
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
};
let title = entry.title().clone();
let full_date =
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
h_flex()
.w_full()
@@ -447,14 +432,11 @@ impl AcpThreadHistory {
.truncate(),
)
.child(
Label::new(display_text)
Label::new(thread_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.tooltip(move |_, cx| {
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
})
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = Some(ix);

View File

@@ -1,9 +1,9 @@
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
use language_model::{IconOrSvg, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::{Divider, List, ListBulletItem, prelude::*};
pub struct ApiKeysWithProviders {
configured_providers: Vec<(IconOrSvg, SharedString)>,
configured_providers: Vec<(IconName, SharedString)>,
}
impl ApiKeysWithProviders {
@@ -13,8 +13,7 @@ impl ApiKeysWithProviders {
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
| language_model::Event::RemovedProvider(_) => {
this.configured_providers = Self::compute_configured_providers(cx)
}
_ => {}
@@ -27,9 +26,9 @@ impl ApiKeysWithProviders {
}
}
fn compute_configured_providers(cx: &App) -> Vec<(IconOrSvg, SharedString)> {
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
LanguageModelRegistry::read_global(cx)
.visible_providers()
.providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
@@ -48,14 +47,7 @@ impl Render for ApiKeysWithProviders {
.map(|(icon, name)| {
h_flex()
.gap_1p5()
.child(
match icon {
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
IconOrSvg::Svg(icon_path) => Icon::from_external_svg(icon_path),
}
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(Label::new(name))
});
div()

View File

@@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
pub struct AgentPanelOnboarding {
user_store: Entity<UserStore>,
client: Arc<Client>,
has_configured_providers: bool,
configured_providers: Vec<(IconName, SharedString)>,
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
}
@@ -27,9 +27,8 @@ impl AgentPanelOnboarding {
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
this.has_configured_providers = Self::has_configured_providers(cx)
| language_model::Event::RemovedProvider(_) => {
this.configured_providers = Self::compute_available_providers(cx)
}
_ => {}
},
@@ -39,16 +38,20 @@ impl AgentPanelOnboarding {
Self {
user_store,
client,
has_configured_providers: Self::has_configured_providers(cx),
configured_providers: Self::compute_available_providers(cx),
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
}
}
fn has_configured_providers(cx: &App) -> bool {
fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> {
LanguageModelRegistry::read_global(cx)
.visible_providers()
.providers()
.iter()
.any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.map(|provider| (provider.icon(), provider.name().0))
.collect()
}
}
@@ -78,7 +81,7 @@ impl Render for AgentPanelOnboarding {
}),
)
.map(|this| {
if enrolled_in_trial || is_pro_user || self.has_configured_providers {
if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
this
} else {
this.child(ApiKeysWithoutProviders::new())

View File

@@ -87,7 +87,7 @@ pub async fn stream_completion(
Ok(None) => None,
Err(err) => Some((
Err(BedrockError::ClientError(anyhow!(
"{}",
"{:?}",
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
))),
stream,

View File

@@ -314,12 +314,6 @@ impl BufferDiffSnapshot {
self.inner.hunks.is_empty()
}
pub fn base_text_string(&self) -> Option<String> {
self.inner
.base_text_exists
.then(|| self.inner.base_text.text())
}
pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> {
self.secondary_diff.as_deref()
}
@@ -1165,34 +1159,6 @@ impl BufferDiff {
new_index_text
}
pub fn stage_or_unstage_all_hunks(
&mut self,
stage: bool,
buffer: &text::BufferSnapshot,
file_exists: bool,
cx: &mut Context<Self>,
) {
let hunks = self
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
.collect::<Vec<_>>();
let Some(secondary) = self.secondary_diff.as_ref() else {
return;
};
self.inner.stage_or_unstage_hunks_impl(
&secondary.read(cx).inner,
stage,
&hunks,
buffer,
file_exists,
);
if let Some((first, last)) = hunks.first().zip(hunks.last()) {
let changed_range = first.buffer_range.start..last.buffer_range.end;
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(changed_range),
});
}
}
pub fn range_to_hunk_range(
&self,
range: Range<Anchor>,
@@ -2189,7 +2155,7 @@ mod tests {
let range = diff_1.inner.compare(&empty_diff.inner, &buffer).unwrap();
assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
// Edit does affects the diff because it recalculates word diffs.
// Edit does not affect the diff.
buffer.edit_via_marked_text(
&"
one
@@ -2204,14 +2170,7 @@ mod tests {
.unindent(),
);
let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
assert_eq!(
Point::new(4, 0)..Point::new(5, 0),
diff_2
.inner
.compare(&diff_1.inner, &buffer)
.unwrap()
.to_point(&buffer)
);
assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer));
// Edit turns a deletion hunk into a modification.
buffer.edit_via_marked_text(

View File

@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result};
use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
use futures::AsyncReadExt;
use gpui::{App, Context, Entity, Task};
use http_client::HttpClient;
@@ -300,6 +300,16 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate {
}));
}
fn cycle(
&mut self,
_buffer: Entity<Buffer>,
_cursor_position: Anchor,
_direction: Direction,
_cx: &mut Context<Self>,
) {
// Codestral doesn't support multiple completions, so cycling does nothing
}
fn accept(&mut self, _cx: &mut Context<Self>) {
log::debug!("Codestral: Completion accepted");
self.pending_request = None;

View File

@@ -113,7 +113,7 @@ impl CopilotSweAgentBot {
const USER_ID: i32 = 198982749;
/// The alias of the GitHub copilot user. Although https://api.github.com/users/copilot
/// yields a 404, GitHub still refers to the copilot bot user as @Copilot in some cases.
const NAME_ALIAS: &'static str = "Copilot";
const NAME_ALIAS: &'static str = "copilot";
/// Returns the `created_at` timestamp for the Dependabot bot user.
fn created_at() -> &'static NaiveDateTime {

View File

@@ -6745,13 +6745,8 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Split pane to the right
pane.update_in(cx, |pane, window, cx| {
pane.split(
workspace::SplitDirection::Right,
workspace::SplitMode::default(),
window,
cx,
);
pane.update(cx, |pane, cx| {
pane.split(workspace::SplitDirection::Right, cx);
});
cx.run_until_parked();
let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());

View File

@@ -1252,7 +1252,7 @@ impl CollabPanel {
context_menu
});
window.focus(&context_menu.focus_handle(cx), cx);
window.focus(&context_menu.focus_handle(cx));
let subscription = cx.subscribe_in(
&context_menu,
window,
@@ -1424,7 +1424,7 @@ impl CollabPanel {
context_menu
});
window.focus(&context_menu.focus_handle(cx), cx);
window.focus(&context_menu.focus_handle(cx));
let subscription = cx.subscribe_in(
&context_menu,
window,
@@ -1487,7 +1487,7 @@ impl CollabPanel {
})
});
window.focus(&context_menu.focus_handle(cx), cx);
window.focus(&context_menu.focus_handle(cx));
let subscription = cx.subscribe_in(
&context_menu,
window,
@@ -1521,9 +1521,9 @@ impl CollabPanel {
if cx.stop_active_drag(window) {
return;
} else if self.take_editing_state(window, cx) {
window.focus(&self.filter_editor.focus_handle(cx), cx);
window.focus(&self.filter_editor.focus_handle(cx));
} else if !self.reset_filter_editor_text(window, cx) {
self.focus_handle.focus(window, cx);
self.focus_handle.focus(window);
}
if self.context_menu.is_some() {
@@ -1826,7 +1826,7 @@ impl CollabPanel {
});
self.update_entries(false, cx);
self.select_channel_editor();
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
window.focus(&self.channel_name_editor.focus_handle(cx));
cx.notify();
}
@@ -1851,7 +1851,7 @@ impl CollabPanel {
});
self.update_entries(false, cx);
self.select_channel_editor();
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
window.focus(&self.channel_name_editor.focus_handle(cx));
cx.notify();
}
@@ -1900,7 +1900,7 @@ impl CollabPanel {
editor.set_text(channel.name.clone(), window, cx);
editor.select_all(&Default::default(), window, cx);
});
window.focus(&self.channel_name_editor.focus_handle(cx), cx);
window.focus(&self.channel_name_editor.focus_handle(cx));
self.update_entries(false, cx);
self.select_channel_editor();
}

View File

@@ -642,7 +642,7 @@ impl ChannelModalDelegate {
});
menu
});
window.focus(&context_menu.focus_handle(cx), cx);
window.focus(&context_menu.focus_handle(cx));
let subscription = cx.subscribe_in(
&context_menu,
window,

View File

@@ -588,7 +588,7 @@ impl PickerDelegate for CommandPaletteDelegate {
})
.detach_and_log_err(cx);
let action = command.action;
window.focus(&self.previous_focus_handle, cx);
window.focus(&self.previous_focus_handle);
self.dismissed(window, cx);
window.dispatch_action(action, cx);
}
@@ -784,7 +784,7 @@ mod tests {
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
});
cx.simulate_keystrokes("cmd-shift-p");
@@ -855,7 +855,7 @@ mod tests {
workspace.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx), cx))
editor.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)))
});
// Test normalize (trimming whitespace and double colons)

View File

@@ -1,45 +0,0 @@
[package]
name = "component_preview"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/component_preview.rs"
[features]
default = []
preview = []
test-support = ["db/test-support"]
[dependencies]
anyhow.workspace = true
client.workspace = true
collections.workspace = true
component.workspace = true
db.workspace = true
fs.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
node_runtime.workspace = true
notifications.workspace = true
project.workspace = true
release_channel.workspace = true
reqwest_client.workspace = true
session.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
ui_input.workspace = true
uuid.workspace = true
workspace.workspace = true
[[example]]
name = "component_preview"
path = "examples/component_preview.rs"
required-features = ["preview"]

View File

@@ -1 +0,0 @@
LICENSE-GPL

View File

@@ -1,18 +0,0 @@
//! Component Preview Example
//!
//! Run with: `cargo run -p component_preview --example component_preview --features="preview"`
//!
//! To use this in other projects, add the following to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! component_preview = { path = "../component_preview", features = ["preview"] }
//!
//! [[example]]
//! name = "component_preview"
//! path = "examples/component_preview.rs"
//! ```
fn main() {
component_preview::run_component_preview();
}

View File

@@ -1,145 +0,0 @@
/// Run the component preview application.
///
/// This initializes the application with minimal required infrastructure
/// and opens a workspace with the ComponentPreview item.
#[cfg(feature = "preview")]
pub fn run_component_preview() {
use fs::RealFs;
use gpui::{
AppContext as _, Application, Bounds, KeyBinding, WindowBounds, WindowOptions, actions,
size,
};
use client::{Client, UserStore};
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use project::Project;
use reqwest_client::ReqwestClient;
use session::{AppSession, Session};
use std::sync::Arc;
use ui::{App, px};
use workspace::{AppState, Workspace, WorkspaceStore};
use crate::{ComponentPreview, init};
actions!(zed, [Quit]);
fn quit(_: &Quit, cx: &mut App) {
cx.quit();
}
Application::new().run(|cx| {
component::init();
cx.on_action(quit);
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
let version = release_channel::AppVersion::load(env!("CARGO_PKG_VERSION"), None, None);
release_channel::init(version, cx);
let http_client =
ReqwestClient::user_agent("component_preview").expect("Failed to create HTTP client");
cx.set_http_client(Arc::new(http_client));
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
<dyn fs::Fs>::set_global(fs.clone(), cx);
settings::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
let client = Client::production(cx);
client::init(&client, cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
let session_id = uuid::Uuid::new_v4().to_string();
let session = cx.background_executor().block(Session::new(session_id));
let session = cx.new(|cx| AppSession::new(session, cx));
let node_runtime = NodeRuntime::unavailable();
let app_state = Arc::new(AppState {
languages,
client,
user_store,
workspace_store,
fs,
build_window_options: |_, _| Default::default(),
node_runtime,
session,
});
AppState::set_global(Arc::downgrade(&app_state), cx);
workspace::init(app_state.clone(), cx);
init(app_state.clone(), cx);
let size = size(px(1200.), px(800.));
let bounds = Bounds::centered(None, size, cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
{
move |window, cx| {
let app_state = app_state;
theme::setup_ui_font(window, cx);
let project = Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
None,
false,
cx,
);
let workspace = cx.new(|cx| {
Workspace::new(
Default::default(),
project.clone(),
app_state.clone(),
window,
cx,
)
});
workspace.update(cx, |workspace, cx| {
let weak_workspace = cx.entity().downgrade();
let language_registry = app_state.languages.clone();
let user_store = app_state.user_store.clone();
let component_preview = cx.new(|cx| {
ComponentPreview::new(
weak_workspace,
project,
language_registry,
user_store,
None,
None,
window,
cx,
)
.expect("Failed to create component preview")
});
workspace.add_item_to_active_pane(
Box::new(component_preview),
None,
true,
window,
cx,
);
});
workspace
}
},
)
.expect("Failed to open component preview window");
cx.activate(true);
});
}

View File

@@ -4,7 +4,6 @@ pub mod copilot_responses;
pub mod request;
mod sign_in;
use crate::request::NextEditSuggestions;
use crate::sign_in::initiate_sign_out;
use ::fs::Fs;
use anyhow::{Context as _, Result, anyhow};
@@ -19,7 +18,7 @@ use http_client::HttpClient;
use language::language_settings::CopilotSettings;
use language::{
Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,
language_settings::{EditPredictionProvider, all_language_settings},
language_settings::{EditPredictionProvider, all_language_settings, language_settings},
point_from_lsp, point_to_lsp,
};
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
@@ -41,7 +40,7 @@ use std::{
sync::Arc,
};
use sum_tree::Dimensions;
use util::{ResultExt, fs::remove_matching};
use util::{ResultExt, fs::remove_matching, rel_path::RelPath};
use workspace::Workspace;
pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;
@@ -316,15 +315,6 @@ struct GlobalCopilot(Entity<Copilot>);
impl Global for GlobalCopilot {}
/// Copilot's NextEditSuggestion response, with coordinates converted to Anchors.
struct CopilotEditPrediction {
buffer: Entity<Buffer>,
range: Range<Anchor>,
text: String,
command: Option<lsp::Command>,
snapshot: BufferSnapshot,
}
impl Copilot {
pub fn global(cx: &App) -> Option<Entity<Self>> {
cx.try_global::<GlobalCopilot>()
@@ -883,19 +873,101 @@ impl Copilot {
}
}
pub(crate) fn completions(
pub fn completions<T>(
&mut self,
buffer: &Entity<Buffer>,
position: Anchor,
position: T,
cx: &mut Context<Self>,
) -> Task<Result<Vec<CopilotEditPrediction>>> {
) -> Task<Result<Vec<Completion>>>
where
T: ToPointUtf16,
{
self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
}
pub fn completions_cycling<T>(
&mut self,
buffer: &Entity<Buffer>,
position: T,
cx: &mut Context<Self>,
) -> Task<Result<Vec<Completion>>>
where
T: ToPointUtf16,
{
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
}
pub fn accept_completion(
&mut self,
completion: &Completion,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let server = match self.server.as_authenticated() {
Ok(server) => server,
Err(error) => return Task::ready(Err(error)),
};
let request =
server
.lsp
.request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
uuid: completion.uuid.clone(),
});
cx.background_spawn(async move {
request
.await
.into_response()
.context("copilot: notify accepted")?;
Ok(())
})
}
pub fn discard_completions(
&mut self,
completions: &[Completion],
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let server = match self.server.as_authenticated() {
Ok(server) => server,
Err(_) => return Task::ready(Ok(())),
};
let request =
server
.lsp
.request::<request::NotifyRejected>(request::NotifyRejectedParams {
uuids: completions
.iter()
.map(|completion| completion.uuid.clone())
.collect(),
});
cx.background_spawn(async move {
request
.await
.into_response()
.context("copilot: notify rejected")?;
Ok(())
})
}
fn request_completions<R, T>(
&mut self,
buffer: &Entity<Buffer>,
position: T,
cx: &mut Context<Self>,
) -> Task<Result<Vec<Completion>>>
where
R: 'static
+ lsp::request::Request<
Params = request::GetCompletionsParams,
Result = request::GetCompletionsResult,
>,
T: ToPointUtf16,
{
self.register_buffer(buffer, cx);
let server = match self.server.as_authenticated() {
Ok(server) => server,
Err(error) => return Task::ready(Err(error)),
};
let buffer_entity = buffer.clone();
let lsp = server.lsp.clone();
let registered_buffer = server
.registered_buffers
@@ -905,31 +977,46 @@ impl Copilot {
let buffer = buffer.read(cx);
let uri = registered_buffer.uri.clone();
let position = position.to_point_utf16(buffer);
let settings = language_settings(
buffer.language_at(position).map(|l| l.name()),
buffer.file(),
cx,
);
let tab_size = settings.tab_size;
let hard_tabs = settings.hard_tabs;
let relative_path = buffer
.file()
.map_or(RelPath::empty().into(), |file| file.path().clone());
cx.background_spawn(async move {
let (version, snapshot) = snapshot.await?;
let result = lsp
.request::<NextEditSuggestions>(request::NextEditSuggestionsParams {
text_document: lsp::VersionedTextDocumentIdentifier { uri, version },
position: point_to_lsp(position),
.request::<R>(request::GetCompletionsParams {
doc: request::GetCompletionsDocument {
uri,
tab_size: tab_size.into(),
indent_size: 1,
insert_spaces: !hard_tabs,
relative_path: relative_path.to_proto(),
position: point_to_lsp(position),
version: version.try_into().unwrap(),
},
})
.await
.into_response()
.context("copilot: get completions")?;
let completions = result
.edits
.completions
.into_iter()
.map(|completion| {
let start = snapshot
.clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
let end =
snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
CopilotEditPrediction {
buffer: buffer_entity.clone(),
Completion {
uuid: completion.uuid,
range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
text: completion.text,
command: completion.command,
snapshot: snapshot.clone(),
}
})
.collect();
@@ -937,35 +1024,6 @@ impl Copilot {
})
}
pub(crate) fn accept_completion(
&mut self,
completion: &CopilotEditPrediction,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let server = match self.server.as_authenticated() {
Ok(server) => server,
Err(error) => return Task::ready(Err(error)),
};
if let Some(command) = &completion.command {
let request = server
.lsp
.request::<lsp::ExecuteCommand>(lsp::ExecuteCommandParams {
command: command.command.clone(),
arguments: command.arguments.clone().unwrap_or_default(),
..Default::default()
});
cx.background_spawn(async move {
request
.await
.into_response()
.context("copilot: notify accepted")?;
Ok(())
})
} else {
Task::ready(Ok(()))
}
}
pub fn status(&self) -> Status {
match &self.server {
CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
@@ -1188,10 +1246,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
.await;
if should_install {
node_runtime
.npm_install_packages(
paths::copilot_dir(),
&[(PACKAGE_NAME, &latest_version.to_string())],
)
.npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)])
.await?;
}
@@ -1202,11 +1257,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
mod tests {
use super::*;
use gpui::TestAppContext;
use util::{
path,
paths::PathStyle,
rel_path::{RelPath, rel_path},
};
use util::{path, paths::PathStyle, rel_path::rel_path};
#[gpui::test(iterations = 10)]
async fn test_buffer_management(cx: &mut TestAppContext) {

View File

@@ -1,29 +1,49 @@
use crate::{Copilot, CopilotEditPrediction};
use crate::{Completion, Copilot};
use anyhow::Result;
use edit_prediction_types::{EditPrediction, EditPredictionDelegate, interpolate_edits};
use gpui::{App, Context, Entity, Task};
use language::{Anchor, Buffer, EditPreview, OffsetRangeExt};
use std::{ops::Range, sync::Arc, time::Duration};
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
use gpui::{App, Context, Entity, EntityId, Task};
use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings};
use settings::Settings;
use std::{path::Path, time::Duration};
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
pub struct CopilotEditPredictionDelegate {
completion: Option<(CopilotEditPrediction, EditPreview)>,
cycled: bool,
buffer_id: Option<EntityId>,
completions: Vec<Completion>,
active_completion_index: usize,
file_extension: Option<String>,
pending_refresh: Option<Task<Result<()>>>,
pending_cycling_refresh: Option<Task<Result<()>>>,
copilot: Entity<Copilot>,
}
impl CopilotEditPredictionDelegate {
pub fn new(copilot: Entity<Copilot>) -> Self {
Self {
completion: None,
cycled: false,
buffer_id: None,
completions: Vec::new(),
active_completion_index: 0,
file_extension: None,
pending_refresh: None,
pending_cycling_refresh: None,
copilot,
}
}
fn active_completion(&self) -> Option<&(CopilotEditPrediction, EditPreview)> {
self.completion.as_ref()
fn active_completion(&self) -> Option<&Completion> {
self.completions.get(self.active_completion_index)
}
fn push_completion(&mut self, new_completion: Completion) {
for completion in &self.completions {
if completion.text == new_completion.text && completion.range == new_completion.range {
return;
}
}
self.completions.push(new_completion);
}
}
@@ -44,8 +64,12 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
true
}
fn supports_jump_to_edit() -> bool {
false
}
fn is_refreshing(&self, _cx: &App) -> bool {
self.pending_refresh.is_some() && self.completion.is_none()
self.pending_refresh.is_some() && self.completions.is_empty()
}
fn is_enabled(
@@ -78,96 +102,160 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
})?
.await?;
if let Some(mut completion) = completions.into_iter().next()
&& let Some(trimmed_completion) = cx
.update(|cx| trim_completion(&completion, cx))
.ok()
.flatten()
{
let preview = buffer
.update(cx, |this, cx| {
this.preview_edits(Arc::from(std::slice::from_ref(&trimmed_completion)), cx)
})?
.await;
this.update(cx, |this, cx| {
this.update(cx, |this, cx| {
if !completions.is_empty() {
this.cycled = false;
this.pending_refresh = None;
completion.range = trimmed_completion.0;
completion.text = trimmed_completion.1.to_string();
this.completion = Some((completion, preview));
this.pending_cycling_refresh = None;
this.completions.clear();
this.active_completion_index = 0;
this.buffer_id = Some(buffer.entity_id());
this.file_extension = buffer.read(cx).file().and_then(|file| {
Some(
Path::new(file.file_name(cx))
.extension()?
.to_str()?
.to_string(),
)
});
for completion in completions {
this.push_completion(completion);
}
cx.notify();
})?;
}
}
})?;
Ok(())
}));
}
fn cycle(
&mut self,
buffer: Entity<Buffer>,
cursor_position: language::Anchor,
direction: Direction,
cx: &mut Context<Self>,
) {
if self.cycled {
match direction {
Direction::Prev => {
self.active_completion_index = if self.active_completion_index == 0 {
self.completions.len().saturating_sub(1)
} else {
self.active_completion_index - 1
};
}
Direction::Next => {
if self.completions.is_empty() {
self.active_completion_index = 0
} else {
self.active_completion_index =
(self.active_completion_index + 1) % self.completions.len();
}
}
}
cx.notify();
} else {
let copilot = self.copilot.clone();
self.pending_cycling_refresh = Some(cx.spawn(async move |this, cx| {
let completions = copilot
.update(cx, |copilot, cx| {
copilot.completions_cycling(&buffer, cursor_position, cx)
})?
.await?;
this.update(cx, |this, cx| {
this.cycled = true;
this.file_extension = buffer.read(cx).file().and_then(|file| {
Some(
Path::new(file.file_name(cx))
.extension()?
.to_str()?
.to_string(),
)
});
for completion in completions {
this.push_completion(completion);
}
this.cycle(buffer, cursor_position, direction, cx);
})?;
Ok(())
}));
}
}
fn accept(&mut self, cx: &mut Context<Self>) {
if let Some((completion, _)) = self.active_completion() {
if let Some(completion) = self.active_completion() {
self.copilot
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
.detach_and_log_err(cx);
}
}
fn discard(&mut self, _: &mut Context<Self>) {}
fn discard(&mut self, cx: &mut Context<Self>) {
let settings = AllLanguageSettings::get_global(cx);
let copilot_enabled = settings.show_edit_predictions(None, cx);
if !copilot_enabled {
return;
}
self.copilot
.update(cx, |copilot, cx| {
copilot.discard_completions(&self.completions, cx)
})
.detach_and_log_err(cx);
}
fn suggest(
&mut self,
buffer: &Entity<Buffer>,
_: language::Anchor,
cursor_position: language::Anchor,
cx: &mut Context<Self>,
) -> Option<EditPrediction> {
let buffer_id = buffer.entity_id();
let buffer = buffer.read(cx);
let (completion, edit_preview) = self.active_completion()?;
if Some(buffer_id) != Some(completion.buffer.entity_id())
let completion = self.active_completion()?;
if Some(buffer_id) != self.buffer_id
|| !completion.range.start.is_valid(buffer)
|| !completion.range.end.is_valid(buffer)
{
return None;
}
let edits = vec![(
completion.range.clone(),
Arc::from(completion.text.as_ref()),
)];
let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits)
.filter(|edits| !edits.is_empty())?;
Some(EditPrediction::Local {
id: None,
edits,
edit_preview: Some(edit_preview.clone()),
})
}
}
let mut completion_range = completion.range.to_offset(buffer);
let prefix_len = common_prefix(
buffer.chars_for_range(completion_range.clone()),
completion.text.chars(),
);
completion_range.start += prefix_len;
let suffix_len = common_prefix(
buffer.reversed_chars_for_range(completion_range.clone()),
completion.text[prefix_len..].chars().rev(),
);
completion_range.end = completion_range.end.saturating_sub(suffix_len);
fn trim_completion(
completion: &CopilotEditPrediction,
cx: &mut App,
) -> Option<(Range<Anchor>, Arc<str>)> {
let buffer = completion.buffer.read(cx);
let mut completion_range = completion.range.to_offset(buffer);
let prefix_len = common_prefix(
buffer.chars_for_range(completion_range.clone()),
completion.text.chars(),
);
completion_range.start += prefix_len;
let suffix_len = common_prefix(
buffer.reversed_chars_for_range(completion_range.clone()),
completion.text[prefix_len..].chars().rev(),
);
completion_range.end = completion_range.end.saturating_sub(suffix_len);
let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
if completion_text.trim().is_empty() {
None
} else {
let completion_range =
buffer.anchor_after(completion_range.start)..buffer.anchor_after(completion_range.end);
Some((completion_range, Arc::from(completion_text)))
if completion_range.is_empty()
&& completion_range.start == cursor_position.to_offset(buffer)
{
let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
if completion_text.trim().is_empty() {
None
} else {
let position = cursor_position.bias_right(buffer);
Some(EditPrediction::Local {
id: None,
edits: vec![(position..position, completion_text.into())],
edit_preview: None,
})
}
} else {
None
}
}
}
@@ -194,7 +282,6 @@ mod tests {
Point,
language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode},
};
use lsp::Uri;
use project::Project;
use serde_json::json;
use settings::{AllLanguageSettingsContent, SettingsStore};
@@ -250,15 +337,12 @@ mod tests {
));
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::NextEditSuggestion {
vec![crate::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, window, cx| {
@@ -299,15 +383,12 @@ mod tests {
));
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::NextEditSuggestion {
vec![crate::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, _, cx| {
@@ -331,15 +412,12 @@ mod tests {
// After debouncing, new Copilot completions should be requested.
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::NextEditSuggestion {
vec![crate::request::Completion {
text: "one.copilot2".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, window, cx| {
@@ -401,6 +479,45 @@ mod tests {
assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
});
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
cx.update_editor(|editor, window, cx| {
editor.set_text("fn foo() {\n \n}", window, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
});
});
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::Completion {
text: " let x = 4;".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
..Default::default()
}],
vec![],
);
cx.update_editor(|editor, window, cx| {
editor.next_edit_prediction(&Default::default(), window, cx)
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, window, cx| {
assert!(editor.has_active_edit_prediction());
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
// Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
editor.tab(&Default::default(), window, cx);
assert!(editor.has_active_edit_prediction());
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
// Using AcceptEditPrediction again accepts the suggestion.
editor.accept_edit_prediction(&Default::default(), window, cx);
assert!(!editor.has_active_edit_prediction());
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
});
}
#[gpui::test(iterations = 10)]
@@ -453,15 +570,12 @@ mod tests {
));
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::NextEditSuggestion {
vec![crate::request::Completion {
text: "one.copilot1".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, window, cx| {
@@ -500,15 +614,12 @@ mod tests {
));
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::NextEditSuggestion {
vec![crate::request::Completion {
text: "one.123. copilot\n 456".into(),
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, window, cx| {
@@ -575,18 +686,15 @@ mod tests {
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::NextEditSuggestion {
vec![crate::request::Completion {
text: "two.foo()".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
vec![],
);
cx.update_editor(|editor, window, cx| {
editor.show_edit_prediction(&Default::default(), window, cx)
editor.next_edit_prediction(&Default::default(), window, cx)
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, window, cx| {
@@ -595,22 +703,15 @@ mod tests {
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
editor.backspace(&Default::default(), window, cx);
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
assert!(editor.has_active_edit_prediction());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\nt\nthree\n");
editor.backspace(&Default::default(), window, cx);
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
assert!(editor.has_active_edit_prediction());
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
assert_eq!(editor.text(cx), "one\n\nthree\n");
// Deleting across the original suggestion range invalidates it.
editor.backspace(&Default::default(), window, cx);
assert!(!editor.has_active_edit_prediction());
@@ -652,7 +753,7 @@ mod tests {
editor
.update(cx, |editor, window, cx| {
use gpui::Focusable;
window.focus(&editor.focus_handle(cx), cx);
window.focus(&editor.focus_handle(cx));
})
.unwrap();
let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
@@ -664,22 +765,19 @@ mod tests {
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::NextEditSuggestion {
vec![crate::request::Completion {
text: "b = 2 + a".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
vec![],
);
_ = editor.update(cx, |editor, window, cx| {
// Ensure copilot suggestions are shown for the first excerpt.
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
});
editor.show_edit_prediction(&Default::default(), window, cx);
editor.next_edit_prediction(&Default::default(), window, cx);
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
_ = editor.update(cx, |editor, _, cx| {
@@ -693,15 +791,12 @@ mod tests {
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::NextEditSuggestion {
vec![crate::request::Completion {
text: "d = 4 + c".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
vec![],
);
_ = editor.update(cx, |editor, window, cx| {
// Move to another excerpt, ensuring the suggestion gets cleared.
@@ -778,18 +873,15 @@ mod tests {
));
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::NextEditSuggestion {
vec![crate::request::Completion {
text: "two.foo()".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
vec![],
);
cx.update_editor(|editor, window, cx| {
editor.show_edit_prediction(&Default::default(), window, cx)
editor.next_edit_prediction(&Default::default(), window, cx)
});
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, _, cx| {
@@ -811,15 +903,12 @@ mod tests {
));
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::NextEditSuggestion {
vec![crate::request::Completion {
text: "two.foo()".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, _, cx| {
@@ -841,15 +930,12 @@ mod tests {
));
handle_copilot_completion_request(
&copilot_lsp,
vec![crate::request::NextEditSuggestion {
vec![crate::request::Completion {
text: "two.foo()".into(),
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
vec![],
);
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
cx.update_editor(|editor, _, cx| {
@@ -914,7 +1000,7 @@ mod tests {
editor
.update(cx, |editor, window, cx| {
use gpui::Focusable;
window.focus(&editor.focus_handle(cx), cx)
window.focus(&editor.focus_handle(cx))
})
.unwrap();
let copilot_provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot));
@@ -925,20 +1011,16 @@ mod tests {
.unwrap();
let mut copilot_requests = copilot_lsp
.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
.set_request_handler::<crate::request::GetCompletions, _, _>(
move |_params, _cx| async move {
Ok(crate::request::NextEditSuggestionsResult {
edits: vec![crate::request::NextEditSuggestion {
Ok(crate::request::GetCompletionsResult {
completions: vec![crate::request::Completion {
text: "next line".into(),
range: lsp::Range::new(
lsp::Position::new(1, 0),
lsp::Position::new(1, 0),
),
command: None,
text_document: lsp::VersionedTextDocumentIdentifier {
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: 0,
},
..Default::default()
}],
})
},
@@ -967,14 +1049,23 @@ mod tests {
fn handle_copilot_completion_request(
lsp: &lsp::FakeLanguageServer,
completions: Vec<crate::request::NextEditSuggestion>,
completions: Vec<crate::request::Completion>,
completions_cycling: Vec<crate::request::Completion>,
) {
lsp.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
let completions = completions.clone();
async move {
Ok(crate::request::GetCompletionsResult {
completions: completions.clone(),
})
}
});
lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
move |_params, _cx| {
let completions = completions.clone();
let completions_cycling = completions_cycling.clone();
async move {
Ok(crate::request::NextEditSuggestionsResult {
edits: completions.clone(),
Ok(crate::request::GetCompletionsResult {
completions: completions_cycling.clone(),
})
}
},

View File

@@ -1,4 +1,3 @@
use lsp::VersionedTextDocumentIdentifier;
use serde::{Deserialize, Serialize};
pub enum CheckStatus {}
@@ -89,6 +88,72 @@ impl lsp::request::Request for SignOut {
const METHOD: &'static str = "signOut";
}
pub enum GetCompletions {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsParams {
pub doc: GetCompletionsDocument,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsDocument {
pub tab_size: u32,
pub indent_size: u32,
pub insert_spaces: bool,
pub uri: lsp::Uri,
pub relative_path: String,
pub position: lsp::Position,
pub version: usize,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCompletionsResult {
pub completions: Vec<Completion>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Completion {
pub text: String,
pub position: lsp::Position,
pub uuid: String,
pub range: lsp::Range,
pub display_text: String,
}
impl lsp::request::Request for GetCompletions {
type Params = GetCompletionsParams;
type Result = GetCompletionsResult;
const METHOD: &'static str = "getCompletions";
}
pub enum GetCompletionsCycling {}
impl lsp::request::Request for GetCompletionsCycling {
type Params = GetCompletionsParams;
type Result = GetCompletionsResult;
const METHOD: &'static str = "getCompletionsCycling";
}
pub enum LogMessage {}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogMessageParams {
pub level: u8,
pub message: String,
pub metadata_str: String,
pub extra: Vec<String>,
}
impl lsp::notification::Notification for LogMessage {
type Params = LogMessageParams;
const METHOD: &'static str = "LogMessage";
}
pub enum StatusNotification {}
#[derive(Debug, Serialize, Deserialize)]
@@ -158,36 +223,3 @@ impl lsp::request::Request for NotifyRejected {
type Result = String;
const METHOD: &'static str = "notifyRejected";
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NextEditSuggestions;
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NextEditSuggestionsParams {
pub(crate) text_document: VersionedTextDocumentIdentifier,
pub(crate) position: lsp::Position,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NextEditSuggestion {
pub text: String,
pub text_document: VersionedTextDocumentIdentifier,
pub range: lsp::Range,
pub command: Option<lsp::Command>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NextEditSuggestionsResult {
pub edits: Vec<NextEditSuggestion>,
}
impl lsp::request::Request for NextEditSuggestions {
type Params = NextEditSuggestionsParams;
type Result = NextEditSuggestionsResult;
const METHOD: &'static str = "textDocument/copilotInlineEdit";
}

View File

@@ -435,8 +435,8 @@ impl Render for CopilotCodeVerification {
.on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
window.focus(&this.focus_handle, cx);
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _| {
window.focus(&this.focus_handle);
}))
.child(
Vector::new(VectorName::ZedXCopilot, rems(8.), rems(4.))

View File

@@ -577,7 +577,7 @@ impl DebugPanel {
menu
});
window.focus(&context_menu.focus_handle(cx), cx);
window.focus(&context_menu.focus_handle(cx));
let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
this.context_menu.take();
cx.notify();
@@ -1052,7 +1052,7 @@ impl DebugPanel {
cx: &mut Context<Self>,
) {
debug_assert!(self.sessions_with_children.contains_key(&session_item));
session_item.focus_handle(cx).focus(window, cx);
session_item.focus_handle(cx).focus(window);
session_item.update(cx, |this, cx| {
this.running_state().update(cx, |this, cx| {
this.go_to_selected_stack_frame(window, cx);
@@ -1579,10 +1579,8 @@ impl Panel for DebugPanel {
Some(proto::PanelId::DebugPanel)
}
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
DebuggerSettings::get_global(cx)
.button
.then_some(IconName::Debug)
fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
Some(IconName::Debug)
}
fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {

View File

@@ -574,7 +574,7 @@ impl Render for NewProcessModal {
NewProcessMode::Launch => NewProcessMode::Task,
};
this.mode_focus_handle(cx).focus(window, cx);
this.mode_focus_handle(cx).focus(window);
}))
.on_action(
cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
@@ -585,7 +585,7 @@ impl Render for NewProcessModal {
NewProcessMode::Launch => NewProcessMode::Attach,
};
this.mode_focus_handle(cx).focus(window, cx);
this.mode_focus_handle(cx).focus(window);
}),
)
.child(
@@ -602,7 +602,7 @@ impl Render for NewProcessModal {
NewProcessMode::Task.to_string(),
cx.listener(|this, _, window, cx| {
this.mode = NewProcessMode::Task;
this.mode_focus_handle(cx).focus(window, cx);
this.mode_focus_handle(cx).focus(window);
cx.notify();
}),
)
@@ -611,7 +611,7 @@ impl Render for NewProcessModal {
NewProcessMode::Debug.to_string(),
cx.listener(|this, _, window, cx| {
this.mode = NewProcessMode::Debug;
this.mode_focus_handle(cx).focus(window, cx);
this.mode_focus_handle(cx).focus(window);
cx.notify();
}),
)
@@ -629,7 +629,7 @@ impl Render for NewProcessModal {
cx,
);
}
this.mode_focus_handle(cx).focus(window, cx);
this.mode_focus_handle(cx).focus(window);
cx.notify();
}),
)
@@ -638,7 +638,7 @@ impl Render for NewProcessModal {
NewProcessMode::Launch.to_string(),
cx.listener(|this, _, window, cx| {
this.mode = NewProcessMode::Launch;
this.mode_focus_handle(cx).focus(window, cx);
this.mode_focus_handle(cx).focus(window);
cx.notify();
}),
)
@@ -840,17 +840,17 @@ impl ConfigureMode {
}
}
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
window.focus_next(cx);
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
}
fn on_tab_prev(
&mut self,
_: &menu::SelectPrevious,
window: &mut Window,
cx: &mut Context<Self>,
_: &mut Context<Self>,
) {
window.focus_prev(cx);
window.focus_prev();
}
fn render(
@@ -923,7 +923,7 @@ impl AttachMode {
window,
cx,
);
window.focus(&modal.focus_handle(cx), cx);
window.focus(&modal.focus_handle(cx));
modal
});

View File

@@ -83,8 +83,8 @@ impl Render for DebuggerOnboardingModal {
debugger_onboarding_event!("Canceled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| {
this.focus_handle.focus(window, cx);
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(
div()

View File

@@ -604,7 +604,7 @@ impl DebugTerminal {
let focus_handle = cx.focus_handle();
let focus_subscription = cx.on_focus(&focus_handle, window, |this, window, cx| {
if let Some(terminal) = this.terminal.as_ref() {
terminal.focus_handle(cx).focus(window, cx);
terminal.focus_handle(cx).focus(window);
}
});

View File

@@ -310,7 +310,7 @@ impl BreakpointList {
fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
if self.input.focus_handle(cx).contains_focused(window, cx) {
self.focus_handle.focus(window, cx);
self.focus_handle.focus(window);
} else if self.strip_mode.is_some() {
self.strip_mode.take();
cx.notify();
@@ -364,9 +364,9 @@ impl BreakpointList {
}
}
}
self.focus_handle.focus(window, cx);
self.focus_handle.focus(window);
} else {
handle.focus(window, cx);
handle.focus(window);
}
return;
@@ -627,7 +627,7 @@ impl BreakpointList {
.on_click({
let focus_handle = focus_handle.clone();
move |_, window, cx| {
focus_handle.focus(window, cx);
focus_handle.focus(window);
window.dispatch_action(ToggleEnableBreakpoint.boxed_clone(), cx)
}
}),
@@ -654,7 +654,7 @@ impl BreakpointList {
)
.on_click({
move |_, window, cx| {
focus_handle.focus(window, cx);
focus_handle.focus(window);
window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
}
}),

View File

@@ -105,7 +105,7 @@ impl Console {
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
cx.on_focus(&focus_handle, window, |console, window, cx| {
if console.is_running(cx) {
console.query_bar.focus_handle(cx).focus(window, cx);
console.query_bar.focus_handle(cx).focus(window);
}
}),
];

View File

@@ -403,7 +403,7 @@ impl MemoryView {
this.set_placeholder_text("Write to Selected Memory Range", window, cx);
});
self.is_writing_memory = true;
self.query_editor.focus_handle(cx).focus(window, cx);
self.query_editor.focus_handle(cx).focus(window);
} else {
self.query_editor.update(cx, |this, cx| {
this.clear(window, cx);

View File

@@ -529,7 +529,7 @@ impl VariableList {
fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
self.edited_path.take();
self.focus_handle.focus(window, cx);
self.focus_handle.focus(window);
cx.notify();
}
@@ -1067,7 +1067,7 @@ impl VariableList {
editor.select_all(&editor::actions::SelectAll, window, cx);
editor
});
editor.focus_handle(cx).focus(window, cx);
editor.focus_handle(cx).focus(window);
editor
}

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