Compare commits
92 Commits
provider-e
...
collab-sta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
554382a87e | ||
|
|
9677da9a60 | ||
|
|
9e5546edc5 | ||
|
|
c7203f7a60 | ||
|
|
c87f12ee42 | ||
|
|
084d01b5f0 | ||
|
|
ed7360206f | ||
|
|
9f599466b5 | ||
|
|
dffda91838 | ||
|
|
6b3412232f | ||
|
|
963cb2c200 | ||
|
|
0c7d639cbd | ||
|
|
189b5ff3a2 | ||
|
|
a3bf58d393 | ||
|
|
d844f5db30 | ||
|
|
fc45f6e042 | ||
|
|
843be766df | ||
|
|
469ad070b0 | ||
|
|
392b6184bf | ||
|
|
fc89e19098 | ||
|
|
f53b01d5a2 | ||
|
|
bf1c8819d9 | ||
|
|
3247264288 | ||
|
|
6d947b7746 | ||
|
|
db221ca72d | ||
|
|
1d006a8cb0 | ||
|
|
aaab9f6960 | ||
|
|
209cf0a48f | ||
|
|
260691c99c | ||
|
|
9e88f3f33c | ||
|
|
2cad6c8ef1 | ||
|
|
bc24ffe863 | ||
|
|
1e4a970ae2 | ||
|
|
3e656a0911 | ||
|
|
57ea23d161 | ||
|
|
a50c5b2c10 | ||
|
|
f1b723973b | ||
|
|
a7ce677ac3 | ||
|
|
ed67f246cb | ||
|
|
93f29326c4 | ||
|
|
85f4681299 | ||
|
|
741c5d5010 | ||
|
|
f03987fb68 | ||
|
|
ca47822667 | ||
|
|
a34fe06bb1 | ||
|
|
0ce484e66c | ||
|
|
251033f88f | ||
|
|
9f90c1a1b7 | ||
|
|
d43cc46288 | ||
|
|
fdb8e71b43 | ||
|
|
6bc433ed43 | ||
|
|
1281f4672c | ||
|
|
ed705c0cbc | ||
|
|
8980333e23 | ||
|
|
acee48bfda | ||
|
|
71298e6949 | ||
|
|
07ada58466 | ||
|
|
dd521a96fb | ||
|
|
f9d9721b93 | ||
|
|
cff3ac6f93 | ||
|
|
746b76488c | ||
|
|
397fcf6083 | ||
|
|
9adb3e1daa | ||
|
|
1469d94683 | ||
|
|
3b626c8ac1 | ||
|
|
3dc0614dba | ||
|
|
045e154915 | ||
|
|
dc72e1c4ba | ||
|
|
0884305e43 | ||
|
|
83449293b6 | ||
|
|
213cb30445 | ||
|
|
4b56fec971 | ||
|
|
32621dc5de | ||
|
|
215ac50bc8 | ||
|
|
a5540a08fb | ||
|
|
3e8c25f5a9 | ||
|
|
7f0842e3a6 | ||
|
|
6dad419cd5 | ||
|
|
0facdfa5ca | ||
|
|
58461377ca | ||
|
|
42d5f7e73e | ||
|
|
5395197619 | ||
|
|
1d76539d28 | ||
|
|
e5eb26e8d6 | ||
|
|
a86b0ab2e0 | ||
|
|
5fb220a19a | ||
|
|
12dbbdd1d3 | ||
|
|
6dfabddbb4 | ||
|
|
895213a94d | ||
|
|
1c576ccf82 | ||
|
|
3f4da03d38 | ||
|
|
ff71f4d46d |
1
.github/actionlint.yml
vendored
1
.github/actionlint.yml
vendored
@@ -25,6 +25,7 @@ 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
|
||||
|
||||
4
.github/workflows/autofix_pr.yml
vendored
4
.github/workflows/autofix_pr.yml
vendored
@@ -54,6 +54,10 @@ 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
|
||||
|
||||
54
.github/workflows/docs_automation.yml
vendored
54
.github/workflows/docs_automation.yml
vendored
@@ -1,11 +1,11 @@
|
||||
name: Documentation Automation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'crates/**'
|
||||
- 'extensions/**'
|
||||
# push:
|
||||
# branches: [main]
|
||||
# paths:
|
||||
# - 'crates/**'
|
||||
# - 'extensions/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
docs-automation:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
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:
|
||||
@@ -102,24 +102,24 @@ jobs:
|
||||
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"
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
> /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"
|
||||
@@ -163,10 +163,10 @@ jobs:
|
||||
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)
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
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 \
|
||||
@@ -194,17 +194,17 @@ jobs:
|
||||
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..."
|
||||
@@ -214,10 +214,10 @@ jobs:
|
||||
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)
|
||||
@@ -228,13 +228,13 @@ jobs:
|
||||
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
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
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
|
||||
|
||||
6
.github/workflows/extension_bump.yml
vendored
6
.github/workflows/extension_bump.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
if: |-
|
||||
(github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') &&
|
||||
(inputs.force-bump == 'true' || needs.check_bump_needed.outputs.needs_bump == 'true')
|
||||
runs-on: namespace-profile-8x16-ubuntu-2204
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: generate-token
|
||||
name: extension_bump::generate_token
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
- name: extension_bump::install_bump_2_version
|
||||
run: pip install bump2version
|
||||
run: pip install bump2version --break-system-packages
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: bump-version
|
||||
name: extension_bump::bump_version
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
needs:
|
||||
- check_bump_needed
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.check_bump_needed.outputs.needs_bump == 'false'
|
||||
runs-on: namespace-profile-8x16-ubuntu-2204
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: generate-token
|
||||
name: extension_bump::generate_token
|
||||
|
||||
2
.github/workflows/extension_release.yml
vendored
2
.github/workflows/extension_release.yml
vendored
@@ -13,7 +13,7 @@ on:
|
||||
jobs:
|
||||
create_release:
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||
runs-on: namespace-profile-8x16-ubuntu-2204
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: generate-token
|
||||
name: extension_bump::generate_token
|
||||
|
||||
4
.github/workflows/extension_tests.yml
vendored
4
.github/workflows/extension_tests.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
needs:
|
||||
- orchestrate
|
||||
if: needs.orchestrate.outputs.check_rust == 'true'
|
||||
runs-on: namespace-profile-16x32-ubuntu-2204
|
||||
runs-on: namespace-profile-4x8-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-2x4-ubuntu-2404
|
||||
runs-on: namespace-profile-8x32-ubuntu-2404
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
106
.github/workflows/extension_workflow_rollout.yml
vendored
Normal file
106
.github/workflows/extension_workflow_rollout.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
# Generated from xtask::workflows::extension_workflow_rollout
|
||||
# Rebuild with `cargo xtask workflows`.
|
||||
name: extension_workflow_rollout
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
jobs:
|
||||
fetch_extension_repos:
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: list-repos
|
||||
name: extension_workflow_rollout::fetch_extension_repos::get_repositories
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const repos = await github.paginate(github.rest.repos.listForOrg, {
|
||||
org: 'zed-extensions',
|
||||
type: 'public',
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const filteredRepos = repos
|
||||
.filter(repo => !repo.archived)
|
||||
.filter(repo => repo.name !== 'workflows' && repo.name !== 'material-icon-theme')
|
||||
.map(repo => repo.name);
|
||||
|
||||
console.log(`Found ${filteredRepos.length} extension repos`);
|
||||
return filteredRepos;
|
||||
result-encoding: json
|
||||
outputs:
|
||||
repos: ${{ steps.list-repos.outputs.result }}
|
||||
timeout-minutes: 5
|
||||
rollout_workflows_to_extension:
|
||||
needs:
|
||||
- fetch_extension_repos
|
||||
if: needs.fetch_extension_repos.outputs.repos != '[]'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
strategy:
|
||||
matrix:
|
||||
repo: ${{ fromJson(needs.fetch_extension_repos.outputs.repos) }}
|
||||
fail-fast: false
|
||||
max-parallel: 5
|
||||
steps:
|
||||
- id: generate-token
|
||||
name: extension_bump::generate_token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
owner: zed-extensions
|
||||
repositories: ${{ matrix.repo }}
|
||||
permission-pull-requests: write
|
||||
permission-contents: write
|
||||
permission-workflows: write
|
||||
- name: checkout_zed_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
path: zed
|
||||
- name: steps::checkout_repo_with_token
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
clean: false
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
repository: zed-extensions/${{ matrix.repo }}
|
||||
path: extension
|
||||
- name: extension_workflow_rollout::rollout_workflows_to_extension::copy_workflow_files
|
||||
run: |
|
||||
mkdir -p extension/.github/workflows
|
||||
cp zed/extensions/workflows/shared/*.yml extension/.github/workflows/
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: short-sha
|
||||
name: extension_workflow_rollout::rollout_workflows_to_extension::get_short_sha
|
||||
run: |
|
||||
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
working-directory: zed
|
||||
- id: create-pr
|
||||
name: extension_workflow_rollout::rollout_workflows_to_extension::create_pull_request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
path: extension
|
||||
title: Update CI workflows to `zed@${{ steps.short-sha.outputs.sha_short }}`
|
||||
body: |
|
||||
This PR updates the CI workflow files from the main Zed repository
|
||||
based on the commit zed-industries/zed@${{ github.sha }}
|
||||
commit-message: Update CI workflows to `zed@${{ steps.short-sha.outputs.sha_short }}`
|
||||
branch: update-workflows
|
||||
committer: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
|
||||
author: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
|
||||
base: main
|
||||
delete-branch: true
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
sign-commits: true
|
||||
- name: extension_workflow_rollout::rollout_workflows_to_extension::enable_auto_merge
|
||||
run: |
|
||||
PR_NUMBER="${{ steps.create-pr.outputs.pull-request-number }}"
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
cd extension
|
||||
gh pr merge "$PR_NUMBER" --auto --squash
|
||||
fi
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
timeout-minutes: 10
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
|
||||
jobs:
|
||||
handle-good-first-issue:
|
||||
if: github.event.label.name == 'good first issue' && github.repository_owner == 'zed-industries'
|
||||
if: github.event.label.name == '.contrib/good first issue' && github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
@@ -23,7 +23,6 @@ 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).
|
||||
|
||||
|
||||
59
Cargo.lock
generated
59
Cargo.lock
generated
@@ -268,6 +268,7 @@ dependencies = [
|
||||
"client",
|
||||
"collections",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -2380,7 +2381,6 @@ dependencies = [
|
||||
name = "buffer_diff"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clock",
|
||||
"ctor",
|
||||
"futures 0.3.31",
|
||||
@@ -3525,6 +3525,33 @@ 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"
|
||||
@@ -5185,6 +5212,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"brotli",
|
||||
"buffer_diff",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_api_types",
|
||||
@@ -5222,7 +5250,9 @@ dependencies = [
|
||||
"strum 0.27.2",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"ui",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -5327,8 +5357,10 @@ dependencies = [
|
||||
"anyhow",
|
||||
"buffer_diff",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"codestral",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"copilot",
|
||||
"edit_prediction",
|
||||
@@ -5337,18 +5369,20 @@ dependencies = [
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"git",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
"log",
|
||||
"language_model",
|
||||
"lsp",
|
||||
"markdown",
|
||||
"menu",
|
||||
"multi_buffer",
|
||||
"paths",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"semver",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"supermaven",
|
||||
@@ -5361,6 +5395,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zeta_prompt",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5924,11 +5959,9 @@ dependencies = [
|
||||
"async-trait",
|
||||
"client",
|
||||
"collections",
|
||||
"credentials_provider",
|
||||
"criterion",
|
||||
"ctor",
|
||||
"dap",
|
||||
"dirs 4.0.0",
|
||||
"extension",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
@@ -5937,11 +5970,8 @@ dependencies = [
|
||||
"http_client",
|
||||
"language",
|
||||
"language_extension",
|
||||
"language_model",
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown",
|
||||
"menu",
|
||||
"moka",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
@@ -5956,21 +5986,17 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"smol",
|
||||
"task",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"theme_extension",
|
||||
"toml 0.8.23",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"url",
|
||||
"util",
|
||||
"wasmparser 0.221.3",
|
||||
"wasmtime",
|
||||
"wasmtime-wasi",
|
||||
"workspace",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
@@ -8627,6 +8653,7 @@ dependencies = [
|
||||
"extension",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp",
|
||||
"paths",
|
||||
"project",
|
||||
"schemars",
|
||||
@@ -14882,7 +14909,6 @@ dependencies = [
|
||||
"copilot",
|
||||
"edit_prediction",
|
||||
"editor",
|
||||
"extension_host",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
@@ -14890,7 +14916,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"heck 0.5.0",
|
||||
"language",
|
||||
"language_model",
|
||||
"language_models",
|
||||
"log",
|
||||
"menu",
|
||||
@@ -20654,6 +20679,7 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette",
|
||||
"component",
|
||||
"component_preview",
|
||||
"copilot",
|
||||
"crashes",
|
||||
"dap",
|
||||
@@ -20759,7 +20785,6 @@ dependencies = [
|
||||
"tree-sitter-md",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"ui_prompt",
|
||||
"url",
|
||||
"urlencoding",
|
||||
@@ -20939,7 +20964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_glsl"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
@@ -20953,7 +20978,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_proto"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.7.0",
|
||||
]
|
||||
|
||||
@@ -39,6 +39,7 @@ members = [
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/component",
|
||||
"crates/component_preview",
|
||||
"crates/context_server",
|
||||
"crates/copilot",
|
||||
"crates/crashes",
|
||||
@@ -275,6 +276,7 @@ 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" }
|
||||
|
||||
@@ -55,6 +55,13 @@
|
||||
"down": "menu::SelectNext",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "menu",
|
||||
"bindings": {
|
||||
"right": "menu::SelectChild",
|
||||
"left": "menu::SelectParent",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
@@ -241,6 +248,7 @@
|
||||
"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",
|
||||
@@ -253,7 +261,6 @@
|
||||
"ctrl-y": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"ctrl-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -285,38 +292,6 @@
|
||||
"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",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"bindings": {
|
||||
@@ -331,14 +306,25 @@
|
||||
"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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -346,11 +332,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -817,7 +799,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"context": "InlineAssistant",
|
||||
"bindings": {
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "agent::CycleNextInlineAssist",
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
"ctrl-cmd-s": "workspace::ToggleWorktreeSecurity",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "menu",
|
||||
"bindings": {
|
||||
"right": "menu::SelectChild",
|
||||
"left": "menu::SelectParent",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -282,6 +289,7 @@
|
||||
"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",
|
||||
@@ -294,7 +302,6 @@
|
||||
"cmd-y": "agent::AllowOnce",
|
||||
"cmd-alt-y": "agent::AllowAlways",
|
||||
"cmd-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -326,41 +333,6 @@
|
||||
"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",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"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,
|
||||
@@ -382,16 +354,25 @@
|
||||
"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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -399,11 +380,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-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",
|
||||
"enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -883,7 +860,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"context": "InlineAssistant > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
"down": "menu::SelectNext",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "menu",
|
||||
"bindings": {
|
||||
"right": "menu::SelectChild",
|
||||
"left": "menu::SelectParent",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -241,6 +248,7 @@
|
||||
"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",
|
||||
@@ -254,7 +262,6 @@
|
||||
"shift-alt-a": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"shift-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -287,41 +294,6 @@
|
||||
"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",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"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,
|
||||
@@ -337,16 +309,25 @@
|
||||
"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",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -354,11 +335,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-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",
|
||||
"enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -826,7 +803,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"context": "InlineAssistant",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistEditor",
|
||||
"context": "InlineAssistant > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "editor::Cancel",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistEditor",
|
||||
"context": "InlineAssistant > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-backspace": "editor::Cancel",
|
||||
|
||||
@@ -11,6 +11,7 @@ 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::*;
|
||||
@@ -883,6 +884,7 @@ pub enum AcpThreadEvent {
|
||||
Refusal,
|
||||
AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
|
||||
ModeUpdated(acp::SessionModeId),
|
||||
ConfigOptionsUpdated(Vec<acp::SessionConfigOption>),
|
||||
}
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
@@ -1192,6 +1194,10 @@ impl AcpThread {
|
||||
current_mode_id,
|
||||
..
|
||||
}) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)),
|
||||
acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate {
|
||||
config_options,
|
||||
..
|
||||
}) => cx.emit(AcpThreadEvent::ConfigOptionsUpdated(config_options)),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1992,37 +1998,42 @@ 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 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 {
|
||||
let Some((_, message)) = self.last_user_message() 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 new_checkpoint = new_checkpoint
|
||||
let Some(new_checkpoint) = new_checkpoint
|
||||
.await
|
||||
.context("failed to get new checkpoint")
|
||||
.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(())
|
||||
})??;
|
||||
}
|
||||
.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));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -2422,8 +2433,10 @@ 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```", value).into(),
|
||||
format!("```json\n{}\n```", pretty_json).into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
@@ -4066,4 +4079,67 @@ 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ use agent_client_protocol::{self as acp};
|
||||
use anyhow::Result;
|
||||
use collections::IndexMap;
|
||||
use gpui::{Entity, SharedString, Task};
|
||||
use language_model::{IconOrSvg, LanguageModelProviderId};
|
||||
use language_model::LanguageModelProviderId;
|
||||
use project::Project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc};
|
||||
use ui::App;
|
||||
use ui::{App, IconName};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
@@ -86,6 +86,14 @@ pub trait AgentConnection {
|
||||
None
|
||||
}
|
||||
|
||||
fn session_config_options(
|
||||
&self,
|
||||
_session_id: &acp::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionConfigOptions>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
}
|
||||
|
||||
@@ -125,6 +133,26 @@ pub trait AgentSessionModes {
|
||||
fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
pub trait AgentSessionConfigOptions {
|
||||
/// Get all current config options with their state
|
||||
fn config_options(&self) -> Vec<acp::SessionConfigOption>;
|
||||
|
||||
/// Set a config option value
|
||||
/// Returns the full updated list of config options
|
||||
fn set_config_option(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value: acp::SessionConfigValueId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<acp::SessionConfigOption>>>;
|
||||
|
||||
/// Whenever the config options are updated the receiver will be notified.
|
||||
/// Optional for agents that don't update their config options dynamically.
|
||||
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthRequired {
|
||||
pub description: Option<String>,
|
||||
@@ -202,12 +230,15 @@ pub trait AgentModelSelector: 'static {
|
||||
fn should_render_footer(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
/// 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),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -215,7 +246,7 @@ pub struct AgentModelInfo {
|
||||
pub id: acp::ModelId,
|
||||
pub name: SharedString,
|
||||
pub description: Option<SharedString>,
|
||||
pub icon: Option<IconOrSvg>,
|
||||
pub icon: Option<AgentModelIcon>,
|
||||
}
|
||||
|
||||
impl From<acp::ModelInfo> for AgentModelInfo {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use buffer_diff::BufferDiff;
|
||||
use editor::{MultiBuffer, PathKey, multibuffer_context_lines};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer,
|
||||
Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, TextBuffer,
|
||||
};
|
||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||
use util::ResultExt;
|
||||
@@ -49,15 +49,15 @@ impl Diff {
|
||||
.update(cx, |multibuffer, cx| {
|
||||
let hunk_ranges = {
|
||||
let buffer = buffer.read(cx);
|
||||
let diff = diff.read(cx);
|
||||
diff.hunks_intersecting_range(
|
||||
Anchor::min_for_buffer(buffer.remote_id())
|
||||
..Anchor::max_for_buffer(buffer.remote_id()),
|
||||
buffer,
|
||||
cx,
|
||||
)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
|
||||
.collect::<Vec<_>>()
|
||||
diff.read(cx)
|
||||
.snapshot(cx)
|
||||
.hunks_intersecting_range(
|
||||
Anchor::min_for_buffer(buffer.remote_id())
|
||||
..Anchor::max_for_buffer(buffer.remote_id()),
|
||||
buffer,
|
||||
)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
multibuffer.set_excerpts_for_path(
|
||||
@@ -86,17 +86,9 @@ impl Diff {
|
||||
|
||||
pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
|
||||
let buffer_text_snapshot = buffer.read(cx).text_snapshot();
|
||||
let base_text_snapshot = buffer.read(cx).snapshot();
|
||||
let base_text = base_text_snapshot.text();
|
||||
debug_assert_eq!(buffer_text_snapshot.text(), base_text);
|
||||
let buffer_diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot);
|
||||
let snapshot = diff.snapshot(cx);
|
||||
let secondary_diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer_text_snapshot, cx);
|
||||
diff.set_snapshot(snapshot, &buffer_text_snapshot, cx);
|
||||
diff
|
||||
});
|
||||
let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, cx);
|
||||
let secondary_diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_text_snapshot, cx));
|
||||
diff.set_secondary_diff(secondary_diff);
|
||||
diff
|
||||
});
|
||||
@@ -109,7 +101,7 @@ impl Diff {
|
||||
|
||||
Self::Pending(PendingDiff {
|
||||
multibuffer,
|
||||
base_text: Arc::new(base_text),
|
||||
base_text: Arc::from(buffer_text_snapshot.text().as_str()),
|
||||
_subscription: cx.observe(&buffer, |this, _, cx| {
|
||||
if let Diff::Pending(diff) = this {
|
||||
diff.update(cx);
|
||||
@@ -176,7 +168,7 @@ impl Diff {
|
||||
new_buffer,
|
||||
..
|
||||
}) => {
|
||||
base_text.as_str() != old_text
|
||||
base_text.as_ref() != old_text
|
||||
|| !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
|
||||
}
|
||||
Diff::Finalized(FinalizedDiff {
|
||||
@@ -184,7 +176,7 @@ impl Diff {
|
||||
new_buffer,
|
||||
..
|
||||
}) => {
|
||||
base_text.as_str() != old_text
|
||||
base_text.as_ref() != old_text
|
||||
|| !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
|
||||
}
|
||||
}
|
||||
@@ -193,7 +185,7 @@ impl Diff {
|
||||
|
||||
pub struct PendingDiff {
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
base_text: Arc<String>,
|
||||
base_text: Arc<str>,
|
||||
new_buffer: Entity<Buffer>,
|
||||
diff: Entity<BufferDiff>,
|
||||
revealed_ranges: Vec<Range<Anchor>>,
|
||||
@@ -208,21 +200,22 @@ impl PendingDiff {
|
||||
let base_text = self.base_text.clone();
|
||||
self.update_diff = cx.spawn(async move |diff, cx| {
|
||||
let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?;
|
||||
let diff_snapshot = BufferDiff::update_diff(
|
||||
buffer_diff.clone(),
|
||||
text_snapshot.clone(),
|
||||
Some(base_text),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
let language = buffer.read_with(cx, |buffer, _| buffer.language().cloned())?;
|
||||
let update = buffer_diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
text_snapshot.clone(),
|
||||
Some(base_text.clone()),
|
||||
false,
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
buffer_diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
|
||||
diff.set_snapshot(update.clone(), &text_snapshot, cx);
|
||||
diff.secondary_diff().unwrap().update(cx, |diff, cx| {
|
||||
diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
|
||||
diff.set_snapshot(update, &text_snapshot, cx);
|
||||
});
|
||||
})?;
|
||||
diff.update(cx, |diff, cx| {
|
||||
@@ -319,13 +312,14 @@ impl PendingDiff {
|
||||
|
||||
fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
|
||||
let buffer = self.new_buffer.read(cx);
|
||||
let diff = self.diff.read(cx);
|
||||
let mut ranges = diff
|
||||
let mut ranges = self
|
||||
.diff
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.hunks_intersecting_range(
|
||||
Anchor::min_for_buffer(buffer.remote_id())
|
||||
..Anchor::max_for_buffer(buffer.remote_id()),
|
||||
buffer,
|
||||
cx,
|
||||
)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -357,60 +351,47 @@ impl PendingDiff {
|
||||
|
||||
pub struct FinalizedDiff {
|
||||
path: String,
|
||||
base_text: Arc<String>,
|
||||
base_text: Arc<str>,
|
||||
new_buffer: Entity<Buffer>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
_update_diff: Task<Result<()>>,
|
||||
}
|
||||
|
||||
async fn build_buffer_diff(
|
||||
old_text: Arc<String>,
|
||||
old_text: Arc<str>,
|
||||
buffer: &Entity<Buffer>,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<BufferDiff>> {
|
||||
let language = cx.update(|cx| buffer.read(cx).language().cloned())?;
|
||||
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
|
||||
|
||||
let old_text_rope = cx
|
||||
.background_spawn({
|
||||
let old_text = old_text.clone();
|
||||
async move { Rope::from(old_text.as_str()) }
|
||||
})
|
||||
.await;
|
||||
let base_buffer = cx
|
||||
.update(|cx| {
|
||||
Buffer::build_snapshot(
|
||||
old_text_rope,
|
||||
buffer.language().cloned(),
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
let secondary_diff = cx.new(|cx| BufferDiff::new(&buffer, cx))?;
|
||||
|
||||
let diff_snapshot = cx
|
||||
.update(|cx| {
|
||||
BufferDiffSnapshot::new_with_base_buffer(
|
||||
let update = secondary_diff
|
||||
.update(cx, |secondary_diff, cx| {
|
||||
secondary_diff.update_diff(
|
||||
buffer.text.clone(),
|
||||
Some(old_text),
|
||||
base_buffer,
|
||||
true,
|
||||
language.clone(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
let secondary_diff = cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer, cx);
|
||||
diff.set_snapshot(diff_snapshot.clone(), &buffer, cx);
|
||||
diff
|
||||
secondary_diff.update(cx, |secondary_diff, cx| {
|
||||
secondary_diff.language_changed(language.clone(), language_registry.clone(), cx);
|
||||
secondary_diff.set_snapshot(update.clone(), &buffer, cx);
|
||||
})?;
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&buffer.text, cx);
|
||||
diff.set_snapshot(diff_snapshot, &buffer, cx);
|
||||
let diff = cx.new(|cx| BufferDiff::new(&buffer, cx))?;
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.language_changed(language, language_registry, cx);
|
||||
diff.set_snapshot(update.clone(), &buffer, cx);
|
||||
diff.set_secondary_diff(secondary_diff);
|
||||
diff
|
||||
})
|
||||
})?;
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -4,22 +4,20 @@ use std::{
|
||||
fmt::Display,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
App, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment,
|
||||
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list,
|
||||
prelude::*,
|
||||
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
|
||||
StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Tooltip, WithScrollbar, prelude::*};
|
||||
use ui::{CopyButton, Tooltip, WithScrollbar, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
@@ -544,15 +542,11 @@ impl Render for AcpTools {
|
||||
|
||||
pub struct AcpToolsToolbarItemView {
|
||||
acp_tools: Option<Entity<AcpTools>>,
|
||||
just_copied: bool,
|
||||
}
|
||||
|
||||
impl AcpToolsToolbarItemView {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
acp_tools: None,
|
||||
just_copied: false,
|
||||
}
|
||||
Self { acp_tools: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,37 +566,14 @@ impl Render for AcpToolsToolbarItemView {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child({
|
||||
let acp_tools = acp_tools.clone();
|
||||
IconButton::new(
|
||||
"copy_all_messages",
|
||||
if self.just_copied {
|
||||
IconName::Check
|
||||
} else {
|
||||
IconName::Copy
|
||||
},
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(if self.just_copied {
|
||||
"Copied!"
|
||||
} else {
|
||||
"Copy All Messages"
|
||||
}))
|
||||
.disabled(!has_messages)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
if let Some(content) = acp_tools.read(cx).serialize_observed_messages() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(content));
|
||||
let message = acp_tools
|
||||
.read(cx)
|
||||
.serialize_observed_messages()
|
||||
.unwrap_or_default();
|
||||
|
||||
this.just_copied = true;
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.just_copied = false;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}))
|
||||
CopyButton::new(message)
|
||||
.tooltip_label("Copy All Messages")
|
||||
.disabled(!has_messages)
|
||||
})
|
||||
.child(
|
||||
IconButton::new("clear_messages", IconName::Trash)
|
||||
|
||||
@@ -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, DiskState, Point, ToPoint};
|
||||
use language::{Anchor, Buffer, BufferEvent, 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() == DiskState::Deleted)
|
||||
.is_some_and(|file| file.disk_state().is_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() != DiskState::Deleted)
|
||||
.is_some_and(|file| !file.disk_state().is_deleted())
|
||||
{
|
||||
// If the buffer had been deleted by a tool, but it got
|
||||
// resurrected externally, we want to clear the edits we
|
||||
@@ -262,7 +262,7 @@ impl ActionLog {
|
||||
);
|
||||
}
|
||||
|
||||
(Arc::new(base_text.to_string()), base_text)
|
||||
(Arc::from(base_text.to_string().as_str()), base_text)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -302,7 +302,7 @@ impl ActionLog {
|
||||
.context("buffer not tracked")?;
|
||||
let old_unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
|
||||
let agent_diff_base = tracked_buffer.diff_base.clone();
|
||||
let git_diff_base = git_diff.read(cx).base_text().as_rope().clone();
|
||||
let git_diff_base = git_diff.read(cx).base_text(cx).as_rope().clone();
|
||||
let buffer_text = tracked_buffer.snapshot.as_rope().clone();
|
||||
anyhow::Ok(cx.background_spawn(async move {
|
||||
let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable();
|
||||
@@ -352,7 +352,7 @@ impl ActionLog {
|
||||
}
|
||||
|
||||
(
|
||||
Arc::new(new_agent_diff_base.to_string()),
|
||||
Arc::from(new_agent_diff_base.to_string().as_str()),
|
||||
new_agent_diff_base,
|
||||
)
|
||||
}))
|
||||
@@ -374,11 +374,11 @@ impl ActionLog {
|
||||
this: &WeakEntity<ActionLog>,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_snapshot: text::BufferSnapshot,
|
||||
new_base_text: Arc<String>,
|
||||
new_base_text: Arc<str>,
|
||||
new_diff_base: Rope,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let (diff, language, language_registry) = this.read_with(cx, |this, cx| {
|
||||
let (diff, language) = this.read_with(cx, |this, cx| {
|
||||
let tracked_buffer = this
|
||||
.tracked_buffers
|
||||
.get(buffer)
|
||||
@@ -386,25 +386,28 @@ impl ActionLog {
|
||||
anyhow::Ok((
|
||||
tracked_buffer.diff.clone(),
|
||||
buffer.read(cx).language().cloned(),
|
||||
buffer.read(cx).language_registry(),
|
||||
))
|
||||
})??;
|
||||
let diff_snapshot = BufferDiff::update_diff(
|
||||
diff.clone(),
|
||||
buffer_snapshot.clone(),
|
||||
Some(new_base_text),
|
||||
true,
|
||||
false,
|
||||
language,
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let update = diff.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
buffer_snapshot.clone(),
|
||||
Some(new_base_text),
|
||||
true,
|
||||
language,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let mut unreviewed_edits = Patch::default();
|
||||
if let Ok(diff_snapshot) = diff_snapshot {
|
||||
if let Ok(update) = update {
|
||||
let update = update.await;
|
||||
|
||||
let diff_snapshot = diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(update.clone(), &buffer_snapshot, cx);
|
||||
diff.snapshot(cx)
|
||||
})?;
|
||||
|
||||
unreviewed_edits = cx
|
||||
.background_spawn({
|
||||
let diff_snapshot = diff_snapshot.clone();
|
||||
let buffer_snapshot = buffer_snapshot.clone();
|
||||
let new_diff_base = new_diff_base.clone();
|
||||
async move {
|
||||
@@ -431,10 +434,6 @@ impl ActionLog {
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx);
|
||||
})?;
|
||||
}
|
||||
this.update(cx, |this, cx| {
|
||||
let tracked_buffer = this
|
||||
@@ -769,7 +768,7 @@ impl ActionLog {
|
||||
tracked.version != buffer.version
|
||||
&& buffer
|
||||
.file()
|
||||
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
|
||||
.is_some_and(|file| !file.disk_state().is_deleted())
|
||||
})
|
||||
.map(|(buffer, _)| buffer)
|
||||
}
|
||||
@@ -975,7 +974,8 @@ impl TrackedBuffer {
|
||||
fn has_edits(&self, cx: &App) -> bool {
|
||||
self.diff
|
||||
.read(cx)
|
||||
.hunks(self.buffer.read(cx), cx)
|
||||
.snapshot(cx)
|
||||
.hunks(self.buffer.read(cx))
|
||||
.next()
|
||||
.is_some()
|
||||
}
|
||||
@@ -2388,13 +2388,14 @@ mod tests {
|
||||
(
|
||||
buffer,
|
||||
diff.read(cx)
|
||||
.hunks(&snapshot, cx)
|
||||
.snapshot(cx)
|
||||
.hunks(&snapshot)
|
||||
.map(|hunk| HunkStatus {
|
||||
diff_status: hunk.status().kind,
|
||||
range: hunk.range,
|
||||
old_text: diff
|
||||
.read(cx)
|
||||
.base_text()
|
||||
.base_text(cx)
|
||||
.text_for_range(hunk.diff_base_byte_range)
|
||||
.collect(),
|
||||
})
|
||||
|
||||
@@ -739,7 +739,7 @@ impl ActivityIndicator {
|
||||
extension_store.outstanding_operations().iter().next()
|
||||
{
|
||||
let (message, icon, rotate) = match operation {
|
||||
ExtensionOperation::Install | ExtensionOperation::AutoInstall => (
|
||||
ExtensionOperation::Install => (
|
||||
format!("Installing {extension_id} extension…"),
|
||||
IconName::LoadCircle,
|
||||
true,
|
||||
|
||||
@@ -30,7 +30,7 @@ use futures::{StreamExt, future};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use language_model::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{
|
||||
ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
|
||||
@@ -153,7 +153,10 @@ impl LanguageModels {
|
||||
id: Self::model_id(model),
|
||||
name: model.name().0,
|
||||
description: None,
|
||||
icon: Some(provider.icon()),
|
||||
icon: Some(match provider.icon() {
|
||||
IconOrSvg::Svg(path) => acp_thread::AgentModelIcon::Path(path),
|
||||
IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1164,10 +1167,6 @@ 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 {
|
||||
@@ -1630,7 +1629,9 @@ mod internal_tests {
|
||||
id: acp::ModelId::new("fake/fake"),
|
||||
name: "Fake".into(),
|
||||
description: None,
|
||||
icon: Some(language_model::IconOrSvg::Icon(ui::IconName::ZedAssistant)),
|
||||
icon: Some(acp_thread::AgentModelIcon::Named(
|
||||
ui::IconName::ZedAssistant
|
||||
)),
|
||||
}]
|
||||
)])
|
||||
);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
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};
|
||||
|
||||
@@ -71,6 +75,38 @@ 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)]
|
||||
|
||||
@@ -5,7 +5,7 @@ use futures::StreamExt;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use language::{OffsetRangeExt, ParseStatus, Point};
|
||||
use project::{
|
||||
Project, WorktreeSettings,
|
||||
Project, SearchResults, WorktreeSettings,
|
||||
search::{SearchQuery, SearchResult},
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
@@ -176,14 +176,17 @@ impl AgentTool for GrepTool {
|
||||
|
||||
let project = self.project.downgrade();
|
||||
cx.spawn(async move |cx| {
|
||||
futures::pin_mut!(results);
|
||||
// Keep the search alive for the duration of result iteration. Dropping this task is the
|
||||
// cancellation mechanism; we intentionally do not detach it.
|
||||
let SearchResults {rx, _task_handle} = results;
|
||||
futures::pin_mut!(rx);
|
||||
|
||||
let mut output = String::new();
|
||||
let mut skips_remaining = input.offset;
|
||||
let mut matches_found = 0;
|
||||
let mut has_more_matches = false;
|
||||
|
||||
'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
|
||||
'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = rx.next().await {
|
||||
if ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ acp_tools.workspace = true
|
||||
acp_thread.workspace = true
|
||||
action_log.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
feature_flags.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
client.workspace = true
|
||||
|
||||
@@ -4,6 +4,7 @@ use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
|
||||
use futures::AsyncBufReadExt as _;
|
||||
use futures::io::BufReader;
|
||||
use project::Project;
|
||||
@@ -12,8 +13,10 @@ use serde::Deserialize;
|
||||
use settings::Settings as _;
|
||||
use task::ShellBuilder;
|
||||
use util::ResultExt as _;
|
||||
use util::process::Child;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::{any::Any, cell::RefCell};
|
||||
use std::{path::Path, rc::Rc};
|
||||
use thiserror::Error;
|
||||
@@ -38,20 +41,37 @@ pub struct AcpConnection {
|
||||
agent_capabilities: acp::AgentCapabilities,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
default_model: Option<acp::ModelId>,
|
||||
default_config_options: HashMap<String, String>,
|
||||
root_dir: PathBuf,
|
||||
// NB: Don't move this into the wait_task, since we need to ensure the process is
|
||||
// killed on drop (setting kill_on_drop on the command seems to not always work).
|
||||
child: smol::process::Child,
|
||||
child: Child,
|
||||
_io_task: Task<Result<(), acp::Error>>,
|
||||
_wait_task: Task<Result<()>>,
|
||||
_stderr_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
struct ConfigOptions {
|
||||
config_options: Rc<RefCell<Vec<acp::SessionConfigOption>>>,
|
||||
tx: Rc<RefCell<watch::Sender<()>>>,
|
||||
rx: watch::Receiver<()>,
|
||||
}
|
||||
|
||||
impl ConfigOptions {
|
||||
fn new(config_options: Rc<RefCell<Vec<acp::SessionConfigOption>>>) -> Self {
|
||||
let (tx, rx) = watch::channel(());
|
||||
Self {
|
||||
config_options,
|
||||
tx: Rc::new(RefCell::new(tx)),
|
||||
rx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
suppress_abort_err: bool,
|
||||
models: Option<Rc<RefCell<acp::SessionModelState>>>,
|
||||
session_modes: Option<Rc<RefCell<acp::SessionModeState>>>,
|
||||
config_options: Option<ConfigOptions>,
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
@@ -60,6 +80,7 @@ pub async fn connect(
|
||||
root_dir: &Path,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
default_model: Option<acp::ModelId>,
|
||||
default_config_options: HashMap<String, String>,
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Rc<dyn AgentConnection>> {
|
||||
@@ -69,6 +90,7 @@ pub async fn connect(
|
||||
root_dir,
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
@@ -85,22 +107,19 @@ impl AcpConnection {
|
||||
root_dir: &Path,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
default_model: Option<acp::ModelId>,
|
||||
default_config_options: HashMap<String, String>,
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?;
|
||||
let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive();
|
||||
let mut child =
|
||||
builder.build_command(Some(command.path.display().to_string()), &command.args);
|
||||
child
|
||||
.envs(command.env.iter().flatten())
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped());
|
||||
builder.build_std_command(Some(command.path.display().to_string()), &command.args);
|
||||
child.envs(command.env.iter().flatten());
|
||||
if !is_remote {
|
||||
child.current_dir(root_dir);
|
||||
}
|
||||
let mut child = child.spawn()?;
|
||||
let mut child = Child::spawn(child, Stdio::piped(), Stdio::piped(), Stdio::piped())?;
|
||||
|
||||
let stdout = child.stdout.take().context("Failed to take stdout")?;
|
||||
let stdin = child.stdin.take().context("Failed to take stdin")?;
|
||||
@@ -217,6 +236,7 @@ impl AcpConnection {
|
||||
agent_capabilities: response.agent_capabilities,
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
_io_task: io_task,
|
||||
_wait_task: wait_task,
|
||||
_stderr_task: stderr_task,
|
||||
@@ -235,7 +255,6 @@ impl AcpConnection {
|
||||
|
||||
impl Drop for AcpConnection {
|
||||
fn drop(&mut self) {
|
||||
// See the comment on the child field.
|
||||
self.child.kill().log_err();
|
||||
}
|
||||
}
|
||||
@@ -256,6 +275,7 @@ impl AgentConnection for AcpConnection {
|
||||
let sessions = self.sessions.clone();
|
||||
let default_mode = self.default_mode.clone();
|
||||
let default_model = self.default_model.clone();
|
||||
let default_config_options = self.default_config_options.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||
let mcp_servers = if project.read(cx).is_local() {
|
||||
@@ -322,8 +342,21 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
})?;
|
||||
|
||||
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
|
||||
let models = response.models.map(|models| Rc::new(RefCell::new(models)));
|
||||
let use_config_options = cx.update(|cx| cx.has_flag::<AcpBetaFeatureFlag>())?;
|
||||
|
||||
// Config options take precedence over legacy modes/models
|
||||
let (modes, models, config_options) = if use_config_options && let Some(opts) = response.config_options {
|
||||
(
|
||||
None,
|
||||
None,
|
||||
Some(Rc::new(RefCell::new(opts))),
|
||||
)
|
||||
} else {
|
||||
// Fall back to legacy modes/models
|
||||
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
|
||||
let models = response.models.map(|models| Rc::new(RefCell::new(models)));
|
||||
(modes, models, None)
|
||||
};
|
||||
|
||||
if let Some(default_mode) = default_mode {
|
||||
if let Some(modes) = modes.as_ref() {
|
||||
@@ -411,6 +444,92 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(config_opts) = config_options.as_ref() {
|
||||
let defaults_to_apply: Vec<_> = {
|
||||
let config_opts_ref = config_opts.borrow();
|
||||
config_opts_ref
|
||||
.iter()
|
||||
.filter_map(|config_option| {
|
||||
let default_value = default_config_options.get(&*config_option.id.0)?;
|
||||
|
||||
let is_valid = match &config_option.kind {
|
||||
acp::SessionConfigKind::Select(select) => match &select.options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(options) => {
|
||||
options.iter().any(|opt| &*opt.value.0 == default_value.as_str())
|
||||
}
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => groups
|
||||
.iter()
|
||||
.any(|g| g.options.iter().any(|opt| &*opt.value.0 == default_value.as_str())),
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_valid {
|
||||
let initial_value = match &config_option.kind {
|
||||
acp::SessionConfigKind::Select(select) => {
|
||||
Some(select.current_value.clone())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Some((config_option.id.clone(), default_value.clone(), initial_value))
|
||||
} else {
|
||||
log::warn!(
|
||||
"`{}` is not a valid value for config option `{}` in {}",
|
||||
default_value,
|
||||
config_option.id.0,
|
||||
name
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
for (config_id, default_value, initial_value) in defaults_to_apply {
|
||||
cx.spawn({
|
||||
let default_value_id = acp::SessionConfigValueId::new(default_value.clone());
|
||||
let session_id = response.session_id.clone();
|
||||
let config_id_clone = config_id.clone();
|
||||
let config_opts = config_opts.clone();
|
||||
let conn = conn.clone();
|
||||
async move |_| {
|
||||
let result = conn
|
||||
.set_session_config_option(
|
||||
acp::SetSessionConfigOptionRequest::new(
|
||||
session_id,
|
||||
config_id_clone.clone(),
|
||||
default_value_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
if result.is_none() {
|
||||
if let Some(initial) = initial_value {
|
||||
let mut opts = config_opts.borrow_mut();
|
||||
if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id_clone) {
|
||||
if let acp::SessionConfigKind::Select(select) =
|
||||
&mut opt.kind
|
||||
{
|
||||
select.current_value = initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut opts = config_opts.borrow_mut();
|
||||
if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id) {
|
||||
if let acp::SessionConfigKind::Select(select) = &mut opt.kind {
|
||||
select.current_value = acp::SessionConfigValueId::new(default_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let session_id = response.session_id;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||
let thread = cx.new(|cx| {
|
||||
@@ -432,6 +551,7 @@ impl AgentConnection for AcpConnection {
|
||||
suppress_abort_err: false,
|
||||
session_modes: modes,
|
||||
models,
|
||||
config_options: config_options.map(|opts| ConfigOptions::new(opts))
|
||||
};
|
||||
sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
@@ -567,6 +687,25 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
fn session_config_options(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionConfigOptions>> {
|
||||
let sessions = self.sessions.borrow();
|
||||
let session = sessions.get(session_id)?;
|
||||
|
||||
let config_opts = session.config_options.as_ref()?;
|
||||
|
||||
Some(Rc::new(AcpSessionConfigOptions {
|
||||
session_id: session_id.clone(),
|
||||
connection: self.connection.clone(),
|
||||
state: config_opts.config_options.clone(),
|
||||
watch_tx: config_opts.tx.clone(),
|
||||
watch_rx: config_opts.rx.clone(),
|
||||
}) as _)
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
@@ -685,6 +824,49 @@ impl acp_thread::AgentModelSelector for AcpModelSelector {
|
||||
}
|
||||
}
|
||||
|
||||
struct AcpSessionConfigOptions {
|
||||
session_id: acp::SessionId,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
state: Rc<RefCell<Vec<acp::SessionConfigOption>>>,
|
||||
watch_tx: Rc<RefCell<watch::Sender<()>>>,
|
||||
watch_rx: watch::Receiver<()>,
|
||||
}
|
||||
|
||||
impl acp_thread::AgentSessionConfigOptions for AcpSessionConfigOptions {
|
||||
fn config_options(&self) -> Vec<acp::SessionConfigOption> {
|
||||
self.state.borrow().clone()
|
||||
}
|
||||
|
||||
fn set_config_option(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value: acp::SessionConfigValueId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<acp::SessionConfigOption>>> {
|
||||
let connection = self.connection.clone();
|
||||
let session_id = self.session_id.clone();
|
||||
let state = self.state.clone();
|
||||
|
||||
let watch_tx = self.watch_tx.clone();
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let response = connection
|
||||
.set_session_config_option(acp::SetSessionConfigOptionRequest::new(
|
||||
session_id, config_id, value,
|
||||
))
|
||||
.await?;
|
||||
|
||||
*state.borrow_mut() = response.config_options.clone();
|
||||
watch_tx.borrow_mut().send(()).ok();
|
||||
Ok(response.config_options)
|
||||
})
|
||||
}
|
||||
|
||||
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||
Some(self.watch_rx.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegate {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
cx: AsyncApp,
|
||||
@@ -778,6 +960,21 @@ impl acp::Client for ClientDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if let acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate {
|
||||
config_options,
|
||||
..
|
||||
}) = ¬ification.update
|
||||
{
|
||||
if let Some(opts) = &session.config_options {
|
||||
*opts.config_options.borrow_mut() = config_options.clone();
|
||||
opts.tx.borrow_mut().send(()).ok();
|
||||
} else {
|
||||
log::error!(
|
||||
"Got a `ConfigOptionUpdate` notification, but the agent didn't specify `config_options` during session setup."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clone so we can inspect meta both before and after handing off to the thread
|
||||
let update_clone = notification.update.clone();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ pub mod e2e_tests;
|
||||
pub use claude::*;
|
||||
use client::ProxySettings;
|
||||
pub use codex::*;
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use custom::*;
|
||||
use fs::Fs;
|
||||
pub use gemini::*;
|
||||
@@ -56,9 +56,19 @@ impl AgentServerDelegate {
|
||||
pub trait AgentServer: Send {
|
||||
fn logo(&self) -> ui::IconName;
|
||||
fn name(&self) -> SharedString;
|
||||
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
|
||||
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: &App) -> Option<agent_client_protocol::SessionModeId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_mode(
|
||||
&self,
|
||||
_mode_id: Option<agent_client_protocol::SessionModeId>,
|
||||
@@ -67,7 +77,7 @@ pub trait AgentServer: Send {
|
||||
) {
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &mut App) -> Option<agent_client_protocol::ModelId> {
|
||||
fn default_model(&self, _cx: &App) -> Option<agent_client_protocol::ModelId> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -79,14 +89,49 @@ pub trait AgentServer: Send {
|
||||
) {
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
|
||||
fn favorite_model_ids(&self, _cx: &mut App) -> HashSet<agent_client_protocol::ModelId> {
|
||||
HashSet::default()
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
fn default_config_option(&self, _config_id: &str, _cx: &App) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
_config_id: &str,
|
||||
_value_id: Option<&str>,
|
||||
_fs: Arc<dyn Fs>,
|
||||
_cx: &mut App,
|
||||
) {
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
_config_id: &agent_client_protocol::SessionConfigId,
|
||||
_cx: &mut App,
|
||||
) -> HashSet<agent_client_protocol::SessionConfigValueId> {
|
||||
HashSet::default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
_config_id: agent_client_protocol::SessionConfigId,
|
||||
_value_id: agent_client_protocol::SessionConfigValueId,
|
||||
_should_be_favorite: bool,
|
||||
_fs: Arc<dyn Fs>,
|
||||
_cx: &App,
|
||||
) {
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
_model_id: agent_client_protocol::ModelId,
|
||||
_should_be_favorite: bool,
|
||||
_fs: Arc<dyn Fs>,
|
||||
_cx: &App,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn AgentServer {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use agent_client_protocol as acp;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use settings::{SettingsStore, update_settings_file};
|
||||
use std::path::Path;
|
||||
@@ -30,7 +31,7 @@ impl AgentServer for ClaudeCode {
|
||||
ui::IconName::AiClaude
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
@@ -51,7 +52,7 @@ impl AgentServer for ClaudeCode {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
||||
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
@@ -72,6 +73,139 @@ 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 default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_config_options.get(config_id).cloned())
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
config_id: &str,
|
||||
value_id: Option<&str>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.map(|s| s.to_string());
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let config_options = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.claude
|
||||
.get_or_insert_default()
|
||||
.default_config_options;
|
||||
|
||||
if let Some(value) = value_id.clone() {
|
||||
config_options.insert(config_id.clone(), value);
|
||||
} else {
|
||||
config_options.remove(&config_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
config_id: &acp::SessionConfigId,
|
||||
cx: &mut App,
|
||||
) -> HashSet<acp::SessionConfigValueId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
|
||||
.map(|values| {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(acp::SessionConfigValueId::new)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value_id: acp::SessionConfigValueId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.to_string();
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let favorites = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.claude
|
||||
.get_or_insert_default()
|
||||
.favorite_config_option_values;
|
||||
|
||||
let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
|
||||
|
||||
if should_be_favorite {
|
||||
if !entry.iter().any(|v| v == &value_id) {
|
||||
entry.push(value_id.clone());
|
||||
}
|
||||
} else {
|
||||
entry.retain(|v| v != &value_id);
|
||||
if entry.is_empty() {
|
||||
favorites.remove(&config_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
@@ -85,6 +219,14 @@ impl AgentServer for ClaudeCode {
|
||||
let extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.claude
|
||||
.as_ref()
|
||||
.map(|s| s.default_config_options.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
@@ -107,6 +249,7 @@ impl AgentServer for ClaudeCode {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ 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};
|
||||
@@ -31,7 +32,7 @@ impl AgentServer for Codex {
|
||||
ui::IconName::AiOpenAi
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
@@ -52,7 +53,7 @@ impl AgentServer for Codex {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
||||
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
@@ -73,6 +74,139 @@ 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 default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_config_options.get(config_id).cloned())
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
config_id: &str,
|
||||
value_id: Option<&str>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.map(|s| s.to_string());
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let config_options = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.codex
|
||||
.get_or_insert_default()
|
||||
.default_config_options;
|
||||
|
||||
if let Some(value) = value_id.clone() {
|
||||
config_options.insert(config_id.clone(), value);
|
||||
} else {
|
||||
config_options.remove(&config_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
config_id: &acp::SessionConfigId,
|
||||
cx: &mut App,
|
||||
) -> HashSet<acp::SessionConfigValueId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
|
||||
.map(|values| {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(acp::SessionConfigValueId::new)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value_id: acp::SessionConfigValueId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.to_string();
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let favorites = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.codex
|
||||
.get_or_insert_default()
|
||||
.favorite_config_option_values;
|
||||
|
||||
let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
|
||||
|
||||
if should_be_favorite {
|
||||
if !entry.iter().any(|v| v == &value_id) {
|
||||
entry.push(value_id.clone());
|
||||
}
|
||||
} else {
|
||||
entry.retain(|v| v != &value_id);
|
||||
if entry.is_empty() {
|
||||
favorites.remove(&config_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
@@ -86,6 +220,14 @@ impl AgentServer for Codex {
|
||||
let extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.codex
|
||||
.as_ref()
|
||||
.map(|s| s.default_config_options.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
@@ -109,6 +251,7 @@ impl AgentServer for Codex {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ 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};
|
||||
@@ -29,7 +30,7 @@ impl AgentServer for CustomAgentServer {
|
||||
IconName::Terminal
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
@@ -43,6 +44,86 @@ impl AgentServer for CustomAgentServer {
|
||||
.and_then(|s| s.default_mode().map(acp::SessionModeId::new))
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
config_id: &acp::SessionConfigId,
|
||||
cx: &mut App,
|
||||
) -> HashSet<acp::SessionConfigValueId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.cloned()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.favorite_config_option_values(config_id.0.as_ref()))
|
||||
.map(|values| {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(acp::SessionConfigValueId::new)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value_id: acp::SessionConfigValueId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let name = self.name();
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.to_string();
|
||||
|
||||
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(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
settings::CustomAgentServerSettings::Custom {
|
||||
favorite_config_option_values,
|
||||
..
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Extension {
|
||||
favorite_config_option_values,
|
||||
..
|
||||
} => {
|
||||
let entry = favorite_config_option_values
|
||||
.entry(config_id.clone())
|
||||
.or_insert_with(Vec::new);
|
||||
|
||||
if should_be_favorite {
|
||||
if !entry.iter().any(|v| v == &value_id) {
|
||||
entry.push(value_id.clone());
|
||||
}
|
||||
} else {
|
||||
entry.retain(|v| v != &value_id);
|
||||
if entry.is_empty() {
|
||||
favorite_config_option_values.remove(&config_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
let name = self.name();
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
@@ -54,6 +135,9 @@ impl AgentServer for CustomAgentServer {
|
||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
@@ -65,7 +149,7 @@ impl AgentServer for CustomAgentServer {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
||||
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
@@ -90,6 +174,9 @@ impl AgentServer for CustomAgentServer {
|
||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
@@ -101,6 +188,125 @@ 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(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
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 default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.cloned()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_config_option(config_id).map(|s| s.to_string()))
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
config_id: &str,
|
||||
value_id: Option<&str>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let name = self.name();
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.map(|s| s.to_string());
|
||||
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(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
settings::CustomAgentServerSettings::Custom {
|
||||
default_config_options,
|
||||
..
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Extension {
|
||||
default_config_options,
|
||||
..
|
||||
} => {
|
||||
if let Some(value) = value_id.clone() {
|
||||
default_config_options.insert(config_id.clone(), value);
|
||||
} else {
|
||||
default_config_options.remove(&config_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
@@ -112,6 +318,23 @@ impl AgentServer for CustomAgentServer {
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.map(|s| match s {
|
||||
project::agent_server_store::CustomAgentServerSettings::Custom {
|
||||
default_config_options,
|
||||
..
|
||||
}
|
||||
| project::agent_server_store::CustomAgentServerSettings::Extension {
|
||||
default_config_options,
|
||||
..
|
||||
} => default_config_options.clone(),
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let store = delegate.store.downgrade();
|
||||
let extra_env = load_proxy_env(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
@@ -137,6 +360,7 @@ impl AgentServer for CustomAgentServer {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -455,20 +455,12 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
project::agent_server_store::AllAgentServersSettings {
|
||||
claude: Some(BuiltinAgentServerSettings {
|
||||
path: Some("claude-code-acp".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
..Default::default()
|
||||
}),
|
||||
gemini: Some(crate::gemini::tests::local_command().into()),
|
||||
codex: Some(BuiltinAgentServerSettings {
|
||||
path: Some("codex-acp".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
..Default::default()
|
||||
}),
|
||||
custom: collections::HashMap::default(),
|
||||
},
|
||||
|
||||
@@ -4,9 +4,10 @@ use std::{any::Any, path::Path};
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, SharedString, Task};
|
||||
use language_models::api_key_for_gemini_cli;
|
||||
use project::agent_server_store::GEMINI_NAME;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use language_models::provider::google::GoogleLanguageModelProvider;
|
||||
use project::agent_server_store::{AllAgentServersSettings, GEMINI_NAME};
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Gemini;
|
||||
@@ -33,11 +34,23 @@ impl AgentServer for Gemini {
|
||||
let mut extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.gemini
|
||||
.as_ref()
|
||||
.map(|s| s.default_config_options.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
|
||||
|
||||
if let Some(api_key) = cx.update(api_key_for_gemini_cli)?.await.ok() {
|
||||
if let Some(api_key) = cx
|
||||
.update(GoogleLanguageModelProvider::api_key_for_gemini_cli)?
|
||||
.await
|
||||
.ok()
|
||||
{
|
||||
extra_env.insert("GEMINI_API_KEY".into(), api_key);
|
||||
}
|
||||
let (command, root_dir, login) = store
|
||||
@@ -61,6 +74,7 @@ impl AgentServer for Gemini {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod config_options;
|
||||
mod entry_view_state;
|
||||
mod message_editor;
|
||||
mod mode_selector;
|
||||
|
||||
775
crates/agent_ui/src/acp/config_options.rs
Normal file
775
crates/agent_ui/src/acp/config_options.rs
Normal file
@@ -0,0 +1,775 @@
|
||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::AgentSessionConfigOptions;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_servers::AgentServer;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
BackgroundExecutor, Context, DismissEvent, Entity, Subscription, Task, Window, prelude::*,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::SettingsStore;
|
||||
use ui::{
|
||||
ElevationIndex, IconButton, ListItem, ListItemSpacing, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::ui::HoldForDefault;
|
||||
|
||||
const PICKER_THRESHOLD: usize = 5;
|
||||
|
||||
pub struct ConfigOptionsView {
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
selectors: Vec<Entity<ConfigOptionSelector>>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
config_option_ids: Vec<acp::SessionConfigId>,
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
|
||||
impl ConfigOptionsView {
|
||||
pub fn new(
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let selectors = Self::build_selectors(&config_options, &agent_server, &fs, window, cx);
|
||||
let config_option_ids = Self::config_option_ids(&config_options);
|
||||
|
||||
let rx = config_options.watch(cx);
|
||||
let refresh_task = cx.spawn_in(window, async move |this, cx| {
|
||||
if let Some(mut rx) = rx {
|
||||
while let Ok(()) = rx.recv().await {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.refresh_selectors_if_needed(window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
config_options,
|
||||
selectors,
|
||||
agent_server,
|
||||
fs,
|
||||
config_option_ids,
|
||||
_refresh_task: refresh_task,
|
||||
}
|
||||
}
|
||||
|
||||
fn config_option_ids(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
) -> Vec<acp::SessionConfigId> {
|
||||
config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.map(|option| option.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn refresh_selectors_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let current_ids = Self::config_option_ids(&self.config_options);
|
||||
if current_ids != self.config_option_ids {
|
||||
self.config_option_ids = current_ids;
|
||||
self.rebuild_selectors(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selectors = Self::build_selectors(
|
||||
&self.config_options,
|
||||
&self.agent_server,
|
||||
&self.fs,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn build_selectors(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
agent_server: &Rc<dyn AgentServer>,
|
||||
fs: &Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<Entity<ConfigOptionSelector>> {
|
||||
config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.map(|option| {
|
||||
let config_options = config_options.clone();
|
||||
let agent_server = agent_server.clone();
|
||||
let fs = fs.clone();
|
||||
cx.new(|cx| {
|
||||
ConfigOptionSelector::new(
|
||||
config_options,
|
||||
option.id.clone(),
|
||||
agent_server,
|
||||
fs,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigOptionsView {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.selectors.is_empty() {
|
||||
return div().into_any_element();
|
||||
}
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(self.selectors.iter().cloned())
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigOptionSelector {
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
picker_handle: PopoverMenuHandle<Picker<ConfigOptionPickerDelegate>>,
|
||||
picker: Entity<Picker<ConfigOptionPickerDelegate>>,
|
||||
setting_value: bool,
|
||||
}
|
||||
|
||||
impl ConfigOptionSelector {
|
||||
pub fn new(
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let option_count = config_options
|
||||
.config_options()
|
||||
.iter()
|
||||
.find(|opt| opt.id == config_id)
|
||||
.map(count_config_options)
|
||||
.unwrap_or(0);
|
||||
|
||||
let is_searchable = option_count >= PICKER_THRESHOLD;
|
||||
|
||||
let picker = {
|
||||
let config_options = config_options.clone();
|
||||
let config_id = config_id.clone();
|
||||
let agent_server = agent_server.clone();
|
||||
let fs = fs.clone();
|
||||
cx.new(move |picker_cx| {
|
||||
let delegate = ConfigOptionPickerDelegate::new(
|
||||
config_options,
|
||||
config_id,
|
||||
agent_server,
|
||||
fs,
|
||||
window,
|
||||
picker_cx,
|
||||
);
|
||||
|
||||
if is_searchable {
|
||||
Picker::list(delegate, window, picker_cx)
|
||||
} else {
|
||||
Picker::nonsearchable_list(delegate, window, picker_cx)
|
||||
}
|
||||
.show_scrollbar(true)
|
||||
.width(rems(20.))
|
||||
.max_height(Some(rems(20.).into()))
|
||||
})
|
||||
};
|
||||
|
||||
Self {
|
||||
config_options,
|
||||
config_id,
|
||||
picker_handle: PopoverMenuHandle::default(),
|
||||
picker,
|
||||
setting_value: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_option(&self) -> Option<acp::SessionConfigOption> {
|
||||
self.config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.find(|opt| opt.id == self.config_id)
|
||||
}
|
||||
|
||||
fn current_value_name(&self) -> String {
|
||||
let Some(option) = self.current_option() else {
|
||||
return "Unknown".to_string();
|
||||
};
|
||||
|
||||
match &option.kind {
|
||||
acp::SessionConfigKind::Select(select) => {
|
||||
find_option_name(&select.options, &select.current_value)
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
}
|
||||
_ => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_trigger_button(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Button {
|
||||
let Some(option) = self.current_option() else {
|
||||
return Button::new("config-option-trigger", "Unknown")
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.disabled(true);
|
||||
};
|
||||
|
||||
let icon = if self.picker_handle.is_deployed() {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
};
|
||||
|
||||
Button::new(
|
||||
ElementId::Name(format!("config-option-{}", option.id.0).into()),
|
||||
self.current_value_name(),
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(self.setting_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigOptionSelector {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(option) = self.current_option() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let trigger_button = self.render_trigger_button(window, cx);
|
||||
|
||||
let option_name = option.name.clone();
|
||||
let option_description: Option<SharedString> = option.description.map(Into::into);
|
||||
|
||||
let tooltip = Tooltip::element(move |_window, _cx| {
|
||||
let mut content = v_flex().gap_1().child(Label::new(option_name.clone()));
|
||||
if let Some(desc) = option_description.as_ref() {
|
||||
content = content.child(
|
||||
Label::new(desc.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
);
|
||||
}
|
||||
content.into_any()
|
||||
});
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.picker.clone(),
|
||||
trigger_button,
|
||||
tooltip,
|
||||
gpui::Corner::BottomRight,
|
||||
cx,
|
||||
)
|
||||
.with_handle(self.picker_handle.clone())
|
||||
.render(window, cx)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ConfigOptionPickerEntry {
|
||||
Separator(SharedString),
|
||||
Option(ConfigOptionValue),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ConfigOptionValue {
|
||||
value: acp::SessionConfigValueId,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
group: Option<String>,
|
||||
}
|
||||
|
||||
struct ConfigOptionPickerDelegate {
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
filtered_entries: Vec<ConfigOptionPickerEntry>,
|
||||
all_options: Vec<ConfigOptionValue>,
|
||||
selected_index: usize,
|
||||
selected_description: Option<(usize, SharedString, bool)>,
|
||||
favorites: HashSet<acp::SessionConfigValueId>,
|
||||
_settings_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ConfigOptionPickerDelegate {
|
||||
fn new(
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Self {
|
||||
let favorites = agent_server.favorite_config_option_value_ids(&config_id, cx);
|
||||
|
||||
let all_options = extract_options(&config_options, &config_id);
|
||||
let filtered_entries = options_to_picker_entries(&all_options, &favorites);
|
||||
|
||||
let current_value = get_current_value(&config_options, &config_id);
|
||||
let selected_index = current_value
|
||||
.and_then(|current| {
|
||||
filtered_entries.iter().position(|entry| {
|
||||
matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
|
||||
})
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let agent_server_for_subscription = agent_server.clone();
|
||||
let config_id_for_subscription = config_id.clone();
|
||||
let settings_subscription =
|
||||
cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
|
||||
let new_favorites = agent_server_for_subscription
|
||||
.favorite_config_option_value_ids(&config_id_for_subscription, cx);
|
||||
if new_favorites != picker.delegate.favorites {
|
||||
picker.delegate.favorites = new_favorites;
|
||||
picker.refresh(window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
|
||||
Self {
|
||||
config_options,
|
||||
config_id,
|
||||
agent_server,
|
||||
fs,
|
||||
filtered_entries,
|
||||
all_options,
|
||||
selected_index,
|
||||
selected_description: None,
|
||||
favorites,
|
||||
_settings_subscription: settings_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_value(&self) -> Option<acp::SessionConfigValueId> {
|
||||
get_current_value(&self.config_options, &self.config_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ConfigOptionPickerDelegate {
|
||||
type ListItem = AnyElement;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.filtered_entries.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn can_select(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> bool {
|
||||
match self.filtered_entries.get(ix) {
|
||||
Some(ConfigOptionPickerEntry::Option(_)) => true,
|
||||
Some(ConfigOptionPickerEntry::Separator(_)) | None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Select an option…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let all_options = self.all_options.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_options = match this
|
||||
.read_with(cx, |_, cx| {
|
||||
if query.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((all_options.clone(), query.clone(), cx.background_executor().clone()))
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
Some((options, q, executor)) => fuzzy_search_options(options, &q, executor).await,
|
||||
None => all_options,
|
||||
};
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries =
|
||||
options_to_picker_entries(&filtered_options, &this.delegate.favorites);
|
||||
|
||||
let current_value = this.delegate.current_value();
|
||||
let new_index = current_value
|
||||
.and_then(|current| {
|
||||
this.delegate.filtered_entries.iter().position(|entry| {
|
||||
matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
|
||||
})
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if let Some(ConfigOptionPickerEntry::Option(option)) =
|
||||
self.filtered_entries.get(self.selected_index)
|
||||
{
|
||||
if window.modifiers().secondary() {
|
||||
let default_value = self
|
||||
.agent_server
|
||||
.default_config_option(self.config_id.0.as_ref(), cx);
|
||||
let is_default = default_value.as_deref() == Some(&*option.value.0);
|
||||
|
||||
self.agent_server.set_default_config_option(
|
||||
self.config_id.0.as_ref(),
|
||||
if is_default {
|
||||
None
|
||||
} else {
|
||||
Some(option.value.0.as_ref())
|
||||
},
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let task = self.config_options.set_config_option(
|
||||
self.config_id.clone(),
|
||||
option.value.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(async move |_, _| {
|
||||
if let Err(err) = task.await {
|
||||
log::error!("Failed to set config option: {:?}", err);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
cx.defer_in(window, |picker, window, cx| {
|
||||
picker.set_query("", window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
match self.filtered_entries.get(ix)? {
|
||||
ConfigOptionPickerEntry::Separator(title) => Some(
|
||||
div()
|
||||
.when(ix > 0, |this| this.mt_1())
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(title.clone()),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
ConfigOptionPickerEntry::Option(option) => {
|
||||
let current_value = self.current_value();
|
||||
let is_selected = current_value.as_ref() == Some(&option.value);
|
||||
|
||||
let default_value = self
|
||||
.agent_server
|
||||
.default_config_option(self.config_id.0.as_ref(), cx);
|
||||
let is_default = default_value.as_deref() == Some(&*option.value.0);
|
||||
|
||||
let is_favorite = self.favorites.contains(&option.value);
|
||||
|
||||
let option_name = option.name.clone();
|
||||
let description = option.description.clone();
|
||||
|
||||
Some(
|
||||
div()
|
||||
.id(("config-option-picker-item", ix))
|
||||
.when_some(description, |this, desc| {
|
||||
let desc: SharedString = desc.into();
|
||||
this.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||
if *hovered {
|
||||
menu.delegate.selected_description =
|
||||
Some((ix, desc.clone(), is_default));
|
||||
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix)
|
||||
{
|
||||
menu.delegate.selected_description = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(h_flex().w_full().child(Label::new(option_name).truncate()))
|
||||
.end_slot(div().pr_2().when(is_selected, |this| {
|
||||
this.child(Icon::new(IconName::Check).color(Color::Accent))
|
||||
}))
|
||||
.end_hover_slot(div().pr_1p5().child({
|
||||
let (icon, color, tooltip) = if is_favorite {
|
||||
(IconName::StarFilled, Color::Accent, "Unfavorite")
|
||||
} else {
|
||||
(IconName::Star, Color::Default, "Favorite")
|
||||
};
|
||||
|
||||
let config_id = self.config_id.clone();
|
||||
let value_id = option.value.clone();
|
||||
let agent_server = self.agent_server.clone();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
IconButton::new(("toggle-favorite-config-option", ix), icon)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.icon_color(color)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.on_click(move |_, _, cx| {
|
||||
agent_server.toggle_favorite_config_option_value(
|
||||
config_id.clone(),
|
||||
value_id.clone(),
|
||||
!is_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn documentation_aside(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<ui::DocumentationAside> {
|
||||
self.selected_description
|
||||
.as_ref()
|
||||
.map(|(_, description, is_default)| {
|
||||
let description = description.clone();
|
||||
let is_default = *is_default;
|
||||
|
||||
ui::DocumentationAside::new(
|
||||
ui::DocumentationSide::Left,
|
||||
Rc::new(move |_| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(description.clone()))
|
||||
.child(HoldForDefault::new(is_default))
|
||||
.into_any_element()
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn documentation_aside_index(&self) -> Option<usize> {
|
||||
self.selected_description.as_ref().map(|(ix, _, _)| *ix)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_options(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: &acp::SessionConfigId,
|
||||
) -> Vec<ConfigOptionValue> {
|
||||
let Some(option) = config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.find(|opt| &opt.id == config_id)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
match &option.kind {
|
||||
acp::SessionConfigKind::Select(select) => match &select.options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(options) => options
|
||||
.iter()
|
||||
.map(|opt| ConfigOptionValue {
|
||||
value: opt.value.clone(),
|
||||
name: opt.name.clone(),
|
||||
description: opt.description.clone(),
|
||||
group: None,
|
||||
})
|
||||
.collect(),
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => groups
|
||||
.iter()
|
||||
.flat_map(|group| {
|
||||
group.options.iter().map(|opt| ConfigOptionValue {
|
||||
value: opt.value.clone(),
|
||||
name: opt.name.clone(),
|
||||
description: opt.description.clone(),
|
||||
group: Some(group.name.clone()),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_value(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: &acp::SessionConfigId,
|
||||
) -> Option<acp::SessionConfigValueId> {
|
||||
config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.find(|opt| &opt.id == config_id)
|
||||
.and_then(|opt| match &opt.kind {
|
||||
acp::SessionConfigKind::Select(select) => Some(select.current_value.clone()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn options_to_picker_entries(
|
||||
options: &[ConfigOptionValue],
|
||||
favorites: &HashSet<acp::SessionConfigValueId>,
|
||||
) -> Vec<ConfigOptionPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let mut favorite_options = Vec::new();
|
||||
|
||||
for option in options {
|
||||
if favorites.contains(&option.value) {
|
||||
favorite_options.push(option.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !favorite_options.is_empty() {
|
||||
entries.push(ConfigOptionPickerEntry::Separator("Favorites".into()));
|
||||
for option in favorite_options {
|
||||
entries.push(ConfigOptionPickerEntry::Option(option));
|
||||
}
|
||||
|
||||
// If the remaining list would start ungrouped (group == None), insert a separator so
|
||||
// Favorites doesn't visually run into the main list.
|
||||
if let Some(option) = options.first()
|
||||
&& option.group.is_none()
|
||||
{
|
||||
entries.push(ConfigOptionPickerEntry::Separator("All Options".into()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut current_group: Option<String> = None;
|
||||
for option in options {
|
||||
if option.group != current_group {
|
||||
if let Some(group_name) = &option.group {
|
||||
entries.push(ConfigOptionPickerEntry::Separator(
|
||||
group_name.clone().into(),
|
||||
));
|
||||
}
|
||||
current_group = option.group.clone();
|
||||
}
|
||||
entries.push(ConfigOptionPickerEntry::Option(option.clone()));
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
async fn fuzzy_search_options(
|
||||
options: Vec<ConfigOptionValue>,
|
||||
query: &str,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Vec<ConfigOptionValue> {
|
||||
let candidates = options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, opt)| StringMatchCandidate::new(ix, &opt.name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches.sort_unstable_by_key(|mat| {
|
||||
let candidate = &candidates[mat.candidate_id];
|
||||
(Reverse(OrderedFloat(mat.score)), candidate.id)
|
||||
});
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| options[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn find_option_name(
|
||||
options: &acp::SessionConfigSelectOptions,
|
||||
value_id: &acp::SessionConfigValueId,
|
||||
) -> Option<String> {
|
||||
match options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(opts) => opts
|
||||
.iter()
|
||||
.find(|o| &o.value == value_id)
|
||||
.map(|o| o.name.clone()),
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => groups.iter().find_map(|group| {
|
||||
group
|
||||
.options
|
||||
.iter()
|
||||
.find(|o| &o.value == value_id)
|
||||
.map(|o| o.name.clone())
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn count_config_options(option: &acp::SessionConfigOption) -> usize {
|
||||
match &option.kind {
|
||||
acp::SessionConfigKind::Select(select) => match &select.options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(options) => options.len(),
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => {
|
||||
groups.iter().map(|g| g.options.len()).sum()
|
||||
}
|
||||
_ => 0,
|
||||
},
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ use rope::Point;
|
||||
use settings::Settings;
|
||||
use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc};
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use ui::{ContextMenu, prelude::*};
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::{Chat, PasteRaw};
|
||||
@@ -132,6 +132,21 @@ 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 =
|
||||
|
||||
@@ -7,8 +7,8 @@ use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
|
||||
use settings::Settings as _;
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use ui::{
|
||||
Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding,
|
||||
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
Button, ContextMenu, ContextMenuEntry, DocumentationSide, KeyBinding, PopoverMenu,
|
||||
PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault};
|
||||
@@ -105,7 +105,7 @@ impl ModeSelector {
|
||||
.toggleable(IconPosition::End, is_selected);
|
||||
|
||||
let entry = if let Some(description) = &mode.description {
|
||||
entry.documentation_aside(side, DocumentationEdge::Bottom, {
|
||||
entry.documentation_aside(side, {
|
||||
let description = description.clone();
|
||||
|
||||
move |_| {
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use acp_thread::{AgentModelIcon, 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, Task, WeakEntity,
|
||||
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language_model::IconOrSvg;
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, prelude::*};
|
||||
use settings::SettingsStore;
|
||||
use ui::{DocumentationAside, DocumentationSide, IntoElement, prelude::*};
|
||||
use util::ResultExt;
|
||||
use zed_actions::agent::OpenSettings;
|
||||
|
||||
@@ -55,7 +54,9 @@ 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,
|
||||
}
|
||||
|
||||
@@ -103,6 +104,19 @@ 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,
|
||||
@@ -112,7 +126,9 @@ impl AcpModelPickerDelegate {
|
||||
selected_model: None,
|
||||
selected_index: 0,
|
||||
selected_description: None,
|
||||
favorites,
|
||||
_refresh_models_task: refresh_models_task,
|
||||
_settings_subscription: settings_subscription,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
@@ -121,40 +137,37 @@ 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.selector.supports_favorites() {
|
||||
if self.favorites.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let favorites = AgentSettings::get_global(cx).favorite_model_ids();
|
||||
|
||||
if favorites.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(models) = self.models.clone() else {
|
||||
let Some(models) = &self.models else {
|
||||
return;
|
||||
};
|
||||
|
||||
let all_models: Vec<AgentModelInfo> = match models {
|
||||
AgentModelList::Flat(list) => list,
|
||||
AgentModelList::Grouped(index_map) => index_map
|
||||
.into_values()
|
||||
.flatten()
|
||||
.collect::<Vec<AgentModelInfo>>(),
|
||||
let all_models: Vec<&AgentModelInfo> = match models {
|
||||
AgentModelList::Flat(list) => list.iter().collect(),
|
||||
AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
|
||||
};
|
||||
|
||||
let favorite_models = all_models
|
||||
.iter()
|
||||
.filter(|model| favorites.contains(&model.id))
|
||||
let favorite_models: Vec<_> = all_models
|
||||
.into_iter()
|
||||
.filter(|model| self.favorites.contains(&model.id))
|
||||
.unique_by(|model| &model.id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
|
||||
let current_id = self.selected_model.as_ref().map(|m| m.id.clone());
|
||||
if favorite_models.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let current_id = self.selected_model.as_ref().map(|m| &m.id);
|
||||
|
||||
let current_index_in_favorites = current_id
|
||||
.as_ref()
|
||||
.and_then(|id| favorite_models.iter().position(|m| &m.id == id))
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
@@ -221,11 +234,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let favorites = if self.selector.supports_favorites() {
|
||||
AgentSettings::get_global(cx).favorite_model_ids()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let favorites = self.favorites.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_models = match this
|
||||
@@ -318,21 +327,20 @@ 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();
|
||||
|
||||
move |cx: &App| {
|
||||
crate::favorite_models::toggle_model_id_in_settings(
|
||||
cx.listener(move |_, _, _, cx| {
|
||||
agent_server.toggle_favorite_model(
|
||||
model_id.clone(),
|
||||
!is_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Some(
|
||||
@@ -352,16 +360,14 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
.child(
|
||||
ModelSelectorListItem::new(ix, model_info.name.clone())
|
||||
.map(|this| match &model_info.icon {
|
||||
Some(IconOrSvg::Svg(path)) => this.icon_path(path.clone()),
|
||||
Some(IconOrSvg::Icon(icon)) => this.icon(*icon),
|
||||
Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),
|
||||
Some(AgentModelIcon::Named(icon)) => this.icon(*icon),
|
||||
None => this,
|
||||
})
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.when(supports_favorites, |this| {
|
||||
this.is_favorite(is_favorite)
|
||||
.on_toggle_favorite(handle_action_click)
|
||||
}),
|
||||
.is_favorite(is_favorite)
|
||||
.on_toggle_favorite(handle_action_click),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
@@ -382,7 +388,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
DocumentationAside::new(
|
||||
DocumentationSide::Left,
|
||||
DocumentationEdge::Top,
|
||||
Rc::new(move |_| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
@@ -394,6 +399,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn documentation_aside_index(&self) -> Option<usize> {
|
||||
self.selected_description.as_ref().map(|(ix, _, _)| *ix)
|
||||
}
|
||||
|
||||
fn render_footer(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
@@ -604,6 +613,46 @@ mod tests {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fuzzy_match(cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
(
|
||||
"zed",
|
||||
vec![
|
||||
"Claude 3.7 Sonnet",
|
||||
"Claude 3.7 Sonnet Thinking",
|
||||
"gpt-4.1",
|
||||
"gpt-4.1-nano",
|
||||
],
|
||||
),
|
||||
("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]),
|
||||
("ollama", vec!["mistral", "deepseek"]),
|
||||
]);
|
||||
|
||||
// Results should preserve models order whenever possible.
|
||||
// In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
|
||||
// similarity scores, but `zed/gpt-4.1` was higher in the models list,
|
||||
// so it should appear first in the results.
|
||||
let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await;
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
("zed", vec!["gpt-4.1", "gpt-4.1-nano"]),
|
||||
("openai", vec!["gpt-4.1", "gpt-4.1-nano"]),
|
||||
],
|
||||
);
|
||||
|
||||
// Fuzzy search
|
||||
let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await;
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
("zed", vec!["gpt-4.1-nano"]),
|
||||
("openai", vec!["gpt-4.1-nano"]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
@@ -740,42 +789,48 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fuzzy_match(cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
(
|
||||
"zed",
|
||||
vec![
|
||||
"Claude 3.7 Sonnet",
|
||||
"Claude 3.7 Sonnet Thinking",
|
||||
"gpt-4.1",
|
||||
"gpt-4.1-nano",
|
||||
],
|
||||
),
|
||||
("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]),
|
||||
("ollama", vec!["mistral", "deepseek"]),
|
||||
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"]);
|
||||
|
||||
// Results should preserve models order whenever possible.
|
||||
// In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
|
||||
// similarity scores, but `zed/gpt-4.1` was higher in the models list,
|
||||
// so it should appear first in the results.
|
||||
let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await;
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
("zed", vec!["gpt-4.1", "gpt-4.1-nano"]),
|
||||
("openai", vec!["gpt-4.1", "gpt-4.1-nano"]),
|
||||
],
|
||||
);
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
// Fuzzy search
|
||||
let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await;
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
("zed", vec!["gpt-4.1-nano"]),
|
||||
("openai", vec!["gpt-4.1-nano"]),
|
||||
],
|
||||
);
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use acp_thread::{AgentModelInfo, AgentModelSelector};
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle};
|
||||
use language_model::IconOrSvg;
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::Settings as _;
|
||||
use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
|
||||
use crate::ui::ModelSelectorTooltip;
|
||||
|
||||
pub struct AcpModelSelectorPopover {
|
||||
selector: Entity<AcpModelSelector>,
|
||||
@@ -24,7 +19,7 @@ pub struct AcpModelSelectorPopover {
|
||||
impl AcpModelSelectorPopover {
|
||||
pub(crate) fn new(
|
||||
selector: Rc<dyn AgentModelSelector>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
agent_server: Rc<dyn agent_servers::AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<AcpModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -65,7 +60,8 @@ impl AcpModelSelectorPopover {
|
||||
|
||||
impl Render for AcpModelSelectorPopover {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let model = self.selector.read(cx).delegate.active_model();
|
||||
let selector = self.selector.read(cx);
|
||||
let model = selector.delegate.active_model();
|
||||
let model_name = model
|
||||
.as_ref()
|
||||
.map(|model| model.name.clone())
|
||||
@@ -81,43 +77,13 @@ impl Render for AcpModelSelectorPopover {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
let should_show_cycle_row = !AgentSettings::get_global(cx)
|
||||
.favorite_model_ids()
|
||||
.is_empty();
|
||||
let show_cycle_row = selector.delegate.favorites_count() > 1;
|
||||
|
||||
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()
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, _cx| {
|
||||
ModelSelectorTooltip::new(focus_handle.clone())
|
||||
.show_cycle_row(show_cycle_row)
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -128,8 +94,8 @@ impl Render for AcpModelSelectorPopover {
|
||||
.when_some(model_icon, |this, icon| {
|
||||
this.child(
|
||||
match icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
|
||||
AgentModelIcon::Path(path) => Icon::from_external_svg(path),
|
||||
AgentModelIcon::Named(icon_name) => Icon::new(icon_name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
|
||||
@@ -24,11 +24,11 @@ use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use futures::FutureExt as _;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
|
||||
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
|
||||
ListOffset, ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task,
|
||||
TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, WindowHandle, div,
|
||||
ease_in_out, linear_color_stop, linear_gradient, list, point, pulsating_between,
|
||||
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, CursorStyle,
|
||||
EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset,
|
||||
ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task, TextStyle,
|
||||
TextStyleRefinement, UnderlineStyle, WeakEntity, Window, WindowHandle, div, ease_in_out,
|
||||
linear_color_stop, linear_gradient, list, point, pulsating_between,
|
||||
};
|
||||
use language::Buffer;
|
||||
|
||||
@@ -47,14 +47,16 @@ use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::Anchor;
|
||||
use theme::{AgentFontSize, ThemeSettings};
|
||||
use ui::{
|
||||
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
|
||||
PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
|
||||
Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, Disclosure, Divider,
|
||||
DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip,
|
||||
WithScrollbar, prelude::*, right_click_menu,
|
||||
};
|
||||
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
||||
use workspace::{CollaboratorId, NewTerminal, Workspace};
|
||||
use zed_actions::agent::{Chat, ToggleModelSelector};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
|
||||
use super::config_options::ConfigOptionsView;
|
||||
use super::entry_view_state::EntryViewState;
|
||||
use crate::acp::AcpModelSelectorPopover;
|
||||
use crate::acp::ModeSelector;
|
||||
@@ -271,12 +273,14 @@ pub struct AcpThreadView {
|
||||
message_editor: Entity<MessageEditor>,
|
||||
focus_handle: FocusHandle,
|
||||
model_selector: Option<Entity<AcpModelSelectorPopover>>,
|
||||
config_options_view: Option<Entity<ConfigOptionsView>>,
|
||||
profile_selector: Option<Entity<ProfileSelector>>,
|
||||
notifications: Vec<WindowHandle<AgentNotification>>,
|
||||
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
|
||||
thread_retry_status: Option<RetryStatus>,
|
||||
thread_error: Option<ThreadError>,
|
||||
thread_error_markdown: Option<Entity<Markdown>>,
|
||||
token_limit_callout_dismissed: bool,
|
||||
thread_feedback: ThreadFeedbackState,
|
||||
list_state: ListState,
|
||||
auth_task: Option<Task<()>>,
|
||||
@@ -428,14 +432,15 @@ impl AcpThreadView {
|
||||
login: None,
|
||||
message_editor,
|
||||
model_selector: None,
|
||||
config_options_view: None,
|
||||
profile_selector: None,
|
||||
|
||||
notifications: Vec::new(),
|
||||
notification_subscriptions: HashMap::default(),
|
||||
list_state: list_state,
|
||||
thread_retry_status: None,
|
||||
thread_error: None,
|
||||
thread_error_markdown: None,
|
||||
token_limit_callout_dismissed: false,
|
||||
thread_feedback: Default::default(),
|
||||
auth_task: None,
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
@@ -612,42 +617,64 @@ impl AcpThreadView {
|
||||
|
||||
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
||||
|
||||
this.model_selector = thread
|
||||
// Check for config options first
|
||||
// Config options take precedence over legacy mode/model selectors
|
||||
// (feature flag gating happens at the data layer)
|
||||
let config_options_provider = thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.model_selector(thread.read(cx).session_id())
|
||||
.map(|selector| {
|
||||
let agent_server = this.agent.clone();
|
||||
let fs = this.project.read(cx).fs().clone();
|
||||
cx.new(|cx| {
|
||||
AcpModelSelectorPopover::new(
|
||||
selector,
|
||||
agent_server,
|
||||
fs,
|
||||
PopoverMenuHandle::default(),
|
||||
this.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
.session_config_options(thread.read(cx).session_id(), cx);
|
||||
|
||||
let mode_selector = thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.session_modes(thread.read(cx).session_id(), cx)
|
||||
.map(|session_modes| {
|
||||
let fs = this.project.read(cx).fs().clone();
|
||||
let focus_handle = this.focus_handle(cx);
|
||||
cx.new(|_cx| {
|
||||
ModeSelector::new(
|
||||
session_modes,
|
||||
this.agent.clone(),
|
||||
fs,
|
||||
focus_handle,
|
||||
)
|
||||
})
|
||||
});
|
||||
let mode_selector;
|
||||
if let Some(config_options) = config_options_provider {
|
||||
// Use config options - don't create mode_selector or model_selector
|
||||
let agent_server = this.agent.clone();
|
||||
let fs = this.project.read(cx).fs().clone();
|
||||
this.config_options_view = Some(cx.new(|cx| {
|
||||
ConfigOptionsView::new(config_options, agent_server, fs, window, cx)
|
||||
}));
|
||||
this.model_selector = None;
|
||||
mode_selector = None;
|
||||
} else {
|
||||
// Fall back to legacy mode/model selectors
|
||||
this.config_options_view = None;
|
||||
this.model_selector = thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.model_selector(thread.read(cx).session_id())
|
||||
.map(|selector| {
|
||||
let agent_server = this.agent.clone();
|
||||
let fs = this.project.read(cx).fs().clone();
|
||||
cx.new(|cx| {
|
||||
AcpModelSelectorPopover::new(
|
||||
selector,
|
||||
agent_server,
|
||||
fs,
|
||||
PopoverMenuHandle::default(),
|
||||
this.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
mode_selector = thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.session_modes(thread.read(cx).session_id(), cx)
|
||||
.map(|session_modes| {
|
||||
let fs = this.project.read(cx).fs().clone();
|
||||
let focus_handle = this.focus_handle(cx);
|
||||
cx.new(|_cx| {
|
||||
ModeSelector::new(
|
||||
session_modes,
|
||||
this.agent.clone(),
|
||||
fs,
|
||||
focus_handle,
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
let mut subscriptions = vec![
|
||||
cx.subscribe_in(&thread, window, Self::handle_thread_event),
|
||||
@@ -1393,6 +1420,7 @@ impl AcpThreadView {
|
||||
fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
|
||||
self.thread_error = None;
|
||||
self.thread_error_markdown = None;
|
||||
self.token_limit_callout_dismissed = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1519,6 +1547,10 @@ impl AcpThreadView {
|
||||
// The connection keeps track of the mode
|
||||
cx.notify();
|
||||
}
|
||||
AcpThreadEvent::ConfigOptionsUpdated(_) => {
|
||||
// The watch task in ConfigOptionsView handles rebuilding selectors
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -2038,7 +2070,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()
|
||||
@@ -2154,7 +2186,6 @@ impl AcpThreadView {
|
||||
if this_is_blank {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_thinking_block(
|
||||
entry_ix,
|
||||
@@ -2180,7 +2211,7 @@ impl AcpThreadView {
|
||||
.when(is_last, |this| this.pb_4())
|
||||
.w_full()
|
||||
.text_ui(cx)
|
||||
.child(message_body)
|
||||
.child(self.render_message_context_menu(entry_ix, message_body, cx))
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
@@ -2287,6 +2318,70 @@ 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()
|
||||
@@ -2489,9 +2584,11 @@ impl AcpThreadView {
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(input_output_header("Raw Input:".into()))
|
||||
.children(tool_call.raw_input_markdown.clone().map(|input| {
|
||||
self.render_markdown(
|
||||
input,
|
||||
default_markdown_style(false, false, window, cx),
|
||||
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())),
|
||||
@@ -2499,15 +2596,17 @@ impl AcpThreadView {
|
||||
})
|
||||
.children(tool_call.content.iter().enumerate().map(
|
||||
|(content_ix, content)| {
|
||||
div().child(self.render_tool_call_content(
|
||||
entry_ix,
|
||||
content,
|
||||
content_ix,
|
||||
tool_call,
|
||||
use_card_layout,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
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,
|
||||
),
|
||||
)
|
||||
},
|
||||
))
|
||||
.into_any(),
|
||||
@@ -4284,37 +4383,6 @@ 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()
|
||||
@@ -4378,8 +4446,12 @@ impl AcpThreadView {
|
||||
.gap_1()
|
||||
.children(self.render_token_usage(cx))
|
||||
.children(self.profile_selector.clone())
|
||||
.children(self.mode_selector().cloned())
|
||||
.children(self.model_selector.clone())
|
||||
// Either config_options_view OR (mode_selector + model_selector)
|
||||
.children(self.config_options_view.clone())
|
||||
.when(self.config_options_view.is_none(), |this| {
|
||||
this.children(self.mode_selector().cloned())
|
||||
.children(self.model_selector.clone())
|
||||
})
|
||||
.child(self.render_send_button(cx)),
|
||||
),
|
||||
)
|
||||
@@ -5354,22 +5426,26 @@ impl AcpThreadView {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_token_limit_callout(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Callout> {
|
||||
fn render_token_limit_callout(&self, cx: &mut Context<Self>) -> Option<Callout> {
|
||||
if self.token_limit_callout_dismissed {
|
||||
return None;
|
||||
}
|
||||
|
||||
let token_usage = self.thread()?.read(cx).token_usage()?;
|
||||
let ratio = token_usage.ratio();
|
||||
|
||||
let (severity, title) = match ratio {
|
||||
let (severity, icon, title) = match ratio {
|
||||
acp_thread::TokenUsageRatio::Normal => return None,
|
||||
acp_thread::TokenUsageRatio::Warning => {
|
||||
(Severity::Warning, "Thread reaching the token limit soon")
|
||||
}
|
||||
acp_thread::TokenUsageRatio::Exceeded => {
|
||||
(Severity::Error, "Thread reached the token limit")
|
||||
}
|
||||
acp_thread::TokenUsageRatio::Warning => (
|
||||
Severity::Warning,
|
||||
IconName::Warning,
|
||||
"Thread reaching the token limit soon",
|
||||
),
|
||||
acp_thread::TokenUsageRatio::Exceeded => (
|
||||
Severity::Error,
|
||||
IconName::XCircle,
|
||||
"Thread reached the token limit",
|
||||
),
|
||||
};
|
||||
|
||||
let burn_mode_available = self.as_native_thread(cx).is_some_and(|thread| {
|
||||
@@ -5389,7 +5465,7 @@ impl AcpThreadView {
|
||||
Some(
|
||||
Callout::new()
|
||||
.severity(severity)
|
||||
.line_height(line_height)
|
||||
.icon(icon)
|
||||
.title(title)
|
||||
.description(description)
|
||||
.actions_slot(
|
||||
@@ -5421,7 +5497,8 @@ impl AcpThreadView {
|
||||
})),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.dismiss_action(self.dismiss_error_button(cx)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5844,18 +5921,13 @@ impl AcpThreadView {
|
||||
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
|
||||
let message = message.into();
|
||||
|
||||
IconButton::new("copy", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Copy Error Message"))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
||||
})
|
||||
CopyButton::new(message).tooltip_label("Copy Error Message")
|
||||
}
|
||||
|
||||
fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Dismiss Error"))
|
||||
.tooltip(Tooltip::text("Dismiss"))
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _, cx| {
|
||||
this.clear_thread_error(cx);
|
||||
@@ -6001,6 +6073,37 @@ 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 {
|
||||
@@ -6084,7 +6187,7 @@ impl Render for AcpThreadView {
|
||||
if let Some(usage_callout) = self.render_usage_callout(line_height, cx) {
|
||||
Some(usage_callout.into_any_element())
|
||||
} else {
|
||||
self.render_token_limit_callout(line_height, cx)
|
||||
self.render_token_limit_callout(cx)
|
||||
.map(|token_limit_callout| token_limit_callout.into_any_element())
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1370,6 +1370,9 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
env: Some(HashMap::default()),
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use gpui::{
|
||||
Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
};
|
||||
|
||||
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
||||
use language::{Buffer, Capability, OffsetRangeExt, Point};
|
||||
use multi_buffer::PathKey;
|
||||
use project::{Project, ProjectItem, ProjectPath};
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -146,13 +146,13 @@ impl AgentDiffPane {
|
||||
paths_to_delete.remove(&path_key);
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let diff = diff_handle.read(cx);
|
||||
|
||||
let diff_hunk_ranges = diff
|
||||
let diff_hunk_ranges = diff_handle
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.hunks_intersecting_range(
|
||||
language::Anchor::min_max_range_for_buffer(snapshot.remote_id()),
|
||||
&snapshot,
|
||||
cx,
|
||||
)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -192,7 +192,7 @@ impl AgentDiffPane {
|
||||
&& buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.is_some_and(|file| file.disk_state() == DiskState::Deleted)
|
||||
.is_some_and(|file| file.disk_state().is_deleted())
|
||||
{
|
||||
editor.fold_buffer(snapshot.text.remote_id(), cx)
|
||||
}
|
||||
@@ -1363,7 +1363,8 @@ impl AgentDiff {
|
||||
| AcpThreadEvent::PromptCapabilitiesUpdated
|
||||
| AcpThreadEvent::AvailableCommandsUpdated(_)
|
||||
| AcpThreadEvent::Retry(_)
|
||||
| AcpThreadEvent::ModeUpdated(_) => {}
|
||||
| AcpThreadEvent::ModeUpdated(_)
|
||||
| AcpThreadEvent::ConfigOptionsUpdated(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
ModelUsageContext,
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
ui::ModelSelectorTooltip,
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
@@ -9,7 +10,6 @@ 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,6 +81,12 @@ 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 {
|
||||
@@ -98,8 +104,18 @@ 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")
|
||||
@@ -125,9 +141,7 @@ impl Render for AgentModelSelector {
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
},
|
||||
tooltip,
|
||||
gpui::Corner::TopRight,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -368,49 +368,26 @@ fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out models from providers that are not authenticated
|
||||
fn is_provider_authenticated(
|
||||
selection: &LanguageModelSelection,
|
||||
registry: &LanguageModelRegistry,
|
||||
cx: &App,
|
||||
) -> bool {
|
||||
let provider_id = LanguageModelProviderId::from(selection.provider.0.clone());
|
||||
registry
|
||||
.provider(&provider_id)
|
||||
.map_or(false, |provider| provider.is_authenticated(cx))
|
||||
}
|
||||
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
let registry_ref = registry.read(cx);
|
||||
|
||||
let default = settings
|
||||
.default_model
|
||||
.as_ref()
|
||||
.filter(|s| is_provider_authenticated(s, registry_ref, cx))
|
||||
.map(to_selected_model);
|
||||
let default = settings.default_model.as_ref().map(to_selected_model);
|
||||
let inline_assistant = settings
|
||||
.inline_assistant_model
|
||||
.as_ref()
|
||||
.filter(|s| is_provider_authenticated(s, registry_ref, cx))
|
||||
.map(to_selected_model);
|
||||
let commit_message = settings
|
||||
.commit_message_model
|
||||
.as_ref()
|
||||
.filter(|s| is_provider_authenticated(s, registry_ref, cx))
|
||||
.map(to_selected_model);
|
||||
let thread_summary = settings
|
||||
.thread_summary_model
|
||||
.as_ref()
|
||||
.filter(|s| is_provider_authenticated(s, registry_ref, cx))
|
||||
.map(to_selected_model);
|
||||
let inline_alternatives = settings
|
||||
.inline_alternatives
|
||||
.iter()
|
||||
.filter(|s| is_provider_authenticated(s, registry_ref, cx))
|
||||
.map(to_selected_model)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.select_default_model(default.as_ref(), cx);
|
||||
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
|
||||
registry.select_commit_message_model(commit_message.as_ref(), cx);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::ModelId;
|
||||
use fs::Fs;
|
||||
use language_model::LanguageModel;
|
||||
use settings::{LanguageModelSelection, update_settings_file};
|
||||
@@ -13,20 +12,11 @@ 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: &App,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let selection = language_model_to_selection(&model);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
@@ -38,20 +28,3 @@ 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ 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::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
||||
use crate::{
|
||||
CycleFavoriteModels, CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext,
|
||||
};
|
||||
|
||||
actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
|
||||
|
||||
@@ -148,7 +150,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.into_any_element();
|
||||
|
||||
v_flex()
|
||||
.key_context("PromptEditor")
|
||||
.key_context("InlineAssistant")
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.block_mouse_except_scroll()
|
||||
.size_full()
|
||||
@@ -162,10 +164,6 @@ 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))
|
||||
@@ -174,6 +172,15 @@ 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()
|
||||
@@ -855,7 +862,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Ignored)
|
||||
.icon_color(Color::Disabled)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Good Result",
|
||||
@@ -865,8 +872,15 @@ impl<T: 'static> PromptEditor<T> {
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Good Result"))
|
||||
this.icon_color(Color::Muted).tooltip(
|
||||
move |_, cx| {
|
||||
Tooltip::for_action(
|
||||
"Good Result",
|
||||
&ThumbsUpResult,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -879,7 +893,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Ignored)
|
||||
.icon_color(Color::Disabled)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Bad Result",
|
||||
@@ -889,8 +903,15 @@ impl<T: 'static> PromptEditor<T> {
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Bad Result"))
|
||||
this.icon_color(Color::Muted).tooltip(
|
||||
move |_, cx| {
|
||||
Tooltip::for_action(
|
||||
"Bad Result",
|
||||
&ThumbsDownResult,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -1088,7 +1109,6 @@ impl<T: 'static> PromptEditor<T> {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
div()
|
||||
.key_context("InlineAssistEditor")
|
||||
.size_full()
|
||||
.p_2()
|
||||
.pl_1()
|
||||
|
||||
@@ -2,9 +2,10 @@ use std::{cmp::Reverse, sync::Arc};
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use collections::{HashMap, HashSet, IndexMap};
|
||||
use futures::{StreamExt, channel::mpsc};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Task};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
@@ -19,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, &App) + 'static>;
|
||||
type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &mut 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, &App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -75,7 +76,7 @@ fn all_models(cx: &App) -> GroupedModels {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let all: Vec<ModelInfo> = providers
|
||||
let all = providers
|
||||
.iter()
|
||||
.flat_map(|provider| {
|
||||
provider
|
||||
@@ -123,7 +124,7 @@ pub struct LanguageModelPickerDelegate {
|
||||
filtered_entries: Vec<LanguageModelPickerEntry>,
|
||||
selected_index: usize,
|
||||
_authenticate_all_providers_task: Task<()>,
|
||||
_refresh_models_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
@@ -132,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, &App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -150,42 +151,24 @@ impl LanguageModelPickerDelegate {
|
||||
get_active_model: Arc::new(get_active_model),
|
||||
on_toggle_favorite: Arc::new(on_toggle_favorite),
|
||||
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
|
||||
_refresh_models_task: {
|
||||
// Create a channel to signal when models need refreshing
|
||||
let (refresh_tx, mut refresh_rx) = mpsc::unbounded::<()>();
|
||||
|
||||
// Subscribe to registry events and send refresh signals through the channel
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
cx.subscribe(®istry, move |_picker, _, event, _cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
refresh_tx.unbounded_send(()).ok();
|
||||
}
|
||||
language_model::Event::DefaultModelChanged
|
||||
| language_model::Event::InlineAssistantModelChanged
|
||||
| language_model::Event::CommitMessageModelChanged
|
||||
| language_model::Event::ThreadSummaryModelChanged => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Spawn a task that listens for refresh signals and updates the picker
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
while let Some(()) = refresh_rx.next().await {
|
||||
if this
|
||||
.update_in(cx, |picker, window, cx| {
|
||||
picker.delegate.all_models = Arc::new(all_models(cx));
|
||||
picker.refresh(window, cx);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
// Picker was dropped, exit the loop
|
||||
break;
|
||||
_subscriptions: vec![cx.subscribe_in(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
window,
|
||||
|picker, _, event, window, cx| {
|
||||
match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
let query = picker.query(cx);
|
||||
picker.delegate.all_models = Arc::new(all_models(cx));
|
||||
// Update matches will automatically drop the previous task
|
||||
// if we get a provider event again
|
||||
picker.update_matches(query, window, cx)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
)],
|
||||
popover_styles,
|
||||
focus_handle,
|
||||
}
|
||||
@@ -267,6 +250,10 @@ 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;
|
||||
@@ -578,7 +565,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
let handle_action_click = {
|
||||
let model = model_info.model.clone();
|
||||
let on_toggle_favorite = self.on_toggle_favorite.clone();
|
||||
move |cx: &App| on_toggle_favorite(model.clone(), !is_favorite, cx)
|
||||
cx.listener(move |picker, _, window, cx| {
|
||||
on_toggle_favorite(model.clone(), !is_favorite, cx);
|
||||
picker.refresh(window, cx);
|
||||
})
|
||||
};
|
||||
|
||||
Some(
|
||||
|
||||
@@ -12,8 +12,8 @@ use editor::{
|
||||
};
|
||||
use futures::{AsyncReadExt as _, FutureExt as _, future::Shared};
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Empty, Entity, EntityId,
|
||||
Image, ImageFormat, Img, SharedString, Task, WeakEntity, pulsating_between,
|
||||
AppContext, ClipboardEntry, Context, Empty, Entity, EntityId, Image, ImageFormat, Img,
|
||||
SharedString, Task, WeakEntity,
|
||||
};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use itertools::Either;
|
||||
@@ -32,13 +32,14 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use text::OffsetRangeExt;
|
||||
use ui::{ButtonLike, Disclosure, TintColor, Toggleable, prelude::*};
|
||||
use ui::{Disclosure, Toggleable, prelude::*};
|
||||
use util::{ResultExt, debug_panic, rel_path::RelPath};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt as _};
|
||||
|
||||
use crate::ui::MentionCrease;
|
||||
|
||||
pub type MentionTask = Shared<Task<Result<Mention, String>>>;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
@@ -754,25 +755,8 @@ fn render_fold_icon_button(
|
||||
.update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
|
||||
.unwrap_or_default();
|
||||
|
||||
ButtonLike::new(fold_id)
|
||||
.style(ButtonStyle::Filled)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.toggle_state(is_in_text_selection)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::from_path(icon_path.clone())
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(label.clone())
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx)
|
||||
.single_line(),
|
||||
),
|
||||
)
|
||||
MentionCrease::new(fold_id, icon_path.clone(), label.clone())
|
||||
.is_toggled(is_in_text_selection)
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
@@ -947,12 +931,14 @@ impl Render for LoadingContext {
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
|
||||
.unwrap_or_default();
|
||||
ButtonLike::new(("loading-context", self.id))
|
||||
.style(ButtonStyle::Filled)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.toggle_state(is_in_text_selection)
|
||||
.when_some(self.image.clone(), |el, image_task| {
|
||||
el.hoverable_tooltip(move |_, cx| {
|
||||
|
||||
let id = ElementId::from(("loading_context", self.id));
|
||||
|
||||
MentionCrease::new(id, self.icon.clone(), self.label.clone())
|
||||
.is_toggled(is_in_text_selection)
|
||||
.is_loading(self.loading.is_some())
|
||||
.when_some(self.image.clone(), |this, image_task| {
|
||||
this.image_preview(move |_, cx| {
|
||||
let image = image_task.peek().cloned().transpose().ok().flatten();
|
||||
let image_task = image_task.clone();
|
||||
cx.new::<ImageHover>(|cx| ImageHover {
|
||||
@@ -971,35 +957,6 @@ impl Render for LoadingContext {
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::from_path(self.icon.clone())
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(self.label.clone())
|
||||
.size(LabelSize::Small)
|
||||
.buffer_font(cx)
|
||||
.single_line(),
|
||||
)
|
||||
.map(|el| {
|
||||
if self.loading.is_some() {
|
||||
el.with_animation(
|
||||
"loading-context-crease",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.opacity(delta),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
el.into_any()
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ use std::{
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::{
|
||||
DocumentationAside, DocumentationEdge, DocumentationSide, HighlightedLabel, KeyBinding,
|
||||
LabelSize, ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
|
||||
DocumentationAside, DocumentationSide, HighlightedLabel, KeyBinding, LabelSize, ListItem,
|
||||
ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
|
||||
};
|
||||
|
||||
/// Trait for types that can provide and manage agent profiles
|
||||
@@ -244,6 +244,7 @@ pub(crate) struct ProfilePickerDelegate {
|
||||
string_candidates: Arc<Vec<StringMatchCandidate>>,
|
||||
filtered_entries: Vec<ProfilePickerEntry>,
|
||||
selected_index: usize,
|
||||
hovered_index: Option<usize>,
|
||||
query: String,
|
||||
cancel: Option<Arc<AtomicBool>>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -270,6 +271,7 @@ impl ProfilePickerDelegate {
|
||||
string_candidates,
|
||||
filtered_entries,
|
||||
selected_index: 0,
|
||||
hovered_index: None,
|
||||
query: String::new(),
|
||||
cancel: None,
|
||||
focus_handle,
|
||||
@@ -578,23 +580,38 @@ impl PickerDelegate for ProfilePickerDelegate {
|
||||
let candidate = self.candidates.get(entry.candidate_index)?;
|
||||
let active_id = self.provider.profile_id(cx);
|
||||
let is_active = active_id == candidate.id;
|
||||
let has_documentation = Self::documentation(candidate).is_some();
|
||||
|
||||
Some(
|
||||
ListItem::new(candidate.id.0.clone())
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
candidate.name.clone(),
|
||||
entry.positions.clone(),
|
||||
))
|
||||
.when(is_active, |this| {
|
||||
this.end_slot(
|
||||
div()
|
||||
.pr_2()
|
||||
.child(Icon::new(IconName::Check).color(Color::Accent)),
|
||||
)
|
||||
div()
|
||||
.id(("profile-picker-item", ix))
|
||||
.when(has_documentation, |this| {
|
||||
this.on_hover(cx.listener(move |picker, hovered, _, cx| {
|
||||
if *hovered {
|
||||
picker.delegate.hovered_index = Some(ix);
|
||||
} else if picker.delegate.hovered_index == Some(ix) {
|
||||
picker.delegate.hovered_index = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
ListItem::new(candidate.id.0.clone())
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
candidate.name.clone(),
|
||||
entry.positions.clone(),
|
||||
))
|
||||
.when(is_active, |this| {
|
||||
this.end_slot(
|
||||
div()
|
||||
.pr_2()
|
||||
.child(Icon::new(IconName::Check).color(Color::Accent)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
@@ -608,7 +625,8 @@ impl PickerDelegate for ProfilePickerDelegate {
|
||||
) -> Option<DocumentationAside> {
|
||||
use std::rc::Rc;
|
||||
|
||||
let entry = match self.filtered_entries.get(self.selected_index)? {
|
||||
let hovered_index = self.hovered_index?;
|
||||
let entry = match self.filtered_entries.get(hovered_index)? {
|
||||
ProfilePickerEntry::Profile(entry) => entry,
|
||||
ProfilePickerEntry::Header(_) => return None,
|
||||
};
|
||||
@@ -626,11 +644,14 @@ impl PickerDelegate for ProfilePickerDelegate {
|
||||
|
||||
Some(DocumentationAside {
|
||||
side,
|
||||
edge: DocumentationEdge::Top,
|
||||
render: Rc::new(move |_| Label::new(docs_aside.clone()).into_any_element()),
|
||||
})
|
||||
}
|
||||
|
||||
fn documentation_aside_index(&self) -> Option<usize> {
|
||||
self.hovered_index
|
||||
}
|
||||
|
||||
fn render_footer(
|
||||
&self,
|
||||
_: &mut Window,
|
||||
@@ -718,6 +739,7 @@ mod tests {
|
||||
string_candidates: Arc::new(Vec::new()),
|
||||
filtered_entries: Vec::new(),
|
||||
selected_index: 0,
|
||||
hovered_index: None,
|
||||
query: String::new(),
|
||||
cancel: None,
|
||||
focus_handle,
|
||||
@@ -752,6 +774,7 @@ mod tests {
|
||||
background: cx.background_executor().clone(),
|
||||
candidates,
|
||||
string_candidates: Arc::new(Vec::new()),
|
||||
hovered_index: None,
|
||||
filtered_entries: vec![
|
||||
ProfilePickerEntry::Profile(ProfileMatchEntry {
|
||||
candidate_index: 0,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
ui::BurnModeTooltip,
|
||||
ui::{BurnModeTooltip, ModelSelectorTooltip},
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use agent_settings::CompletionMode;
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
|
||||
use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
|
||||
@@ -2252,43 +2252,18 @@ impl TextThreadEditor {
|
||||
.color(color)
|
||||
.size(IconSize::XSmall);
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
let should_show_cycle_row = !AgentSettings::get_global(cx)
|
||||
.favorite_model_ids()
|
||||
.is_empty();
|
||||
let show_cycle_row = self
|
||||
.language_model_selector
|
||||
.read(cx)
|
||||
.delegate
|
||||
.favorites_count()
|
||||
> 1;
|
||||
|
||||
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()
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, _cx| {
|
||||
ModelSelectorTooltip::new(focus_handle.clone())
|
||||
.show_cycle_row(show_cycle_row)
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ mod burn_mode_tooltip;
|
||||
mod claude_code_onboarding_modal;
|
||||
mod end_trial_upsell;
|
||||
mod hold_for_default;
|
||||
mod mention_crease;
|
||||
mod model_selector_components;
|
||||
mod onboarding_modal;
|
||||
mod usage_callout;
|
||||
@@ -14,6 +15,7 @@ pub use burn_mode_tooltip::*;
|
||||
pub use claude_code_onboarding_modal::*;
|
||||
pub use end_trial_upsell::*;
|
||||
pub use hold_for_default::*;
|
||||
pub use mention_crease::*;
|
||||
pub use model_selector_components::*;
|
||||
pub use onboarding_modal::*;
|
||||
pub use usage_callout::*;
|
||||
|
||||
@@ -27,7 +27,7 @@ impl RenderOnce for HoldForDefault {
|
||||
PlatformStyle::platform(),
|
||||
None,
|
||||
Some(TextSize::Default.rems(cx).into()),
|
||||
true,
|
||||
false,
|
||||
)))
|
||||
.child(div().map(|this| {
|
||||
if self.is_default {
|
||||
|
||||
100
crates/agent_ui/src/ui/mention_crease.rs
Normal file
100
crates/agent_ui/src/ui/mention_crease.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{Animation, AnimationExt, AnyView, IntoElement, Window, pulsating_between};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{ButtonLike, TintColor, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct MentionCrease {
|
||||
id: ElementId,
|
||||
icon: SharedString,
|
||||
label: SharedString,
|
||||
is_toggled: bool,
|
||||
is_loading: bool,
|
||||
image_preview: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
|
||||
}
|
||||
|
||||
impl MentionCrease {
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
icon: impl Into<SharedString>,
|
||||
label: impl Into<SharedString>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
icon: icon.into(),
|
||||
label: label.into(),
|
||||
is_toggled: false,
|
||||
is_loading: false,
|
||||
image_preview: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_toggled(mut self, is_toggled: bool) -> Self {
|
||||
self.is_toggled = is_toggled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_loading(mut self, is_loading: bool) -> Self {
|
||||
self.is_loading = is_loading;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn image_preview(
|
||||
mut self,
|
||||
builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
|
||||
) -> Self {
|
||||
self.image_preview = Some(Box::new(builder));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for MentionCrease {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = settings.agent_buffer_font_size(cx);
|
||||
let buffer_font = settings.buffer_font.clone();
|
||||
|
||||
let button_height = DefiniteLength::Absolute(AbsoluteLength::Pixels(
|
||||
px(window.line_height().into()) - px(1.),
|
||||
));
|
||||
|
||||
ButtonLike::new(self.id)
|
||||
.style(ButtonStyle::Outlined)
|
||||
.size(ButtonSize::Compact)
|
||||
.height(button_height)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.toggle_state(self.is_toggled)
|
||||
.when_some(self.image_preview, |this, image_preview| {
|
||||
this.hoverable_tooltip(image_preview)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.pb_px()
|
||||
.gap_1()
|
||||
.font(buffer_font)
|
||||
.text_size(font_size)
|
||||
.child(
|
||||
Icon::from_path(self.icon.clone())
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(self.label.clone())
|
||||
.map(|this| {
|
||||
if self.is_loading {
|
||||
this.with_animation(
|
||||
"loading-context-crease",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.opacity(delta),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
this.into_any()
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
use gpui::{Action, FocusHandle, prelude::*};
|
||||
use gpui::{Action, ClickEvent, 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 {
|
||||
@@ -35,11 +43,6 @@ impl RenderOnce for ModelSelectorHeader {
|
||||
}
|
||||
}
|
||||
|
||||
enum ModelIcon {
|
||||
Name(IconName),
|
||||
Path(SharedString),
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelectorListItem {
|
||||
index: usize,
|
||||
@@ -48,7 +51,7 @@ pub struct ModelSelectorListItem {
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
is_favorite: bool,
|
||||
on_toggle_favorite: Option<Box<dyn Fn(&App) + 'static>>,
|
||||
on_toggle_favorite: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl ModelSelectorListItem {
|
||||
@@ -89,7 +92,10 @@ impl ModelSelectorListItem {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle_favorite(mut self, handler: impl Fn(&App) + 'static) -> Self {
|
||||
pub fn on_toggle_favorite(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_toggle_favorite = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
@@ -141,7 +147,7 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
.icon_color(color)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.on_click(move |_, _, cx| (handle_click)(cx)),
|
||||
.on_click(move |event, window, cx| (handle_click)(event, window, cx)),
|
||||
)
|
||||
}
|
||||
}))
|
||||
@@ -187,3 +193,57 @@ 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,
|
||||
)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::B
|
||||
use http_client::http::{self, HeaderMap, HeaderValue};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use settings::ModelMode;
|
||||
pub use settings::{AnthropicAvailableModel as AvailableModel, ModelMode};
|
||||
use strum::{EnumIter, EnumString};
|
||||
use thiserror::Error;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ path = "src/buffer_diff.rs"
|
||||
test-support = ["settings"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clock.workspace = true
|
||||
futures.workspace = true
|
||||
git2.workspace = true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
|
||||
@@ -2647,13 +2647,13 @@ async fn test_git_diff_base_change(
|
||||
local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
|
||||
);
|
||||
});
|
||||
@@ -2677,13 +2677,13 @@ async fn test_git_diff_base_change(
|
||||
remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
|
||||
);
|
||||
});
|
||||
@@ -2699,13 +2699,13 @@ async fn test_git_diff_base_change(
|
||||
remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(committed_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(
|
||||
1..2,
|
||||
"TWO\n",
|
||||
@@ -2731,13 +2731,13 @@ async fn test_git_diff_base_change(
|
||||
local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
|
||||
);
|
||||
});
|
||||
@@ -2746,13 +2746,13 @@ async fn test_git_diff_base_change(
|
||||
remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
|
||||
);
|
||||
});
|
||||
@@ -2760,13 +2760,13 @@ async fn test_git_diff_base_change(
|
||||
remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_a.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(new_committed_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(
|
||||
1..2,
|
||||
"TWO_HUNDRED\n",
|
||||
@@ -2813,13 +2813,13 @@ async fn test_git_diff_base_change(
|
||||
local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_b.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&diff.base_text_string().unwrap(),
|
||||
&diff.base_text_string(cx).unwrap(),
|
||||
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
|
||||
);
|
||||
});
|
||||
@@ -2842,11 +2842,11 @@ async fn test_git_diff_base_change(
|
||||
remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_b.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&staged_text,
|
||||
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
|
||||
@@ -2864,11 +2864,11 @@ async fn test_git_diff_base_change(
|
||||
local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
|
||||
let buffer = buffer_local_b.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&new_staged_text,
|
||||
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
|
||||
@@ -2878,11 +2878,11 @@ async fn test_git_diff_base_change(
|
||||
remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
|
||||
let buffer = remote_buffer_b.read(cx);
|
||||
assert_eq!(
|
||||
diff.base_text_string().as_deref(),
|
||||
diff.base_text_string(cx).as_deref(),
|
||||
Some(new_staged_text.as_str())
|
||||
);
|
||||
assert_hunks(
|
||||
diff.hunks_in_row_range(0..4, buffer, cx),
|
||||
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
|
||||
buffer,
|
||||
&new_staged_text,
|
||||
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
|
||||
@@ -5195,7 +5195,7 @@ async fn test_project_search(
|
||||
cx,
|
||||
)
|
||||
});
|
||||
while let Ok(result) = search_rx.recv().await {
|
||||
while let Ok(result) = search_rx.rx.recv().await {
|
||||
match result {
|
||||
SearchResult::Buffer { buffer, ranges } => {
|
||||
results.entry(buffer).or_insert(ranges);
|
||||
@@ -6745,8 +6745,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Split pane to the right
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.split(workspace::SplitDirection::Right, cx);
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.split(
|
||||
workspace::SplitDirection::Right,
|
||||
workspace::SplitMode::default(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
@@ -905,7 +905,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
drop(project);
|
||||
let search = cx.executor().spawn(async move {
|
||||
let mut results = HashMap::default();
|
||||
while let Ok(result) = search.recv().await {
|
||||
while let Ok(result) = search.rx.recv().await {
|
||||
if let SearchResult::Buffer { buffer, ranges } = result {
|
||||
results.entry(buffer).or_insert(ranges);
|
||||
}
|
||||
@@ -1377,7 +1377,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.get_unstaged_diff(host_buffer.read(cx).remote_id(), cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.base_text_string()
|
||||
.base_text_string(cx)
|
||||
});
|
||||
let guest_diff_base = guest_project.read_with(client_cx, |project, cx| {
|
||||
project
|
||||
@@ -1386,7 +1386,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.get_unstaged_diff(guest_buffer.read(cx).remote_id(), cx)
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.base_text_string()
|
||||
.base_text_string(cx)
|
||||
});
|
||||
assert_eq!(
|
||||
guest_diff_base, host_diff_base,
|
||||
|
||||
@@ -855,8 +855,6 @@ async fn test_slow_adapter_startup_retries(
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
||||
use project::trusted_worktrees::RemoteHostLocation;
|
||||
|
||||
cx_a.update(|cx| {
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
project::trusted_worktrees::init(HashMap::default(), None, None, cx);
|
||||
@@ -991,23 +989,19 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
|
||||
});
|
||||
assert_eq!(worktree_ids.len(), 2);
|
||||
|
||||
let remote_host = project_a.read_with(cx_a, |project, cx| {
|
||||
project
|
||||
.remote_connection_options(cx)
|
||||
.map(RemoteHostLocation::from)
|
||||
});
|
||||
|
||||
let trusted_worktrees =
|
||||
cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
|
||||
let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
||||
});
|
||||
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[1], cx)
|
||||
});
|
||||
assert!(!can_trust_a, "project_a should be restricted initially");
|
||||
assert!(!can_trust_b, "project_b should be restricted initially");
|
||||
|
||||
let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
|
||||
let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
|
||||
store.has_restricted_worktrees(&worktree_store, cx)
|
||||
});
|
||||
@@ -1054,8 +1048,8 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
|
||||
|
||||
trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
|
||||
remote_host.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -1080,25 +1074,29 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
|
||||
"inlay hints should be queried after trust approval"
|
||||
);
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
||||
});
|
||||
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[1], cx)
|
||||
});
|
||||
assert!(can_trust_a, "project_a should be trusted after trust()");
|
||||
assert!(!can_trust_b, "project_b should still be restricted");
|
||||
|
||||
trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
|
||||
remote_host.clone(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_a =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
|
||||
let can_trust_b =
|
||||
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
|
||||
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[0], cx)
|
||||
});
|
||||
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_ids[1], cx)
|
||||
});
|
||||
assert!(can_trust_a, "project_a should remain trusted");
|
||||
assert!(can_trust_b, "project_b should now be trusted");
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ use smallvec::SmallVec;
|
||||
use std::{mem, sync::Arc};
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{
|
||||
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, HighlightedLabel,
|
||||
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tab, Tooltip,
|
||||
prelude::*, tooltip_container,
|
||||
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, CopyButton, Facepile,
|
||||
HighlightedLabel, Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem,
|
||||
Tab, Tooltip, prelude::*, tooltip_container,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use workspace::{
|
||||
@@ -2527,16 +2527,9 @@ impl CollabPanel {
|
||||
|
||||
let button = match section {
|
||||
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||
let channel_link_copy = channel_link;
|
||||
IconButton::new("channel-link", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.size(ButtonSize::None)
|
||||
CopyButton::new(channel_link)
|
||||
.visible_on_hover("section-header")
|
||||
.on_click(move |_, _, cx| {
|
||||
let item = ClipboardItem::new_string(channel_link_copy.clone());
|
||||
cx.write_to_clipboard(item)
|
||||
})
|
||||
.tooltip(Tooltip::text("Copy channel link"))
|
||||
.tooltip_label("Copy Channel Link")
|
||||
.into_any_element()
|
||||
}),
|
||||
Section::Contacts => Some(
|
||||
|
||||
45
crates/component_preview/Cargo.toml
Normal file
45
crates/component_preview/Cargo.toml
Normal file
@@ -0,0 +1,45 @@
|
||||
[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"]
|
||||
1
crates/component_preview/LICENSE-GPL
Symbolic link
1
crates/component_preview/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
LICENSE-GPL
|
||||
18
crates/component_preview/examples/component_preview.rs
Normal file
18
crates/component_preview/examples/component_preview.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
//! 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();
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
//! # Component Preview
|
||||
//!
|
||||
//! A view for exploring Zed components.
|
||||
|
||||
mod component_preview_example;
|
||||
mod persistence;
|
||||
|
||||
use client::UserStore;
|
||||
@@ -11,18 +8,21 @@ use gpui::{
|
||||
App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*,
|
||||
};
|
||||
use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};
|
||||
use languages::LanguageRegistry;
|
||||
use language::LanguageRegistry;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use persistence::COMPONENT_PREVIEW_DB;
|
||||
use project::Project;
|
||||
use std::{iter::Iterator, ops::Range, sync::Arc};
|
||||
use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use workspace::AppState;
|
||||
use workspace::{
|
||||
AppState, Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items,
|
||||
item::ItemEvent,
|
||||
Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items, item::ItemEvent,
|
||||
};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use component_preview_example::*;
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||
workspace::register_serializable_item::<ComponentPreview>(cx);
|
||||
|
||||
145
crates/component_preview/src/component_preview_example.rs
Normal file
145
crates/component_preview/src/component_preview_example.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
/// 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);
|
||||
});
|
||||
}
|
||||
@@ -34,7 +34,7 @@ impl StdioTransport {
|
||||
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?;
|
||||
let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive();
|
||||
let mut command =
|
||||
builder.build_command(Some(binary.executable.display().to_string()), &binary.args);
|
||||
builder.build_smol_command(Some(binary.executable.display().to_string()), &binary.args);
|
||||
|
||||
command
|
||||
.envs(binary.env.unwrap_or_default())
|
||||
|
||||
@@ -24,7 +24,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util::ConnectionResult;
|
||||
use util::{ConnectionResult, ResultExt, process::Child};
|
||||
|
||||
use crate::{
|
||||
adapters::{DebugAdapterBinary, TcpArguments},
|
||||
@@ -528,7 +528,7 @@ impl TcpTransport {
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
|
||||
let mut p = Child::spawn(command, Stdio::null())
|
||||
let mut p = Child::spawn(command, Stdio::null(), Stdio::piped(), Stdio::piped())
|
||||
.with_context(|| "failed to start debug adapter.")?;
|
||||
|
||||
stdout_task = p.stdout.take().map(|stdout| {
|
||||
@@ -582,7 +582,7 @@ impl Transport for TcpTransport {
|
||||
|
||||
fn kill(&mut self) {
|
||||
if let Some(process) = &mut *self.process.lock() {
|
||||
process.kill();
|
||||
process.kill().log_err();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,7 +647,7 @@ impl Transport for TcpTransport {
|
||||
impl Drop for TcpTransport {
|
||||
fn drop(&mut self) {
|
||||
if let Some(mut p) = self.process.lock().take() {
|
||||
p.kill()
|
||||
p.kill().log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,7 +678,7 @@ impl StdioTransport {
|
||||
command.args(&binary.arguments);
|
||||
command.envs(&binary.envs);
|
||||
|
||||
let mut process = Child::spawn(command, Stdio::piped())?;
|
||||
let mut process = Child::spawn(command, Stdio::piped(), Stdio::piped(), Stdio::piped())?;
|
||||
|
||||
let _stderr_task = process.stderr.take().map(|stderr| {
|
||||
cx.background_spawn(TransportDelegate::handle_adapter_log(
|
||||
@@ -703,7 +703,7 @@ impl Transport for StdioTransport {
|
||||
}
|
||||
|
||||
fn kill(&mut self) {
|
||||
self.process.lock().kill();
|
||||
self.process.lock().kill().log_err();
|
||||
}
|
||||
|
||||
fn connect(
|
||||
@@ -731,7 +731,7 @@ impl Transport for StdioTransport {
|
||||
|
||||
impl Drop for StdioTransport {
|
||||
fn drop(&mut self) {
|
||||
self.process.lock().kill();
|
||||
self.process.lock().kill().log_err();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1024,68 +1024,3 @@ impl Transport for FakeTransport {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct Child {
|
||||
process: smol::process::Child,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Child {
|
||||
type Target = smol::process::Child;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.process
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for Child {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.process
|
||||
}
|
||||
}
|
||||
|
||||
impl Child {
|
||||
fn into_inner(self) -> smol::process::Child {
|
||||
self.process
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn spawn(mut command: std::process::Command, stdin: Stdio) -> Result<Self> {
|
||||
util::set_pre_exec_to_start_new_session(&mut command);
|
||||
let mut command = smol::process::Command::from(command);
|
||||
let process = command
|
||||
.stdin(stdin)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to spawn command `{command:?}`",))?;
|
||||
Ok(Self { process })
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn spawn(command: std::process::Command, stdin: Stdio) -> Result<Self> {
|
||||
// TODO(windows): create a job object and add the child process handle to it,
|
||||
// see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
|
||||
let mut command = smol::process::Command::from(command);
|
||||
let process = command
|
||||
.stdin(stdin)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to spawn command `{command:?}`",))?;
|
||||
Ok(Self { process })
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn kill(&mut self) {
|
||||
let pid = self.process.id();
|
||||
unsafe {
|
||||
libc::killpg(pid as i32, libc::SIGKILL);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn kill(&mut self) {
|
||||
// TODO(windows): terminate the job object in kill
|
||||
let _ = self.process.kill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1579,8 +1579,10 @@ impl Panel for DebugPanel {
|
||||
Some(proto::PanelId::DebugPanel)
|
||||
}
|
||||
|
||||
fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
|
||||
Some(IconName::Debug)
|
||||
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
|
||||
DebuggerSettings::get_global(cx)
|
||||
.button
|
||||
.then_some(IconName::Debug)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
|
||||
|
||||
@@ -19,6 +19,7 @@ ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
arrayvec.workspace = true
|
||||
brotli.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
@@ -52,7 +53,9 @@ settings.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
375
crates/edit_prediction/src/capture_example.rs
Normal file
375
crates/edit_prediction/src/capture_example.rs
Normal file
@@ -0,0 +1,375 @@
|
||||
use crate::{
|
||||
EditPredictionStore, StoredEvent,
|
||||
cursor_excerpt::editable_and_context_ranges_for_cursor_position, example_spec::ExampleSpec,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use buffer_diff::BufferDiffSnapshot;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{Buffer, ToPoint as _};
|
||||
use project::Project;
|
||||
use std::{collections::hash_map, fmt::Write as _, path::Path, sync::Arc};
|
||||
use text::{BufferSnapshot as TextBufferSnapshot, ToOffset as _};
|
||||
|
||||
pub fn capture_example(
|
||||
project: Entity<Project>,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_anchor: language::Anchor,
|
||||
last_event_is_expected_patch: bool,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<ExampleSpec>>> {
|
||||
let ep_store = EditPredictionStore::try_global(cx)?;
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let file = snapshot.file()?;
|
||||
let worktree_id = file.worktree_id(cx);
|
||||
let repository = project.read(cx).active_repository(cx)?;
|
||||
let repository_snapshot = repository.read(cx).snapshot();
|
||||
let worktree = project.read(cx).worktree_for_id(worktree_id, cx)?;
|
||||
let cursor_path = worktree.read(cx).root_name().join(file.path());
|
||||
if worktree.read(cx).abs_path() != repository_snapshot.work_directory_abs_path {
|
||||
return None;
|
||||
}
|
||||
|
||||
let repository_url = repository_snapshot
|
||||
.remote_origin_url
|
||||
.clone()
|
||||
.or_else(|| repository_snapshot.remote_upstream_url.clone())?;
|
||||
let revision = repository_snapshot.head_commit.as_ref()?.sha.to_string();
|
||||
|
||||
let mut events = ep_store.update(cx, |store, cx| {
|
||||
store.edit_history_for_project_with_pause_split_last_event(&project, cx)
|
||||
});
|
||||
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
|
||||
Some(cx.spawn(async move |mut cx| {
|
||||
let snapshots_by_path = collect_snapshots(&project, &git_store, &events, &mut cx).await?;
|
||||
let cursor_excerpt = cx
|
||||
.background_executor()
|
||||
.spawn(async move { compute_cursor_excerpt(&snapshot, cursor_anchor) })
|
||||
.await;
|
||||
let uncommitted_diff = cx
|
||||
.background_executor()
|
||||
.spawn(async move { compute_uncommitted_diff(snapshots_by_path) })
|
||||
.await;
|
||||
|
||||
let mut edit_history = String::new();
|
||||
let mut expected_patch = String::new();
|
||||
if last_event_is_expected_patch {
|
||||
if let Some(stored_event) = events.pop() {
|
||||
zeta_prompt::write_event(&mut expected_patch, &stored_event.event);
|
||||
}
|
||||
}
|
||||
|
||||
for stored_event in &events {
|
||||
zeta_prompt::write_event(&mut edit_history, &stored_event.event);
|
||||
if !edit_history.ends_with('\n') {
|
||||
edit_history.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
let name = generate_timestamp_name();
|
||||
|
||||
Ok(ExampleSpec {
|
||||
name,
|
||||
repository_url,
|
||||
revision,
|
||||
uncommitted_diff,
|
||||
cursor_path: cursor_path.as_std_path().into(),
|
||||
cursor_position: cursor_excerpt,
|
||||
edit_history,
|
||||
expected_patch,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn compute_cursor_excerpt(
|
||||
snapshot: &language::BufferSnapshot,
|
||||
cursor_anchor: language::Anchor,
|
||||
) -> String {
|
||||
let cursor_point = cursor_anchor.to_point(snapshot);
|
||||
let (_editable_range, context_range) =
|
||||
editable_and_context_ranges_for_cursor_position(cursor_point, snapshot, 100, 50);
|
||||
|
||||
let context_start_offset = context_range.start.to_offset(snapshot);
|
||||
let cursor_offset = cursor_anchor.to_offset(snapshot);
|
||||
let cursor_offset_in_excerpt = cursor_offset.saturating_sub(context_start_offset);
|
||||
let mut excerpt = snapshot.text_for_range(context_range).collect::<String>();
|
||||
if cursor_offset_in_excerpt <= excerpt.len() {
|
||||
excerpt.insert_str(cursor_offset_in_excerpt, zeta_prompt::CURSOR_MARKER);
|
||||
}
|
||||
excerpt
|
||||
}
|
||||
|
||||
async fn collect_snapshots(
|
||||
project: &Entity<Project>,
|
||||
git_store: &Entity<project::git_store::GitStore>,
|
||||
events: &[StoredEvent],
|
||||
cx: &mut gpui::AsyncApp,
|
||||
) -> Result<HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>> {
|
||||
let mut snapshots_by_path = HashMap::default();
|
||||
for stored_event in events {
|
||||
let zeta_prompt::Event::BufferChange { path, .. } = stored_event.event.as_ref();
|
||||
if let Some((project_path, full_path)) = project.read_with(cx, |project, cx| {
|
||||
let project_path = project.find_project_path(path, cx)?;
|
||||
let full_path = project
|
||||
.worktree_for_id(project_path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.root_name()
|
||||
.join(&project_path.path)
|
||||
.as_std_path()
|
||||
.into();
|
||||
Some((project_path, full_path))
|
||||
})? {
|
||||
if let hash_map::Entry::Vacant(entry) = snapshots_by_path.entry(full_path) {
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
let diff = git_store
|
||||
.update(cx, |git_store, cx| {
|
||||
git_store.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx))?;
|
||||
entry.insert((stored_event.old_snapshot.clone(), diff_snapshot));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(snapshots_by_path)
|
||||
}
|
||||
|
||||
fn compute_uncommitted_diff(
|
||||
snapshots_by_path: HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>,
|
||||
) -> String {
|
||||
let mut uncommitted_diff = String::new();
|
||||
for (full_path, (before_text, diff_snapshot)) in snapshots_by_path {
|
||||
if let Some(head_text) = &diff_snapshot.base_text_string() {
|
||||
let file_diff = language::unified_diff(head_text, &before_text.text());
|
||||
if !file_diff.is_empty() {
|
||||
let path_str = full_path.to_string_lossy();
|
||||
writeln!(uncommitted_diff, "--- a/{path_str}").ok();
|
||||
writeln!(uncommitted_diff, "+++ b/{path_str}").ok();
|
||||
uncommitted_diff.push_str(&file_diff);
|
||||
if !uncommitted_diff.ends_with('\n') {
|
||||
uncommitted_diff.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
uncommitted_diff
|
||||
}
|
||||
|
||||
fn generate_timestamp_name() -> String {
|
||||
let format = time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]");
|
||||
match format {
|
||||
Ok(format) => {
|
||||
let now = time::OffsetDateTime::now_local()
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
|
||||
now.format(&format)
|
||||
.unwrap_or_else(|_| "unknown-time".to_string())
|
||||
}
|
||||
Err(_) => "unknown-time".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{AppContext as _, TestAppContext, http_client::FakeHttpClient};
|
||||
use indoc::indoc;
|
||||
use language::{Anchor, Point};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::path::Path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_capture_example(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
let committed_contents = indoc! {"
|
||||
fn main() {
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
four();
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
nine();
|
||||
}
|
||||
"};
|
||||
|
||||
let disk_contents = indoc! {"
|
||||
fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
four();
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"};
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
".git": {},
|
||||
"src": {
|
||||
"main.rs": disk_contents,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_head_for_repo(
|
||||
Path::new("/project/.git"),
|
||||
&[("src/main.rs", committed_contents.to_string())],
|
||||
"abc123def456",
|
||||
);
|
||||
fs.set_remote_for_repo(
|
||||
Path::new("/project/.git"),
|
||||
"origin",
|
||||
"https://github.com/test/repo.git",
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), ["/project".as_ref()], cx).await;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/project/src/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let ep_store = cx.read(|cx| EditPredictionStore::try_global(cx).unwrap());
|
||||
ep_store.update(cx, |ep_store, cx| {
|
||||
ep_store.register_buffer(&buffer, &project, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let point = Point::new(6, 0);
|
||||
buffer.edit([(point..point, " // comment 3\n")], None, cx);
|
||||
let point = Point::new(4, 0);
|
||||
buffer.edit([(point..point, " // comment 4\n")], None, cx);
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
buffer.text(),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
// comment 4
|
||||
three();
|
||||
four();
|
||||
// comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let mut example = cx
|
||||
.update(|cx| {
|
||||
capture_example(project.clone(), buffer.clone(), Anchor::MIN, false, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
example.name = "test".to_string();
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
example,
|
||||
ExampleSpec {
|
||||
name: "test".to_string(),
|
||||
repository_url: "https://github.com/test/repo.git".to_string(),
|
||||
revision: "abc123def456".to_string(),
|
||||
uncommitted_diff: indoc! {"
|
||||
--- a/project/src/main.rs
|
||||
+++ b/project/src/main.rs
|
||||
@@ -1,4 +1,5 @@
|
||||
fn main() {
|
||||
+ // comment 1
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
@@ -7,5 +8,6 @@
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
+ // comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
.to_string(),
|
||||
cursor_path: Path::new("project/src/main.rs").into(),
|
||||
cursor_position: indoc! {"
|
||||
<|user_cursor|>fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
// comment 4
|
||||
three();
|
||||
four();
|
||||
// comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
.to_string(),
|
||||
edit_history: indoc! {"
|
||||
--- a/project/src/main.rs
|
||||
+++ b/project/src/main.rs
|
||||
@@ -2,8 +2,10 @@
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
+ // comment 4
|
||||
three();
|
||||
four();
|
||||
+ // comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
"}
|
||||
.to_string(),
|
||||
expected_patch: "".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
zlog::init_test();
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(Arc::new(FakeSystemClock::new()), http_client, cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
EditPredictionStore::global(&client, &user_store, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ use semver::Version;
|
||||
use serde::de::DeserializeOwned;
|
||||
use settings::{EditPredictionProvider, SettingsStore, update_settings_file};
|
||||
use std::collections::{VecDeque, hash_map};
|
||||
use text::Edit;
|
||||
use workspace::Workspace;
|
||||
|
||||
use std::ops::Range;
|
||||
@@ -57,9 +58,9 @@ pub mod open_ai_response;
|
||||
mod prediction;
|
||||
pub mod sweep_ai;
|
||||
|
||||
#[cfg(any(test, feature = "test-support", feature = "cli-support"))]
|
||||
pub mod udiff;
|
||||
|
||||
mod capture_example;
|
||||
mod zed_edit_prediction_delegate;
|
||||
pub mod zeta1;
|
||||
pub mod zeta2;
|
||||
@@ -74,6 +75,7 @@ pub use crate::prediction::EditPrediction;
|
||||
pub use crate::prediction::EditPredictionId;
|
||||
use crate::prediction::EditPredictionResult;
|
||||
pub use crate::sweep_ai::SweepAi;
|
||||
pub use capture_example::capture_example;
|
||||
pub use language_model::ApiKeyState;
|
||||
pub use telemetry_events::EditPredictionRating;
|
||||
pub use zed_edit_prediction_delegate::ZedEditPredictionDelegate;
|
||||
@@ -231,8 +233,15 @@ pub struct EditPredictionFinishedDebugEvent {
|
||||
|
||||
pub type RequestDebugInfo = predict_edits_v3::DebugInfo;
|
||||
|
||||
/// An event with associated metadata for reconstructing buffer state.
|
||||
#[derive(Clone)]
|
||||
pub struct StoredEvent {
|
||||
pub event: Arc<zeta_prompt::Event>,
|
||||
pub old_snapshot: TextBufferSnapshot,
|
||||
}
|
||||
|
||||
struct ProjectState {
|
||||
events: VecDeque<Arc<zeta_prompt::Event>>,
|
||||
events: VecDeque<StoredEvent>,
|
||||
last_event: Option<LastEvent>,
|
||||
recent_paths: VecDeque<ProjectPath>,
|
||||
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
||||
@@ -248,7 +257,7 @@ struct ProjectState {
|
||||
}
|
||||
|
||||
impl ProjectState {
|
||||
pub fn events(&self, cx: &App) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
pub fn events(&self, cx: &App) -> Vec<StoredEvent> {
|
||||
self.events
|
||||
.iter()
|
||||
.cloned()
|
||||
@@ -260,7 +269,7 @@ impl ProjectState {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn events_split_by_pause(&self, cx: &App) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
pub fn events_split_by_pause(&self, cx: &App) -> Vec<StoredEvent> {
|
||||
self.events
|
||||
.iter()
|
||||
.cloned()
|
||||
@@ -415,7 +424,7 @@ impl LastEvent {
|
||||
&self,
|
||||
license_detection_watchers: &HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
|
||||
cx: &App,
|
||||
) -> Option<Arc<zeta_prompt::Event>> {
|
||||
) -> Option<StoredEvent> {
|
||||
let path = buffer_path_with_id_fallback(self.new_file.as_ref(), &self.new_snapshot, cx);
|
||||
let old_path = buffer_path_with_id_fallback(self.old_file.as_ref(), &self.old_snapshot, cx);
|
||||
|
||||
@@ -430,19 +439,22 @@ impl LastEvent {
|
||||
})
|
||||
});
|
||||
|
||||
let diff = language::unified_diff(&self.old_snapshot.text(), &self.new_snapshot.text());
|
||||
let diff = compute_diff_between_snapshots(&self.old_snapshot, &self.new_snapshot)?;
|
||||
|
||||
if path == old_path && diff.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Arc::new(zeta_prompt::Event::BufferChange {
|
||||
old_path,
|
||||
path,
|
||||
diff,
|
||||
in_open_source_repo,
|
||||
// TODO: Actually detect if this edit was predicted or not
|
||||
predicted: false,
|
||||
}))
|
||||
Some(StoredEvent {
|
||||
event: Arc::new(zeta_prompt::Event::BufferChange {
|
||||
old_path,
|
||||
path,
|
||||
diff,
|
||||
in_open_source_repo,
|
||||
// TODO: Actually detect if this edit was predicted or not
|
||||
predicted: false,
|
||||
}),
|
||||
old_snapshot: self.old_snapshot.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,6 +487,52 @@ impl LastEvent {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn compute_diff_between_snapshots(
|
||||
old_snapshot: &TextBufferSnapshot,
|
||||
new_snapshot: &TextBufferSnapshot,
|
||||
) -> Option<String> {
|
||||
let edits: Vec<Edit<usize>> = new_snapshot
|
||||
.edits_since::<usize>(&old_snapshot.version)
|
||||
.collect();
|
||||
|
||||
let (first_edit, last_edit) = edits.first().zip(edits.last())?;
|
||||
|
||||
let old_start_point = old_snapshot.offset_to_point(first_edit.old.start);
|
||||
let old_end_point = old_snapshot.offset_to_point(last_edit.old.end);
|
||||
let new_start_point = new_snapshot.offset_to_point(first_edit.new.start);
|
||||
let new_end_point = new_snapshot.offset_to_point(last_edit.new.end);
|
||||
|
||||
const CONTEXT_LINES: u32 = 3;
|
||||
|
||||
let old_context_start_row = old_start_point.row.saturating_sub(CONTEXT_LINES);
|
||||
let new_context_start_row = new_start_point.row.saturating_sub(CONTEXT_LINES);
|
||||
let old_context_end_row =
|
||||
(old_end_point.row + 1 + CONTEXT_LINES).min(old_snapshot.max_point().row);
|
||||
let new_context_end_row =
|
||||
(new_end_point.row + 1 + CONTEXT_LINES).min(new_snapshot.max_point().row);
|
||||
|
||||
let old_start_line_offset = old_snapshot.point_to_offset(Point::new(old_context_start_row, 0));
|
||||
let new_start_line_offset = new_snapshot.point_to_offset(Point::new(new_context_start_row, 0));
|
||||
let old_end_line_offset = old_snapshot
|
||||
.point_to_offset(Point::new(old_context_end_row + 1, 0).min(old_snapshot.max_point()));
|
||||
let new_end_line_offset = new_snapshot
|
||||
.point_to_offset(Point::new(new_context_end_row + 1, 0).min(new_snapshot.max_point()));
|
||||
let old_edit_range = old_start_line_offset..old_end_line_offset;
|
||||
let new_edit_range = new_start_line_offset..new_end_line_offset;
|
||||
|
||||
let old_region_text: String = old_snapshot.text_for_range(old_edit_range).collect();
|
||||
let new_region_text: String = new_snapshot.text_for_range(new_edit_range).collect();
|
||||
|
||||
let diff = language::unified_diff_with_offsets(
|
||||
&old_region_text,
|
||||
&new_region_text,
|
||||
old_context_start_row,
|
||||
new_context_start_row,
|
||||
);
|
||||
|
||||
Some(diff)
|
||||
}
|
||||
|
||||
fn buffer_path_with_id_fallback(
|
||||
file: Option<&Arc<dyn File>>,
|
||||
snapshot: &TextBufferSnapshot,
|
||||
@@ -643,7 +701,7 @@ impl EditPredictionStore {
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &App,
|
||||
) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
) -> Vec<StoredEvent> {
|
||||
self.projects
|
||||
.get(&project.entity_id())
|
||||
.map(|project_state| project_state.events(cx))
|
||||
@@ -654,7 +712,7 @@ impl EditPredictionStore {
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &App,
|
||||
) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
) -> Vec<StoredEvent> {
|
||||
self.projects
|
||||
.get(&project.entity_id())
|
||||
.map(|project_state| project_state.events_split_by_pause(cx))
|
||||
@@ -1536,8 +1594,10 @@ impl EditPredictionStore {
|
||||
|
||||
self.get_or_init_project(&project, cx);
|
||||
let project_state = self.projects.get(&project.entity_id()).unwrap();
|
||||
let events = project_state.events(cx);
|
||||
let has_events = !events.is_empty();
|
||||
let stored_events = project_state.events(cx);
|
||||
let has_events = !stored_events.is_empty();
|
||||
let events: Vec<Arc<zeta_prompt::Event>> =
|
||||
stored_events.into_iter().map(|e| e.event).collect();
|
||||
let debug_tx = project_state.debug_tx.clone();
|
||||
|
||||
let snapshot = active_buffer.read(cx).snapshot();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::{udiff::apply_diff_to_string, zeta1::MAX_EVENT_TOKENS};
|
||||
use crate::{compute_diff_between_snapshots, udiff::apply_diff_to_string, zeta1::MAX_EVENT_TOKENS};
|
||||
use client::{UserStore, test::FakeServer};
|
||||
use clock::{FakeSystemClock, ReplicaId};
|
||||
use cloud_api_types::{CreateLlmTokenResponse, LlmToken};
|
||||
@@ -360,7 +360,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
|
||||
ep_store.edit_history_for_project(&project, cx)
|
||||
});
|
||||
assert_eq!(events.len(), 1);
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].as_ref();
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].event.as_ref();
|
||||
assert_eq!(
|
||||
diff.as_str(),
|
||||
indoc! {"
|
||||
@@ -377,7 +377,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
|
||||
ep_store.edit_history_for_project_with_pause_split_last_event(&project, cx)
|
||||
});
|
||||
assert_eq!(events.len(), 2);
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].as_ref();
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].event.as_ref();
|
||||
assert_eq!(
|
||||
diff.as_str(),
|
||||
indoc! {"
|
||||
@@ -389,7 +389,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
|
||||
"}
|
||||
);
|
||||
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[1].as_ref();
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[1].event.as_ref();
|
||||
assert_eq!(
|
||||
diff.as_str(),
|
||||
indoc! {"
|
||||
@@ -2082,6 +2082,74 @@ async fn test_unauthenticated_with_custom_url_allows_prediction_impl(cx: &mut Te
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_compute_diff_between_snapshots(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| {
|
||||
Buffer::local(
|
||||
indoc! {"
|
||||
zero
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
eleven
|
||||
twelve
|
||||
thirteen
|
||||
fourteen
|
||||
fifteen
|
||||
sixteen
|
||||
seventeen
|
||||
eighteen
|
||||
nineteen
|
||||
twenty
|
||||
twenty-one
|
||||
twenty-two
|
||||
twenty-three
|
||||
twenty-four
|
||||
"},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let old_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let point = Point::new(12, 0);
|
||||
buffer.edit([(point..point, "SECOND INSERTION\n")], None, cx);
|
||||
let point = Point::new(8, 0);
|
||||
buffer.edit([(point..point, "FIRST INSERTION\n")], None, cx);
|
||||
});
|
||||
|
||||
let new_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||||
|
||||
let diff = compute_diff_between_snapshots(&old_snapshot, &new_snapshot).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
diff,
|
||||
indoc! {"
|
||||
@@ -6,10 +6,12 @@
|
||||
five
|
||||
six
|
||||
seven
|
||||
+FIRST INSERTION
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
eleven
|
||||
+SECOND INSERTION
|
||||
twelve
|
||||
thirteen
|
||||
fourteen
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
zlog::init_test();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write as _, mem, path::Path, sync::Arc};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ExampleSpec {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
|
||||
@@ -45,6 +45,11 @@ pub async fn run_format_prompt(
|
||||
let snapshot = state.buffer.read_with(&cx, |buffer, _| buffer.snapshot())?;
|
||||
let project = state.project.clone();
|
||||
let (_, input) = ep_store.update(&mut cx, |ep_store, cx| {
|
||||
let events = ep_store
|
||||
.edit_history_for_project(&project, cx)
|
||||
.into_iter()
|
||||
.map(|e| e.event)
|
||||
.collect();
|
||||
anyhow::Ok(zeta2_prompt_input(
|
||||
&snapshot,
|
||||
example
|
||||
@@ -53,7 +58,7 @@ pub async fn run_format_prompt(
|
||||
.context("context must be set")?
|
||||
.files
|
||||
.clone(),
|
||||
ep_store.edit_history_for_project(&project, cx),
|
||||
events,
|
||||
example.spec.cursor_path.clone(),
|
||||
example
|
||||
.buffer
|
||||
|
||||
@@ -15,8 +15,7 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
git.workspace = true
|
||||
log.workspace = true
|
||||
collections.workspace = true
|
||||
time.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
@@ -50,11 +49,18 @@ zed_actions.workspace = true
|
||||
zeta_prompt.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock.workspace = true
|
||||
copilot = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
futures.workspace = true
|
||||
indoc.workspace = true
|
||||
language_model.workspace = true
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
semver.workspace = true
|
||||
serde_json.workspace = true
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
|
||||
@@ -36,8 +36,8 @@ use std::{
|
||||
};
|
||||
use supermaven::{AccountStatus, Supermaven};
|
||||
use ui::{
|
||||
Clickable, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton,
|
||||
IconButtonShape, Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
|
||||
Clickable, ContextMenu, ContextMenuEntry, DocumentationSide, IconButton, IconButtonShape,
|
||||
Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
@@ -680,7 +680,7 @@ impl EditPredictionButton {
|
||||
menu = menu.item(
|
||||
entry
|
||||
.disabled(true)
|
||||
.documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_cx| {
|
||||
.documentation_aside(DocumentationSide::Left, move |_cx| {
|
||||
Label::new(format!("Edit predictions cannot be toggled for this buffer because they are disabled for {}", language.name()))
|
||||
.into_any_element()
|
||||
})
|
||||
@@ -726,7 +726,7 @@ impl EditPredictionButton {
|
||||
.item(
|
||||
ContextMenuEntry::new("Eager")
|
||||
.toggleable(IconPosition::Start, eager_mode)
|
||||
.documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_| {
|
||||
.documentation_aside(DocumentationSide::Left, move |_| {
|
||||
Label::new("Display predictions inline when there are no language server completions available.").into_any_element()
|
||||
})
|
||||
.handler({
|
||||
@@ -739,7 +739,7 @@ impl EditPredictionButton {
|
||||
.item(
|
||||
ContextMenuEntry::new("Subtle")
|
||||
.toggleable(IconPosition::Start, subtle_mode)
|
||||
.documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |_| {
|
||||
.documentation_aside(DocumentationSide::Left, move |_| {
|
||||
Label::new("Display predictions inline only when holding a modifier key (alt by default).").into_any_element()
|
||||
})
|
||||
.handler({
|
||||
@@ -778,7 +778,7 @@ impl EditPredictionButton {
|
||||
.toggleable(IconPosition::Start, data_collection.is_enabled())
|
||||
.icon(icon_name)
|
||||
.icon_color(icon_color)
|
||||
.documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, move |cx| {
|
||||
.documentation_aside(DocumentationSide::Left, move |cx| {
|
||||
let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) {
|
||||
(true, true) => (
|
||||
"Project identified as open source, and you're sharing data.",
|
||||
@@ -862,7 +862,7 @@ impl EditPredictionButton {
|
||||
ContextMenuEntry::new("Configure Excluded Files")
|
||||
.icon(IconName::LockOutlined)
|
||||
.icon_color(Color::Muted)
|
||||
.documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, |_| {
|
||||
.documentation_aside(DocumentationSide::Left, |_| {
|
||||
Label::new(indoc!{"
|
||||
Open your settings to add sensitive paths for which Zed will never predict edits."}).into_any_element()
|
||||
})
|
||||
@@ -915,11 +915,8 @@ impl EditPredictionButton {
|
||||
.when(
|
||||
cx.has_flag::<PredictEditsRatePredictionsFeatureFlag>(),
|
||||
|this| {
|
||||
this.action(
|
||||
"Capture Edit Prediction Example",
|
||||
CaptureExample.boxed_clone(),
|
||||
)
|
||||
.action("Rate Predictions", RatePredictions.boxed_clone())
|
||||
this.action("Capture Prediction Example", CaptureExample.boxed_clone())
|
||||
.action("Rate Predictions", RatePredictions.boxed_clone())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,25 +2,17 @@ mod edit_prediction_button;
|
||||
mod edit_prediction_context_view;
|
||||
mod rate_prediction_modal;
|
||||
|
||||
use std::any::{Any as _, TypeId};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use edit_prediction::{
|
||||
EditPredictionStore, ResetOnboarding, Zeta2FeatureFlag, example_spec::ExampleSpec,
|
||||
};
|
||||
use edit_prediction::{ResetOnboarding, Zeta2FeatureFlag, capture_example};
|
||||
use edit_prediction_context_view::EditPredictionContextView;
|
||||
use editor::Editor;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use git::repository::DiffType;
|
||||
use gpui::{Window, actions};
|
||||
use language::ToPoint as _;
|
||||
use log;
|
||||
use gpui::actions;
|
||||
use language::language_settings::AllLanguageSettings;
|
||||
use project::DisableAiSettings;
|
||||
use rate_prediction_modal::RatePredictionsModal;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use text::ToOffset as _;
|
||||
use std::any::{Any as _, TypeId};
|
||||
use ui::{App, prelude::*};
|
||||
use workspace::{SplitDirection, Workspace};
|
||||
|
||||
@@ -56,7 +48,9 @@ pub fn init(cx: &mut App) {
|
||||
}
|
||||
});
|
||||
|
||||
workspace.register_action(capture_edit_prediction_example);
|
||||
workspace.register_action(|workspace, _: &CaptureExample, window, cx| {
|
||||
capture_example_as_markdown(workspace, window, cx);
|
||||
});
|
||||
workspace.register_action_renderer(|div, _, _, cx| {
|
||||
let has_flag = cx.has_flag::<Zeta2FeatureFlag>();
|
||||
div.when(has_flag, |div| {
|
||||
@@ -138,182 +132,48 @@ fn feature_gate_predict_edits_actions(cx: &mut App) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn capture_edit_prediction_example(
|
||||
fn capture_example_as_markdown(
|
||||
workspace: &mut Workspace,
|
||||
_: &CaptureExample,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let Some(ep_store) = EditPredictionStore::try_global(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = workspace.project().clone();
|
||||
|
||||
let (worktree_root, repository) = {
|
||||
let project_ref = project.read(cx);
|
||||
let worktree_root = project_ref
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.map(|worktree| worktree.read(cx).abs_path());
|
||||
let repository = project_ref.active_repository(cx);
|
||||
(worktree_root, repository)
|
||||
};
|
||||
|
||||
let (Some(worktree_root), Some(repository)) = (worktree_root, repository) else {
|
||||
log::error!("CaptureExampleSpec: missing worktree or active repository");
|
||||
return;
|
||||
};
|
||||
|
||||
let repository_snapshot = repository.read(cx).snapshot();
|
||||
if worktree_root.as_ref() != repository_snapshot.work_directory_abs_path.as_ref() {
|
||||
log::error!(
|
||||
"repository is not at worktree root (repo={:?}, worktree={:?})",
|
||||
repository_snapshot.work_directory_abs_path,
|
||||
worktree_root
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(repository_url) = repository_snapshot
|
||||
.remote_origin_url
|
||||
.clone()
|
||||
.or_else(|| repository_snapshot.remote_upstream_url.clone())
|
||||
else {
|
||||
log::error!("active repository has no origin/upstream remote url");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(revision) = repository_snapshot
|
||||
.head_commit
|
||||
.as_ref()
|
||||
.map(|commit| commit.sha.to_string())
|
||||
else {
|
||||
log::error!("active repository has no head commit");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut events = ep_store.update(cx, |store, cx| {
|
||||
store.edit_history_for_project_with_pause_split_last_event(&project, cx)
|
||||
});
|
||||
|
||||
let Some(editor) = workspace.active_item_as::<Editor>(cx) else {
|
||||
log::error!("no active editor");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(project_path) = editor.read(cx).project_path(cx) else {
|
||||
log::error!("active editor has no project path");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((buffer, cursor_anchor)) = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.text_anchor_for_position(editor.read(cx).selections.newest_anchor().head(), cx)
|
||||
else {
|
||||
log::error!("failed to resolve cursor buffer/anchor");
|
||||
return;
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let cursor_point = cursor_anchor.to_point(&snapshot);
|
||||
let (_editable_range, context_range) =
|
||||
edit_prediction::cursor_excerpt::editable_and_context_ranges_for_cursor_position(
|
||||
cursor_point,
|
||||
&snapshot,
|
||||
100,
|
||||
50,
|
||||
);
|
||||
|
||||
let cursor_path: Arc<Path> = repository
|
||||
.read(cx)
|
||||
.project_path_to_repo_path(&project_path, cx)
|
||||
.map(|repo_path| Path::new(repo_path.as_unix_str()).into())
|
||||
.unwrap_or_else(|| Path::new(project_path.path.as_unix_str()).into());
|
||||
|
||||
let cursor_position = {
|
||||
let context_start_offset = context_range.start.to_offset(&snapshot);
|
||||
let cursor_offset = cursor_anchor.to_offset(&snapshot);
|
||||
let cursor_offset_in_excerpt = cursor_offset.saturating_sub(context_start_offset);
|
||||
let mut excerpt = snapshot.text_for_range(context_range).collect::<String>();
|
||||
if cursor_offset_in_excerpt <= excerpt.len() {
|
||||
excerpt.insert_str(cursor_offset_in_excerpt, zeta_prompt::CURSOR_MARKER);
|
||||
}
|
||||
excerpt
|
||||
};
|
||||
|
||||
) -> Option<()> {
|
||||
let markdown_language = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let project = workspace.project().clone();
|
||||
let editor = workspace.active_item_as::<Editor>(cx)?;
|
||||
let editor = editor.read(cx);
|
||||
let (buffer, cursor_anchor) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.text_anchor_for_position(editor.selections.newest_anchor().head(), cx)?;
|
||||
let example = capture_example(project.clone(), buffer, cursor_anchor, true, cx)?;
|
||||
|
||||
let examples_dir = AllLanguageSettings::get_global(cx)
|
||||
.edit_predictions
|
||||
.examples_dir
|
||||
.clone();
|
||||
|
||||
cx.spawn_in(window, async move |workspace_entity, cx| {
|
||||
let markdown_language = markdown_language.await?;
|
||||
let example_spec = example.await?;
|
||||
let buffer = if let Some(dir) = examples_dir {
|
||||
fs.create_dir(&dir).await.ok();
|
||||
let mut path = dir.join(&example_spec.name.replace(' ', "--").replace(':', "-"));
|
||||
path.set_extension("md");
|
||||
project.update(cx, |project, cx| project.open_local_buffer(&path, cx))
|
||||
} else {
|
||||
project.update(cx, |project, cx| project.create_buffer(false, cx))
|
||||
}?
|
||||
.await?;
|
||||
|
||||
let uncommitted_diff_rx = repository.update(cx, |repository, cx| {
|
||||
repository.diff(DiffType::HeadToWorktree, cx)
|
||||
})?;
|
||||
|
||||
let uncommitted_diff = match uncommitted_diff_rx.await {
|
||||
Ok(Ok(diff)) => diff,
|
||||
Ok(Err(error)) => {
|
||||
log::error!("failed to compute uncommitted diff: {error:#}");
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("uncommitted diff channel dropped: {error:#}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let mut edit_history = String::new();
|
||||
let mut expected_patch = String::new();
|
||||
if let Some(last_event) = events.pop() {
|
||||
for event in &events {
|
||||
zeta_prompt::write_event(&mut edit_history, event);
|
||||
if !edit_history.ends_with('\n') {
|
||||
edit_history.push('\n');
|
||||
}
|
||||
edit_history.push('\n');
|
||||
}
|
||||
|
||||
zeta_prompt::write_event(&mut expected_patch, &last_event);
|
||||
}
|
||||
|
||||
let format =
|
||||
time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]");
|
||||
let name = match format {
|
||||
Ok(format) => {
|
||||
let now = time::OffsetDateTime::now_local()
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
|
||||
now.format(&format)
|
||||
.unwrap_or_else(|_| "unknown-time".to_string())
|
||||
}
|
||||
Err(_) => "unknown-time".to_string(),
|
||||
};
|
||||
|
||||
let markdown = ExampleSpec {
|
||||
name,
|
||||
repository_url,
|
||||
revision,
|
||||
uncommitted_diff,
|
||||
cursor_path,
|
||||
cursor_position,
|
||||
edit_history,
|
||||
expected_patch,
|
||||
}
|
||||
.to_markdown();
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.create_buffer(false, cx))?
|
||||
.await?;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_text(markdown, cx);
|
||||
buffer.set_text(example_spec.to_markdown(), cx);
|
||||
buffer.set_language(Some(markdown_language), cx);
|
||||
})?;
|
||||
|
||||
workspace_entity.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(
|
||||
@@ -327,4 +187,5 @@ fn capture_edit_prediction_example(
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use buffer_diff::BufferDiff;
|
||||
use edit_prediction::{EditPrediction, EditPredictionRating, EditPredictionStore};
|
||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use feature_flags::FeatureFlag;
|
||||
@@ -323,22 +323,23 @@ impl RatePredictionsModal {
|
||||
let start = Point::new(range.start.row.saturating_sub(5), 0);
|
||||
let end = Point::new(range.end.row + 5, 0).min(new_buffer_snapshot.max_point());
|
||||
|
||||
let diff = cx.new::<BufferDiff>(|cx| {
|
||||
let diff_snapshot = BufferDiffSnapshot::new_with_base_buffer(
|
||||
let language = new_buffer_snapshot.language().cloned();
|
||||
let diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot.text, cx));
|
||||
diff.update(cx, |diff, cx| {
|
||||
let update = diff.update_diff(
|
||||
new_buffer_snapshot.text.clone(),
|
||||
Some(old_buffer_snapshot.text().into()),
|
||||
old_buffer_snapshot.clone(),
|
||||
true,
|
||||
language,
|
||||
cx,
|
||||
);
|
||||
let diff = BufferDiff::new(&new_buffer_snapshot, cx);
|
||||
cx.spawn(async move |diff, cx| {
|
||||
let diff_snapshot = diff_snapshot.await;
|
||||
let update = update.await;
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(diff_snapshot, &new_buffer_snapshot.text, cx);
|
||||
diff.set_snapshot(update, &new_buffer_snapshot.text, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
diff
|
||||
});
|
||||
|
||||
editor.disable_header_for_buffer(new_buffer_id, cx);
|
||||
|
||||
@@ -232,8 +232,6 @@ impl DisplayMap {
|
||||
.update(cx, |map, cx| map.sync(tab_snapshot, edits, cx));
|
||||
let block_snapshot = self.block_map.read(wrap_snapshot, edits).snapshot;
|
||||
|
||||
// todo word diff here?
|
||||
|
||||
DisplaySnapshot {
|
||||
block_snapshot,
|
||||
diagnostics_max_severity: self.diagnostics_max_severity,
|
||||
|
||||
@@ -1119,7 +1119,6 @@ impl Iterator for WrapRows<'_> {
|
||||
RowInfo {
|
||||
buffer_id: None,
|
||||
buffer_row: None,
|
||||
base_text_row: None,
|
||||
multibuffer_row: None,
|
||||
diff_status,
|
||||
expand_info: None,
|
||||
|
||||
@@ -1072,6 +1072,7 @@ pub struct Editor {
|
||||
minimap_visibility: MinimapVisibility,
|
||||
offset_content: bool,
|
||||
disable_expand_excerpt_buttons: bool,
|
||||
delegate_expand_excerpts: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
use_relative_line_numbers: Option<bool>,
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
@@ -1203,6 +1204,7 @@ pub struct Editor {
|
||||
hide_mouse_mode: HideMouseMode,
|
||||
pub change_list: ChangeList,
|
||||
inline_value_cache: InlineValueCache,
|
||||
number_deleted_lines: bool,
|
||||
|
||||
selection_drag_state: SelectionDragState,
|
||||
colors: Option<LspColorData>,
|
||||
@@ -1215,7 +1217,6 @@ pub struct Editor {
|
||||
applicable_language_settings: HashMap<Option<LanguageName>, LanguageSettings>,
|
||||
accent_data: Option<AccentData>,
|
||||
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
|
||||
use_base_text_line_numbers: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -1256,6 +1257,7 @@ pub struct EditorSnapshot {
|
||||
show_gutter: bool,
|
||||
offset_content: bool,
|
||||
show_line_numbers: Option<bool>,
|
||||
number_deleted_lines: bool,
|
||||
show_git_diff_gutter: Option<bool>,
|
||||
show_code_actions: Option<bool>,
|
||||
show_runnables: Option<bool>,
|
||||
@@ -2237,6 +2239,7 @@ impl Editor {
|
||||
show_line_numbers: (!full_mode).then_some(false),
|
||||
use_relative_line_numbers: None,
|
||||
disable_expand_excerpt_buttons: !full_mode,
|
||||
delegate_expand_excerpts: false,
|
||||
show_git_diff_gutter: None,
|
||||
show_code_actions: None,
|
||||
show_runnables: None,
|
||||
@@ -2405,7 +2408,7 @@ impl Editor {
|
||||
applicable_language_settings: HashMap::default(),
|
||||
accent_data: None,
|
||||
fetched_tree_sitter_chunks: HashMap::default(),
|
||||
use_base_text_line_numbers: false,
|
||||
number_deleted_lines: false,
|
||||
};
|
||||
|
||||
if is_minimap {
|
||||
@@ -2940,6 +2943,7 @@ impl Editor {
|
||||
show_gutter: self.show_gutter,
|
||||
offset_content: self.offset_content,
|
||||
show_line_numbers: self.show_line_numbers,
|
||||
number_deleted_lines: self.number_deleted_lines,
|
||||
show_git_diff_gutter: self.show_git_diff_gutter,
|
||||
show_code_actions: self.show_code_actions,
|
||||
show_runnables: self.show_runnables,
|
||||
@@ -11496,7 +11500,7 @@ impl Editor {
|
||||
let buffer = buffer.read(cx);
|
||||
let original_text = diff
|
||||
.read(cx)
|
||||
.base_text()
|
||||
.base_text(cx)
|
||||
.as_rope()
|
||||
.slice(hunk.diff_base_byte_range.start.0..hunk.diff_base_byte_range.end.0);
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
@@ -16590,7 +16594,6 @@ impl Editor {
|
||||
&mut self,
|
||||
lines: u32,
|
||||
direction: ExpandExcerptDirection,
|
||||
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let selections = self.selections.disjoint_anchors_arc();
|
||||
@@ -16601,14 +16604,24 @@ impl Editor {
|
||||
lines
|
||||
};
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let mut excerpt_ids = selections
|
||||
.iter()
|
||||
.flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range()))
|
||||
.collect::<Vec<_>>();
|
||||
excerpt_ids.sort();
|
||||
excerpt_ids.dedup();
|
||||
|
||||
if self.delegate_expand_excerpts {
|
||||
cx.emit(EditorEvent::ExpandExcerptsRequested {
|
||||
excerpt_ids,
|
||||
lines,
|
||||
direction,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let mut excerpt_ids = selections
|
||||
.iter()
|
||||
.flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range()))
|
||||
.collect::<Vec<_>>();
|
||||
excerpt_ids.sort();
|
||||
excerpt_ids.dedup();
|
||||
buffer.expand_excerpts(excerpt_ids, lines, direction, cx)
|
||||
})
|
||||
}
|
||||
@@ -16620,8 +16633,18 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let current_scroll_position = self.scroll_position(cx);
|
||||
let lines_to_expand = EditorSettings::get_global(cx).expand_excerpt_lines;
|
||||
|
||||
if self.delegate_expand_excerpts {
|
||||
cx.emit(EditorEvent::ExpandExcerptsRequested {
|
||||
excerpt_ids: vec![excerpt],
|
||||
lines: lines_to_expand,
|
||||
direction,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let current_scroll_position = self.scroll_position(cx);
|
||||
let mut scroll = None;
|
||||
|
||||
if direction == ExpandExcerptDirection::Down {
|
||||
@@ -19698,10 +19721,6 @@ impl Editor {
|
||||
self.display_map.read(cx).fold_placeholder.clone()
|
||||
}
|
||||
|
||||
pub fn set_use_base_text_line_numbers(&mut self, show: bool, _cx: &mut Context<Self>) {
|
||||
self.use_base_text_line_numbers = show;
|
||||
}
|
||||
|
||||
pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_all_diff_hunks_expanded(cx);
|
||||
@@ -19943,7 +19962,7 @@ impl Editor {
|
||||
buffer_word_diffs: Vec::default(),
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.start.0
|
||||
..hunk.diff_base_byte_range.end.0,
|
||||
secondary_status: hunk.secondary_status,
|
||||
secondary_status: hunk.status.secondary,
|
||||
range: Point::zero()..Point::zero(), // unused
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
@@ -20572,6 +20591,10 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_delegate_expand_excerpts(&mut self, delegate: bool) {
|
||||
self.delegate_expand_excerpts = delegate;
|
||||
}
|
||||
|
||||
pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context<Self>) {
|
||||
self.show_git_diff_gutter = Some(show_git_diff_gutter);
|
||||
cx.notify();
|
||||
@@ -20983,8 +21006,12 @@ impl Editor {
|
||||
|
||||
Some((
|
||||
multi_buffer.buffer(buffer.remote_id()).unwrap(),
|
||||
buffer_diff_snapshot.row_to_base_text_row(start_row_in_buffer, buffer)
|
||||
..buffer_diff_snapshot.row_to_base_text_row(end_row_in_buffer, buffer),
|
||||
buffer_diff_snapshot.row_to_base_text_row(start_row_in_buffer, Bias::Left, buffer)
|
||||
..buffer_diff_snapshot.row_to_base_text_row(
|
||||
end_row_in_buffer,
|
||||
Bias::Left,
|
||||
buffer,
|
||||
),
|
||||
))
|
||||
});
|
||||
|
||||
@@ -25298,36 +25325,34 @@ impl EditorSnapshot {
|
||||
/// Returns the line delta from `base` to `line` in the multibuffer, ignoring wrapped lines.
|
||||
///
|
||||
/// This is positive if `base` is before `line`.
|
||||
fn relative_line_delta(&self, base: DisplayRow, line: DisplayRow) -> i64 {
|
||||
fn relative_line_delta(
|
||||
&self,
|
||||
base: DisplayRow,
|
||||
line: DisplayRow,
|
||||
consider_wrapped_lines: bool,
|
||||
) -> i64 {
|
||||
let point = DisplayPoint::new(line, 0).to_point(self);
|
||||
self.relative_line_delta_to_point(base, point)
|
||||
self.relative_line_delta_to_point(base, point, consider_wrapped_lines)
|
||||
}
|
||||
|
||||
/// Returns the line delta from `base` to `point` in the multibuffer, ignoring wrapped lines.
|
||||
/// Returns the line delta from `base` to `point` in the multibuffer.
|
||||
///
|
||||
/// This is positive if `base` is before `point`.
|
||||
pub fn relative_line_delta_to_point(&self, base: DisplayRow, point: Point) -> i64 {
|
||||
pub fn relative_line_delta_to_point(
|
||||
&self,
|
||||
base: DisplayRow,
|
||||
point: Point,
|
||||
consider_wrapped_lines: bool,
|
||||
) -> i64 {
|
||||
let base_point = DisplayPoint::new(base, 0).to_point(self);
|
||||
point.row as i64 - base_point.row as i64
|
||||
}
|
||||
|
||||
/// Returns the line delta from `base` to `line` in the multibuffer, counting wrapped lines.
|
||||
///
|
||||
/// This is positive if `base` is before `line`.
|
||||
fn relative_wrapped_line_delta(&self, base: DisplayRow, line: DisplayRow) -> i64 {
|
||||
let point = DisplayPoint::new(line, 0).to_point(self);
|
||||
self.relative_wrapped_line_delta_to_point(base, point)
|
||||
}
|
||||
|
||||
/// Returns the line delta from `base` to `point` in the multibuffer, counting wrapped lines.
|
||||
///
|
||||
/// This is positive if `base` is before `point`.
|
||||
pub fn relative_wrapped_line_delta_to_point(&self, base: DisplayRow, point: Point) -> i64 {
|
||||
let base_point = DisplayPoint::new(base, 0).to_point(self);
|
||||
let wrap_snapshot = self.wrap_snapshot();
|
||||
let base_wrap_row = wrap_snapshot.make_wrap_point(base_point, Bias::Left).row();
|
||||
let wrap_row = wrap_snapshot.make_wrap_point(point, Bias::Left).row();
|
||||
wrap_row.0 as i64 - base_wrap_row.0 as i64
|
||||
if consider_wrapped_lines {
|
||||
let wrap_snapshot = self.wrap_snapshot();
|
||||
let base_wrap_row = wrap_snapshot.make_wrap_point(base_point, Bias::Left).row();
|
||||
let wrap_row = wrap_snapshot.make_wrap_point(point, Bias::Left).row();
|
||||
wrap_row.0 as i64 - base_wrap_row.0 as i64
|
||||
} else {
|
||||
point.row as i64 - base_point.row as i64
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the unsigned relative line number to display for each row in `rows`.
|
||||
@@ -25339,23 +25364,21 @@ impl EditorSnapshot {
|
||||
relative_to: DisplayRow,
|
||||
count_wrapped_lines: bool,
|
||||
) -> HashMap<DisplayRow, u32> {
|
||||
let initial_offset = if count_wrapped_lines {
|
||||
self.relative_wrapped_line_delta(relative_to, rows.start)
|
||||
} else {
|
||||
self.relative_line_delta(relative_to, rows.start)
|
||||
};
|
||||
let display_row_infos = self
|
||||
.row_infos(rows.start)
|
||||
let initial_offset = self.relative_line_delta(relative_to, rows.start, count_wrapped_lines);
|
||||
|
||||
self.row_infos(rows.start)
|
||||
.take(rows.len())
|
||||
.enumerate()
|
||||
.map(|(i, row_info)| (DisplayRow(rows.start.0 + i as u32), row_info));
|
||||
display_row_infos
|
||||
.map(|(i, row_info)| (DisplayRow(rows.start.0 + i as u32), row_info))
|
||||
.filter(|(_row, row_info)| {
|
||||
row_info.buffer_row.is_some()
|
||||
|| (count_wrapped_lines && row_info.wrapped_buffer_row.is_some())
|
||||
})
|
||||
.enumerate()
|
||||
.map(|(i, (row, _row_info))| (row, (initial_offset + i as i64).unsigned_abs() as u32))
|
||||
.flat_map(|(i, (row, _row_info))| {
|
||||
(row != relative_to)
|
||||
.then_some((row, (initial_offset + i as i64).unsigned_abs() as u32))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -25413,6 +25436,11 @@ pub enum EditorEvent {
|
||||
ExcerptsExpanded {
|
||||
ids: Vec<ExcerptId>,
|
||||
},
|
||||
ExpandExcerptsRequested {
|
||||
excerpt_ids: Vec<ExcerptId>,
|
||||
lines: u32,
|
||||
direction: ExpandExcerptDirection,
|
||||
},
|
||||
BufferEdited,
|
||||
Edited {
|
||||
transaction_id: clock::Lamport,
|
||||
|
||||
@@ -36,8 +36,7 @@ use languages::markdown_lang;
|
||||
use languages::rust_lang;
|
||||
use lsp::CompletionParams;
|
||||
use multi_buffer::{
|
||||
ExcerptRange, IndentGuide, MultiBuffer, MultiBufferFilterMode, MultiBufferOffset,
|
||||
MultiBufferOffsetUtf16, PathKey,
|
||||
ExcerptRange, IndentGuide, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
@@ -13221,30 +13220,28 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
|
||||
// Handle formatting requests to the language server.
|
||||
cx.lsp
|
||||
.set_request_handler::<lsp::request::Formatting, _, _>({
|
||||
let buffer_changes = buffer_changes.clone();
|
||||
move |_, _| {
|
||||
let buffer_changes = buffer_changes.clone();
|
||||
// Insert blank lines between each line of the buffer.
|
||||
async move {
|
||||
// When formatting is requested, trailing whitespace has already been stripped,
|
||||
// and the trailing newline has already been added.
|
||||
assert_eq!(
|
||||
&buffer_changes.lock()[1..],
|
||||
&[
|
||||
(
|
||||
lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
|
||||
"".into()
|
||||
),
|
||||
(
|
||||
lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
|
||||
"".into()
|
||||
),
|
||||
(
|
||||
lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
|
||||
"\n".into()
|
||||
),
|
||||
]
|
||||
);
|
||||
// TODO: this assertion is not reliably true. Currently nothing guarantees that we deliver
|
||||
// DidChangedTextDocument to the LSP before sending the formatting request.
|
||||
// assert_eq!(
|
||||
// &buffer_changes.lock()[1..],
|
||||
// &[
|
||||
// (
|
||||
// lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
|
||||
// "".into()
|
||||
// ),
|
||||
// (
|
||||
// lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
|
||||
// "".into()
|
||||
// ),
|
||||
// (
|
||||
// lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
|
||||
// "\n".into()
|
||||
// ),
|
||||
// ]
|
||||
// );
|
||||
|
||||
Ok(Some(vec![
|
||||
lsp::TextEdit {
|
||||
@@ -13276,7 +13273,6 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
|
||||
]
|
||||
.join("\n"),
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
// Submit a format request.
|
||||
let format = cx
|
||||
@@ -18346,7 +18342,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
project_settings.lsp.0.insert(
|
||||
"Some other server name".into(),
|
||||
LspSettings {
|
||||
binary: None,
|
||||
@@ -18367,7 +18363,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
project_settings.lsp.0.insert(
|
||||
language_server_name.into(),
|
||||
LspSettings {
|
||||
binary: None,
|
||||
@@ -18388,7 +18384,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
project_settings.lsp.0.insert(
|
||||
language_server_name.into(),
|
||||
LspSettings {
|
||||
binary: None,
|
||||
@@ -18409,7 +18405,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
project_settings.lsp.0.insert(
|
||||
language_server_name.into(),
|
||||
LspSettings {
|
||||
binary: None,
|
||||
@@ -19884,7 +19880,9 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
|
||||
(buffer_2.clone(), base_text_2),
|
||||
(buffer_3.clone(), base_text_3),
|
||||
] {
|
||||
let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
|
||||
let diff = cx.new(|cx| {
|
||||
BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
|
||||
});
|
||||
editor
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| buffer.add_diff(diff, cx));
|
||||
@@ -20509,7 +20507,9 @@ async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
|
||||
(buffer_2.clone(), file_2_old),
|
||||
(buffer_3.clone(), file_3_old),
|
||||
] {
|
||||
let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
|
||||
let diff = cx.new(|cx| {
|
||||
BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
|
||||
});
|
||||
editor
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| buffer.add_diff(diff, cx));
|
||||
@@ -20615,7 +20615,9 @@ async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
|
||||
cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
|
||||
editor
|
||||
.update(cx, |editor, _window, cx| {
|
||||
let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
|
||||
let diff = cx.new(|cx| {
|
||||
BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
|
||||
});
|
||||
editor
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| buffer.add_diff(diff, cx))
|
||||
@@ -22049,7 +22051,9 @@ async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
|
||||
|
||||
editor.buffer().update(cx, |multibuffer, cx| {
|
||||
let buffer = multibuffer.as_singleton().unwrap();
|
||||
let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
|
||||
let diff = cx.new(|cx| {
|
||||
BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
|
||||
});
|
||||
|
||||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||||
multibuffer.add_diff(diff, cx);
|
||||
@@ -28725,7 +28729,7 @@ fn test_relative_line_numbers(cx: &mut TestAppContext) {
|
||||
assert_eq!(
|
||||
relative_number,
|
||||
snapshot
|
||||
.relative_line_delta(display_row, base_display_row)
|
||||
.relative_line_delta(display_row, base_display_row, false)
|
||||
.unsigned_abs() as u32,
|
||||
);
|
||||
}
|
||||
@@ -28735,6 +28739,7 @@ fn test_relative_line_numbers(cx: &mut TestAppContext) {
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
|
||||
.filter(|(row, _)| *row != base_display_row)
|
||||
.collect_vec();
|
||||
let actual_relative_numbers = snapshot
|
||||
.calculate_relative_line_numbers(
|
||||
@@ -28751,7 +28756,7 @@ fn test_relative_line_numbers(cx: &mut TestAppContext) {
|
||||
assert_eq!(
|
||||
relative_number,
|
||||
snapshot
|
||||
.relative_wrapped_line_delta(display_row, base_display_row)
|
||||
.relative_line_delta(display_row, base_display_row, true)
|
||||
.unsigned_abs() as u32,
|
||||
);
|
||||
}
|
||||
@@ -29222,208 +29227,6 @@ async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut leader_cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let diff_base = indoc!(
|
||||
r#"
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"#
|
||||
);
|
||||
|
||||
let initial_state = indoc!(
|
||||
r#"
|
||||
ˇone
|
||||
two
|
||||
THREE
|
||||
four
|
||||
five
|
||||
six
|
||||
"#
|
||||
);
|
||||
|
||||
leader_cx.set_state(initial_state);
|
||||
|
||||
leader_cx.set_head_text(&diff_base);
|
||||
leader_cx.run_until_parked();
|
||||
|
||||
let follower = leader_cx.update_multibuffer(|leader, cx| {
|
||||
leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
|
||||
leader.set_all_diff_hunks_expanded(cx);
|
||||
leader.get_or_create_follower(cx)
|
||||
});
|
||||
follower.update(cx, |follower, cx| {
|
||||
follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
|
||||
follower.set_all_diff_hunks_expanded(cx);
|
||||
});
|
||||
|
||||
let follower_editor =
|
||||
leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
|
||||
// leader_cx.window.focus(&follower_editor.focus_handle(cx));
|
||||
|
||||
let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
leader_cx.assert_editor_state(initial_state);
|
||||
follower_cx.assert_editor_state(indoc! {
|
||||
r#"
|
||||
ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"#
|
||||
});
|
||||
|
||||
follower_cx.editor(|editor, _window, cx| {
|
||||
assert!(editor.read_only(cx));
|
||||
});
|
||||
|
||||
leader_cx.update_editor(|editor, _window, cx| {
|
||||
editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
leader_cx.assert_editor_state(indoc! {
|
||||
r#"
|
||||
ˇone
|
||||
two
|
||||
THREE
|
||||
four
|
||||
FIVE
|
||||
six
|
||||
"#
|
||||
});
|
||||
|
||||
follower_cx.assert_editor_state(indoc! {
|
||||
r#"
|
||||
ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"#
|
||||
});
|
||||
|
||||
leader_cx.update_editor(|editor, _window, cx| {
|
||||
editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
leader_cx.assert_editor_state(indoc! {
|
||||
r#"
|
||||
ˇone
|
||||
two
|
||||
THREE
|
||||
four
|
||||
FIVE
|
||||
six
|
||||
SEVEN"#
|
||||
});
|
||||
|
||||
follower_cx.assert_editor_state(indoc! {
|
||||
r#"
|
||||
ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"#
|
||||
});
|
||||
|
||||
leader_cx.update_editor(|editor, window, cx| {
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.refresh_selected_text_highlights(true, window, cx);
|
||||
});
|
||||
leader_cx.run_until_parked();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let base_text = "base\n";
|
||||
let buffer_text = "buffer\n";
|
||||
|
||||
let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
|
||||
let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
|
||||
|
||||
let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
|
||||
let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
|
||||
let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
|
||||
let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
|
||||
|
||||
let leader = cx.new(|cx| {
|
||||
let mut leader = MultiBuffer::new(Capability::ReadWrite);
|
||||
leader.set_all_diff_hunks_expanded(cx);
|
||||
leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
|
||||
leader
|
||||
});
|
||||
let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
|
||||
follower.update(cx, |follower, _| {
|
||||
follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
|
||||
});
|
||||
|
||||
leader.update(cx, |leader, cx| {
|
||||
leader.insert_excerpts_after(
|
||||
ExcerptId::min(),
|
||||
extra_buffer_2.clone(),
|
||||
vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
|
||||
cx,
|
||||
);
|
||||
leader.add_diff(extra_diff_2.clone(), cx);
|
||||
|
||||
leader.insert_excerpts_after(
|
||||
ExcerptId::min(),
|
||||
extra_buffer_1.clone(),
|
||||
vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
|
||||
cx,
|
||||
);
|
||||
leader.add_diff(extra_diff_1.clone(), cx);
|
||||
|
||||
leader.insert_excerpts_after(
|
||||
ExcerptId::min(),
|
||||
buffer1.clone(),
|
||||
vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
|
||||
cx,
|
||||
);
|
||||
leader.add_diff(diff1.clone(), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
let mut cx = cx.add_empty_window();
|
||||
|
||||
let leader_editor = cx
|
||||
.new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
|
||||
let follower_editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::for_multibuffer(follower.clone(), None, window, cx)
|
||||
});
|
||||
|
||||
let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
|
||||
leader_cx.assert_editor_state(indoc! {"
|
||||
ˇbuffer
|
||||
|
||||
dummy text 1
|
||||
|
||||
dummy text 2
|
||||
"});
|
||||
let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
|
||||
follower_cx.assert_editor_state(indoc! {"
|
||||
ˇbase
|
||||
|
||||
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -29602,6 +29405,17 @@ async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 2.1: Works with uppercase checked marker too
|
||||
cx.set_state(indoc! {"
|
||||
- [X] completed taskˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [X] completed task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 3: Cursor position doesn't matter - content after marker is what counts
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] taˇsk
|
||||
@@ -30169,11 +29983,14 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) {
|
||||
.map(|wt| wt.read(cx).id())
|
||||
.expect("should have a worktree")
|
||||
});
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
|
||||
let trusted_worktrees =
|
||||
cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
|
||||
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
|
||||
let can_trust = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(!can_trust, "worktree should be restricted initially");
|
||||
|
||||
let buffer_before_approval = project
|
||||
@@ -30219,8 +30036,8 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) {
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
&worktree_store,
|
||||
std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -30247,8 +30064,9 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) {
|
||||
"inlay hints should be queried after trust approval"
|
||||
);
|
||||
|
||||
let can_trust_after =
|
||||
trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
|
||||
let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
|
||||
store.can_trust(&worktree_store, worktree_id, cx)
|
||||
});
|
||||
assert!(can_trust_after, "worktree should be trusted after trust()");
|
||||
}
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@ use gpui::{
|
||||
KeybindingKeystroke, Length, Modifiers, ModifiersChangedEvent, MouseButton, MouseClickEvent,
|
||||
MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement,
|
||||
Pixels, PressureStage, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
|
||||
Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, WeakEntity,
|
||||
Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px,
|
||||
quad, relative, size, solid_background, transparent_black,
|
||||
Size, StatefulInteractiveElement, Style, Styled, TextAlign, TextRun, TextStyleRefinement,
|
||||
WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline,
|
||||
point, px, quad, relative, size, solid_background, transparent_black,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{IndentGuideSettings, language_settings::ShowWhitespaceSetting};
|
||||
@@ -1695,9 +1695,13 @@ impl EditorElement {
|
||||
[cursor_position.row().minus(visible_display_row_range.start) as usize];
|
||||
let cursor_column = cursor_position.column() as usize;
|
||||
|
||||
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
|
||||
let mut block_width =
|
||||
cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
|
||||
let cursor_character_x = cursor_row_layout.x_for_index(cursor_column)
|
||||
+ cursor_row_layout
|
||||
.alignment_offset(self.style.text.text_align, text_hitbox.size.width);
|
||||
let cursor_next_x = cursor_row_layout.x_for_index(cursor_column + 1)
|
||||
+ cursor_row_layout
|
||||
.alignment_offset(self.style.text.text_align, text_hitbox.size.width);
|
||||
let mut block_width = cursor_next_x - cursor_character_x;
|
||||
if block_width == Pixels::ZERO {
|
||||
block_width = em_advance;
|
||||
}
|
||||
@@ -3260,17 +3264,15 @@ impl EditorElement {
|
||||
line_number.clear();
|
||||
let non_relative_number = if relative.wrapped() {
|
||||
row_info.buffer_row.or(row_info.wrapped_buffer_row)? + 1
|
||||
} else if self.editor.read(cx).use_base_text_line_numbers {
|
||||
row_info.base_text_row?.0 + 1
|
||||
} else {
|
||||
row_info.buffer_row? + 1
|
||||
};
|
||||
let relative_number = relative_rows.get(&display_row);
|
||||
if !(relative_line_numbers_enabled && relative_number.is_some())
|
||||
&& !snapshot.number_deleted_lines
|
||||
&& row_info
|
||||
.diff_status
|
||||
.is_some_and(|status| status.is_deleted())
|
||||
&& !self.editor.read(cx).use_base_text_line_numbers
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -4607,15 +4609,15 @@ impl EditorElement {
|
||||
);
|
||||
|
||||
let line_number = show_line_numbers.then(|| {
|
||||
let relative_number = relative_to.and_then(|base| match relative_line_numbers {
|
||||
RelativeLineNumbers::Disabled => None,
|
||||
RelativeLineNumbers::Enabled => {
|
||||
Some(snapshot.relative_line_delta_to_point(base, start_point))
|
||||
}
|
||||
RelativeLineNumbers::Wrapped => {
|
||||
Some(snapshot.relative_wrapped_line_delta_to_point(base, start_point))
|
||||
}
|
||||
});
|
||||
let relative_number = relative_to
|
||||
.filter(|_| relative_line_numbers != RelativeLineNumbers::Disabled)
|
||||
.map(|base| {
|
||||
snapshot.relative_line_delta_to_point(
|
||||
base,
|
||||
start_point,
|
||||
relative_line_numbers == RelativeLineNumbers::Wrapped,
|
||||
)
|
||||
});
|
||||
let number = relative_number
|
||||
.filter(|&delta| delta != 0)
|
||||
.map(|delta| delta.unsigned_abs() as u32)
|
||||
@@ -6160,10 +6162,25 @@ impl EditorElement {
|
||||
let color = cx.theme().colors().editor_hover_line_number;
|
||||
|
||||
let line = self.shape_line_number(shaped_line.text.clone(), color, window);
|
||||
line.paint(hitbox.origin, line_height, window, cx).log_err()
|
||||
line.paint(
|
||||
hitbox.origin,
|
||||
line_height,
|
||||
TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.log_err()
|
||||
} else {
|
||||
shaped_line
|
||||
.paint(hitbox.origin, line_height, window, cx)
|
||||
.paint(
|
||||
hitbox.origin,
|
||||
line_height,
|
||||
TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.log_err()
|
||||
}) else {
|
||||
continue;
|
||||
@@ -7252,23 +7269,27 @@ impl EditorElement {
|
||||
.map(|row| {
|
||||
let line_layout =
|
||||
&layout.position_map.line_layouts[row.minus(start_row) as usize];
|
||||
let alignment_offset =
|
||||
line_layout.alignment_offset(layout.text_align, layout.content_width);
|
||||
HighlightedRangeLine {
|
||||
start_x: if row == range.start.row() {
|
||||
layout.content_origin.x
|
||||
+ Pixels::from(
|
||||
ScrollPixelOffset::from(
|
||||
line_layout.x_for_index(range.start.column() as usize),
|
||||
line_layout.x_for_index(range.start.column() as usize)
|
||||
+ alignment_offset,
|
||||
) - layout.position_map.scroll_pixel_position.x,
|
||||
)
|
||||
} else {
|
||||
layout.content_origin.x
|
||||
layout.content_origin.x + alignment_offset
|
||||
- Pixels::from(layout.position_map.scroll_pixel_position.x)
|
||||
},
|
||||
end_x: if row == range.end.row() {
|
||||
layout.content_origin.x
|
||||
+ Pixels::from(
|
||||
ScrollPixelOffset::from(
|
||||
line_layout.x_for_index(range.end.column() as usize),
|
||||
line_layout.x_for_index(range.end.column() as usize)
|
||||
+ alignment_offset,
|
||||
) - layout.position_map.scroll_pixel_position.x,
|
||||
)
|
||||
} else {
|
||||
@@ -7276,6 +7297,7 @@ impl EditorElement {
|
||||
ScrollPixelOffset::from(
|
||||
layout.content_origin.x
|
||||
+ line_layout.width
|
||||
+ alignment_offset
|
||||
+ line_end_overshoot,
|
||||
) - layout.position_map.scroll_pixel_position.x,
|
||||
)
|
||||
@@ -8516,8 +8538,15 @@ impl LineWithInvisibles {
|
||||
for fragment in &self.fragments {
|
||||
match fragment {
|
||||
LineFragment::Text(line) => {
|
||||
line.paint(fragment_origin, line_height, window, cx)
|
||||
.log_err();
|
||||
line.paint(
|
||||
fragment_origin,
|
||||
line_height,
|
||||
layout.text_align,
|
||||
Some(layout.content_width),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
fragment_origin.x += line.width;
|
||||
}
|
||||
LineFragment::Element { size, .. } => {
|
||||
@@ -8559,8 +8588,15 @@ impl LineWithInvisibles {
|
||||
for fragment in &self.fragments {
|
||||
match fragment {
|
||||
LineFragment::Text(line) => {
|
||||
line.paint_background(fragment_origin, line_height, window, cx)
|
||||
.log_err();
|
||||
line.paint_background(
|
||||
fragment_origin,
|
||||
line_height,
|
||||
layout.text_align,
|
||||
Some(layout.content_width),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
fragment_origin.x += line.width;
|
||||
}
|
||||
LineFragment::Element { size, .. } => {
|
||||
@@ -8609,7 +8645,7 @@ impl LineWithInvisibles {
|
||||
[token_offset, token_end_offset],
|
||||
Box::new(move |window: &mut Window, cx: &mut App| {
|
||||
invisible_symbol
|
||||
.paint(origin, line_height, window, cx)
|
||||
.paint(origin, line_height, TextAlign::Left, None, window, cx)
|
||||
.log_err();
|
||||
}),
|
||||
)
|
||||
@@ -8770,6 +8806,15 @@ impl LineWithInvisibles {
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn alignment_offset(&self, text_align: TextAlign, content_width: Pixels) -> Pixels {
|
||||
let line_width = self.width;
|
||||
match text_align {
|
||||
TextAlign::Left => px(0.0),
|
||||
TextAlign::Center => (content_width - line_width) / 2.0,
|
||||
TextAlign::Right => content_width - line_width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -9008,14 +9053,8 @@ impl Element for EditorElement {
|
||||
let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
|
||||
let glyph_grid_cell = size(em_advance, line_height);
|
||||
|
||||
let gutter_dimensions = snapshot
|
||||
.gutter_dimensions(
|
||||
font_id,
|
||||
font_size,
|
||||
style,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let gutter_dimensions =
|
||||
snapshot.gutter_dimensions(font_id, font_size, style, window, cx);
|
||||
let text_width = bounds.size.width - gutter_dimensions.width;
|
||||
|
||||
let settings = EditorSettings::get_global(cx);
|
||||
@@ -9229,10 +9268,10 @@ impl Element for EditorElement {
|
||||
};
|
||||
|
||||
let background_color = match diff_status.kind {
|
||||
DiffHunkStatusKind::Added =>
|
||||
cx.theme().colors().version_control_added,
|
||||
DiffHunkStatusKind::Deleted =>
|
||||
cx.theme().colors().version_control_deleted,
|
||||
DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
|
||||
DiffHunkStatusKind::Deleted => {
|
||||
cx.theme().colors().version_control_deleted
|
||||
}
|
||||
DiffHunkStatusKind::Modified => {
|
||||
debug_panic!("modified diff status for row info");
|
||||
continue;
|
||||
@@ -9376,25 +9415,26 @@ impl Element for EditorElement {
|
||||
);
|
||||
|
||||
// relative rows are based on newest selection, even outside the visible area
|
||||
let relative_row_base = self.editor.update(cx, |editor, cx| {
|
||||
if editor.selections.count()==0 {
|
||||
return None;
|
||||
}
|
||||
let relative_row_base = self.editor.update(cx, |editor, cx| {
|
||||
(editor.selections.count() != 0).then(|| {
|
||||
let newest = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx));
|
||||
Some(SelectionLayout::new(
|
||||
|
||||
SelectionLayout::new(
|
||||
newest,
|
||||
editor.selections.line_mode(),
|
||||
editor.cursor_offset_on_selection,
|
||||
editor.cursor_shape,
|
||||
&snapshot.display_snapshot,
|
||||
&snapshot,
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.head.row())
|
||||
});
|
||||
.head
|
||||
.row()
|
||||
})
|
||||
});
|
||||
|
||||
let mut breakpoint_rows = self.editor.update(cx, |editor, cx| {
|
||||
editor.active_breakpoints(start_row..end_row, window, cx)
|
||||
@@ -9554,9 +9594,10 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
debug_panic!(
|
||||
"skipping recursive prepaint at max depth. renderer widths may be stale."
|
||||
);
|
||||
debug_panic!(concat!(
|
||||
"skipping recursive prepaint at max depth. ",
|
||||
"renderer widths may be stale."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9668,9 +9709,10 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
debug_panic!(
|
||||
"skipping recursive prepaint at max depth. block layout may be stale."
|
||||
);
|
||||
debug_panic!(concat!(
|
||||
"skipping recursive prepaint at max depth. ",
|
||||
"block layout may be stale."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10172,6 +10214,8 @@ impl Element for EditorElement {
|
||||
em_width,
|
||||
em_advance,
|
||||
snapshot,
|
||||
text_align: self.style.text.text_align,
|
||||
content_width: text_hitbox.size.width,
|
||||
gutter_hitbox: gutter_hitbox.clone(),
|
||||
text_hitbox: text_hitbox.clone(),
|
||||
inline_blame_bounds: inline_blame_layout
|
||||
@@ -10225,6 +10269,8 @@ impl Element for EditorElement {
|
||||
sticky_buffer_header,
|
||||
sticky_headers,
|
||||
expand_toggles,
|
||||
text_align: self.style.text.text_align,
|
||||
content_width: text_hitbox.size.width,
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -10405,6 +10451,8 @@ pub struct EditorLayout {
|
||||
sticky_buffer_header: Option<AnyElement>,
|
||||
sticky_headers: Option<StickyHeaders>,
|
||||
document_colors: Option<(DocumentColorsRenderMode, Vec<(Range<DisplayPoint>, Hsla)>)>,
|
||||
text_align: TextAlign,
|
||||
content_width: Pixels,
|
||||
}
|
||||
|
||||
struct StickyHeaders {
|
||||
@@ -10572,7 +10620,9 @@ impl StickyHeaderLine {
|
||||
gutter_origin.x + gutter_width - gutter_right_padding - line_number.width,
|
||||
gutter_origin.y,
|
||||
);
|
||||
line_number.paint(origin, line_height, window, cx).log_err();
|
||||
line_number
|
||||
.paint(origin, line_height, TextAlign::Left, None, window, cx)
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11011,6 +11061,8 @@ pub(crate) struct PositionMap {
|
||||
pub visible_row_range: Range<DisplayRow>,
|
||||
pub line_layouts: Vec<LineWithInvisibles>,
|
||||
pub snapshot: EditorSnapshot,
|
||||
pub text_align: TextAlign,
|
||||
pub content_width: Pixels,
|
||||
pub text_hitbox: Hitbox,
|
||||
pub gutter_hitbox: Hitbox,
|
||||
pub inline_blame_bounds: Option<(Bounds<Pixels>, BufferId, BlameEntry)>,
|
||||
@@ -11076,10 +11128,12 @@ impl PositionMap {
|
||||
.line_layouts
|
||||
.get(row as usize - scroll_position.y as usize)
|
||||
{
|
||||
if let Some(ix) = line.index_for_x(x) {
|
||||
let alignment_offset = line.alignment_offset(self.text_align, self.content_width);
|
||||
let x_relative_to_text = x - alignment_offset;
|
||||
if let Some(ix) = line.index_for_x(x_relative_to_text) {
|
||||
(ix as u32, px(0.))
|
||||
} else {
|
||||
(line.len as u32, px(0.).max(x - line.width))
|
||||
(line.len as u32, px(0.).max(x_relative_to_text - line.width))
|
||||
}
|
||||
} else {
|
||||
(0, x)
|
||||
@@ -11268,7 +11322,14 @@ impl CursorLayout {
|
||||
|
||||
if let Some(block_text) = &self.block_text {
|
||||
block_text
|
||||
.paint(self.origin + origin, self.line_height, window, cx)
|
||||
.paint(
|
||||
self.origin + origin,
|
||||
self.line_height,
|
||||
TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
@@ -11657,6 +11718,7 @@ mod tests {
|
||||
assert_eq!(relative_rows[&DisplayRow(1)], 2);
|
||||
assert_eq!(relative_rows[&DisplayRow(2)], 1);
|
||||
// current line has no relative number
|
||||
assert!(!relative_rows.contains_key(&DisplayRow(3)));
|
||||
assert_eq!(relative_rows[&DisplayRow(4)], 1);
|
||||
assert_eq!(relative_rows[&DisplayRow(5)], 2);
|
||||
|
||||
@@ -11803,6 +11865,7 @@ mod tests {
|
||||
assert_eq!(relative_rows[&DisplayRow(1)], 2);
|
||||
assert_eq!(relative_rows[&DisplayRow(2)], 1);
|
||||
// current line has no relative number
|
||||
assert!(!relative_rows.contains_key(&DisplayRow(3)));
|
||||
assert_eq!(relative_rows[&DisplayRow(4)], 1);
|
||||
assert_eq!(relative_rows[&DisplayRow(5)], 2);
|
||||
|
||||
@@ -11858,6 +11921,7 @@ mod tests {
|
||||
assert_eq!(relative_rows[&DisplayRow(1)], 2);
|
||||
assert_eq!(relative_rows[&DisplayRow(2)], 1);
|
||||
// current line, even if deleted, has no relative number
|
||||
assert!(!relative_rows.contains_key(&DisplayRow(3)));
|
||||
assert_eq!(relative_rows[&DisplayRow(4)], 1);
|
||||
assert_eq!(relative_rows[&DisplayRow(5)], 2);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ use std::{borrow::Cow, cell::RefCell};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use std::{path::PathBuf, rc::Rc};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Scrollbars, WithScrollbar, prelude::*, theme_is_transparent};
|
||||
use ui::{CopyButton, Scrollbars, WithScrollbar, prelude::*, theme_is_transparent};
|
||||
use url::Url;
|
||||
use util::TryFutureExt;
|
||||
use workspace::{OpenOptions, OpenVisible, Workspace};
|
||||
@@ -994,11 +994,13 @@ impl DiagnosticPopover {
|
||||
.border_color(self.border_color)
|
||||
.rounded_lg()
|
||||
.child(
|
||||
div()
|
||||
h_flex()
|
||||
.id("diagnostic-content-container")
|
||||
.overflow_y_scroll()
|
||||
.gap_1()
|
||||
.items_start()
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.child(
|
||||
MarkdownElement::new(
|
||||
@@ -1021,7 +1023,11 @@ impl DiagnosticPopover {
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.child({
|
||||
let message = self.local_diagnostic.diagnostic.message.clone();
|
||||
CopyButton::new(message).tooltip_label("Copy Diagnostic")
|
||||
}),
|
||||
)
|
||||
.custom_scrollbars(
|
||||
Scrollbars::for_settings::<EditorSettings>()
|
||||
|
||||
@@ -17,8 +17,8 @@ use gpui::{
|
||||
ParentElement, Pixels, SharedString, Styled, Task, WeakEntity, Window, point,
|
||||
};
|
||||
use language::{
|
||||
Bias, Buffer, BufferRow, CharKind, CharScopeContext, DiskState, LocalFile, Point,
|
||||
SelectionGoal, proto::serialize_anchor as serialize_text_anchor,
|
||||
Bias, Buffer, BufferRow, CharKind, CharScopeContext, LocalFile, Point, SelectionGoal,
|
||||
proto::serialize_anchor as serialize_text_anchor,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::MultiBufferOffset;
|
||||
@@ -722,7 +722,7 @@ impl Item for Editor {
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.is_some_and(|file| file.disk_state() == DiskState::Deleted);
|
||||
.is_some_and(|file| file.disk_state().is_deleted());
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
|
||||
@@ -164,11 +164,6 @@ pub fn deploy_context_menu(
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
}
|
||||
|
||||
// Don't show context menu for inline editors
|
||||
if !editor.mode().is_full() {
|
||||
return;
|
||||
}
|
||||
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let source_anchor = display_map.display_point_to_anchor(point, text::Bias::Right);
|
||||
let context_menu = if let Some(custom) = editor.custom_context_menu.take() {
|
||||
@@ -179,6 +174,11 @@ pub fn deploy_context_menu(
|
||||
};
|
||||
menu
|
||||
} else {
|
||||
// Don't show context menu for inline editors (only applies to default menu)
|
||||
if !editor.mode().is_full() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show the context menu if there isn't a project associated with this editor
|
||||
let Some(project) = editor.project.clone() else {
|
||||
return;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashMap;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
|
||||
use gpui::{
|
||||
Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
|
||||
};
|
||||
use multi_buffer::{MultiBuffer, MultiBufferFilterMode};
|
||||
use language::{Buffer, Capability};
|
||||
use multi_buffer::{Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, PathKey};
|
||||
use project::Project;
|
||||
use rope::Point;
|
||||
use text::{Bias, OffsetRangeExt as _};
|
||||
use ui::{
|
||||
App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
|
||||
Styled as _, Window, div,
|
||||
@@ -33,6 +40,7 @@ struct SplitDiff;
|
||||
struct UnsplitDiff;
|
||||
|
||||
pub struct SplittableEditor {
|
||||
primary_multibuffer: Entity<MultiBuffer>,
|
||||
primary_editor: Entity<Editor>,
|
||||
secondary: Option<SecondaryEditor>,
|
||||
panes: PaneGroup,
|
||||
@@ -41,9 +49,12 @@ pub struct SplittableEditor {
|
||||
}
|
||||
|
||||
struct SecondaryEditor {
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
pane: Entity<Pane>,
|
||||
has_latest_selection: bool,
|
||||
primary_to_secondary: HashMap<ExcerptId, ExcerptId>,
|
||||
secondary_to_primary: HashMap<ExcerptId, ExcerptId>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -63,14 +74,22 @@ impl SplittableEditor {
|
||||
}
|
||||
|
||||
pub fn new_unsplit(
|
||||
buffer: Entity<MultiBuffer>,
|
||||
primary_multibuffer: Entity<MultiBuffer>,
|
||||
project: Entity<Project>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let primary_editor =
|
||||
cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx));
|
||||
let primary_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(
|
||||
primary_multibuffer.clone(),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor
|
||||
});
|
||||
let pane = cx.new(|cx| {
|
||||
let mut pane = Pane::new(
|
||||
workspace.downgrade(),
|
||||
@@ -88,17 +107,25 @@ impl SplittableEditor {
|
||||
});
|
||||
let panes = PaneGroup::new(pane);
|
||||
// TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
|
||||
let subscriptions =
|
||||
vec![
|
||||
cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
|
||||
if let EditorEvent::SelectionsChanged { .. } = event
|
||||
&& let Some(secondary) = &mut this.secondary
|
||||
{
|
||||
let subscriptions = vec![cx.subscribe(
|
||||
&primary_editor,
|
||||
|this, _, event: &EditorEvent, cx| match event {
|
||||
EditorEvent::ExpandExcerptsRequested {
|
||||
excerpt_ids,
|
||||
lines,
|
||||
direction,
|
||||
} => {
|
||||
this.expand_excerpts(excerpt_ids.iter().copied(), *lines, *direction, cx);
|
||||
}
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
if let Some(secondary) = &mut this.secondary {
|
||||
secondary.has_latest_selection = false;
|
||||
}
|
||||
cx.emit(event.clone())
|
||||
}),
|
||||
];
|
||||
cx.emit(event.clone());
|
||||
}
|
||||
_ => cx.emit(event.clone()),
|
||||
},
|
||||
)];
|
||||
|
||||
window.defer(cx, {
|
||||
let workspace = workspace.downgrade();
|
||||
@@ -115,6 +142,7 @@ impl SplittableEditor {
|
||||
});
|
||||
Self {
|
||||
primary_editor,
|
||||
primary_multibuffer,
|
||||
secondary: None,
|
||||
panes,
|
||||
workspace: workspace.downgrade(),
|
||||
@@ -133,24 +161,22 @@ impl SplittableEditor {
|
||||
return;
|
||||
};
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let follower = self.primary_editor.update(cx, |primary, cx| {
|
||||
primary.buffer().update(cx, |buffer, cx| {
|
||||
let follower = buffer.get_or_create_follower(cx);
|
||||
buffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
|
||||
follower
|
||||
})
|
||||
|
||||
let secondary_multibuffer = cx.new(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(Capability::ReadOnly);
|
||||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||||
multibuffer
|
||||
});
|
||||
follower.update(cx, |follower, _| {
|
||||
follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
|
||||
});
|
||||
let secondary_editor = workspace.update(cx, |workspace, cx| {
|
||||
cx.new(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(follower, Some(project), window, cx);
|
||||
// TODO(split-diff) this should be at the multibuffer level
|
||||
editor.set_use_base_text_line_numbers(true, cx);
|
||||
editor.added_to_workspace(workspace, window, cx);
|
||||
editor
|
||||
})
|
||||
let secondary_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(
|
||||
secondary_multibuffer.clone(),
|
||||
Some(project.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.number_deleted_lines = true;
|
||||
editor.set_delegate_expand_excerpts(true);
|
||||
editor
|
||||
});
|
||||
let secondary_pane = cx.new(|cx| {
|
||||
let mut pane = Pane::new(
|
||||
@@ -175,23 +201,59 @@ impl SplittableEditor {
|
||||
pane
|
||||
});
|
||||
|
||||
let subscriptions =
|
||||
vec![
|
||||
cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
|
||||
if let EditorEvent::SelectionsChanged { .. } = event
|
||||
&& let Some(secondary) = &mut this.secondary
|
||||
{
|
||||
let subscriptions = vec![cx.subscribe(
|
||||
&secondary_editor,
|
||||
|this, _, event: &EditorEvent, cx| match event {
|
||||
EditorEvent::ExpandExcerptsRequested {
|
||||
excerpt_ids,
|
||||
lines,
|
||||
direction,
|
||||
} => {
|
||||
if let Some(secondary) = &this.secondary {
|
||||
let primary_ids: Vec<_> = excerpt_ids
|
||||
.iter()
|
||||
.filter_map(|id| secondary.secondary_to_primary.get(id).copied())
|
||||
.collect();
|
||||
this.expand_excerpts(primary_ids.into_iter(), *lines, *direction, cx);
|
||||
}
|
||||
}
|
||||
EditorEvent::SelectionsChanged { .. } => {
|
||||
if let Some(secondary) = &mut this.secondary {
|
||||
secondary.has_latest_selection = true;
|
||||
}
|
||||
cx.emit(event.clone())
|
||||
}),
|
||||
];
|
||||
self.secondary = Some(SecondaryEditor {
|
||||
cx.emit(event.clone());
|
||||
}
|
||||
_ => cx.emit(event.clone()),
|
||||
},
|
||||
)];
|
||||
let mut secondary = SecondaryEditor {
|
||||
editor: secondary_editor,
|
||||
multibuffer: secondary_multibuffer,
|
||||
pane: secondary_pane.clone(),
|
||||
has_latest_selection: false,
|
||||
primary_to_secondary: HashMap::default(),
|
||||
secondary_to_primary: HashMap::default(),
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
self.primary_editor.update(cx, |editor, cx| {
|
||||
editor.set_delegate_expand_excerpts(true);
|
||||
editor.buffer().update(cx, |primary_multibuffer, cx| {
|
||||
primary_multibuffer.set_show_deleted_hunks(false, cx);
|
||||
let paths = primary_multibuffer.paths().cloned().collect::<Vec<_>>();
|
||||
for path in paths {
|
||||
let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path).next()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let snapshot = primary_multibuffer.snapshot(cx);
|
||||
let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
|
||||
let diff = primary_multibuffer.diff_for(buffer.remote_id()).unwrap();
|
||||
secondary.sync_path_excerpts(path.clone(), primary_multibuffer, diff, cx);
|
||||
}
|
||||
})
|
||||
});
|
||||
self.secondary = Some(secondary);
|
||||
|
||||
let primary_pane = self.panes.first_pane();
|
||||
self.panes
|
||||
.split(&primary_pane, &secondary_pane, SplitDirection::Left, cx)
|
||||
@@ -205,8 +267,9 @@ impl SplittableEditor {
|
||||
};
|
||||
self.panes.remove(&secondary.pane, cx).unwrap();
|
||||
self.primary_editor.update(cx, |primary, cx| {
|
||||
primary.buffer().update(cx, |buffer, _| {
|
||||
buffer.set_filter_mode(None);
|
||||
primary.set_delegate_expand_excerpts(false);
|
||||
primary.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_show_deleted_hunks(true, cx);
|
||||
});
|
||||
});
|
||||
cx.notify();
|
||||
@@ -228,6 +291,299 @@ impl SplittableEditor {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_excerpts_for_path(
|
||||
&mut self,
|
||||
path: PathKey,
|
||||
buffer: Entity<Buffer>,
|
||||
ranges: impl IntoIterator<Item = Range<Point>> + Clone,
|
||||
context_line_count: u32,
|
||||
diff: Entity<BufferDiff>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> (Vec<Range<Anchor>>, bool) {
|
||||
self.primary_multibuffer
|
||||
.update(cx, |primary_multibuffer, cx| {
|
||||
let (anchors, added_a_new_excerpt) = primary_multibuffer.set_excerpts_for_path(
|
||||
path.clone(),
|
||||
buffer,
|
||||
ranges,
|
||||
context_line_count,
|
||||
cx,
|
||||
);
|
||||
primary_multibuffer.add_diff(diff.clone(), cx);
|
||||
if let Some(secondary) = &mut self.secondary {
|
||||
secondary.sync_path_excerpts(path, primary_multibuffer, diff, cx);
|
||||
}
|
||||
(anchors, added_a_new_excerpt)
|
||||
})
|
||||
}
|
||||
|
||||
fn expand_excerpts(
|
||||
&mut self,
|
||||
excerpt_ids: impl Iterator<Item = ExcerptId> + Clone,
|
||||
lines: u32,
|
||||
direction: ExpandExcerptDirection,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut corresponding_paths = HashMap::default();
|
||||
self.primary_multibuffer.update(cx, |multibuffer, cx| {
|
||||
let snapshot = multibuffer.snapshot(cx);
|
||||
if self.secondary.is_some() {
|
||||
corresponding_paths = excerpt_ids
|
||||
.clone()
|
||||
.map(|excerpt_id| {
|
||||
let path = multibuffer.path_for_excerpt(excerpt_id).unwrap();
|
||||
let buffer = snapshot.buffer_for_excerpt(excerpt_id).unwrap();
|
||||
let diff = multibuffer.diff_for(buffer.remote_id()).unwrap();
|
||||
(path, diff)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
}
|
||||
multibuffer.expand_excerpts(excerpt_ids.clone(), lines, direction, cx);
|
||||
});
|
||||
|
||||
if let Some(secondary) = &mut self.secondary {
|
||||
self.primary_multibuffer.update(cx, |multibuffer, cx| {
|
||||
for (path, diff) in corresponding_paths {
|
||||
secondary.sync_path_excerpts(path, multibuffer, diff, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context<Self>) {
|
||||
self.primary_multibuffer.update(cx, |buffer, cx| {
|
||||
buffer.remove_excerpts_for_path(path.clone(), cx)
|
||||
});
|
||||
if let Some(secondary) = &mut self.secondary {
|
||||
secondary.remove_mappings_for_path(&path, cx);
|
||||
secondary
|
||||
.multibuffer
|
||||
.update(cx, |buffer, cx| buffer.remove_excerpts_for_path(path, cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SplittableEditor {
|
||||
fn check_invariants(&self, quiesced: bool, cx: &App) {
|
||||
use buffer_diff::DiffHunkStatusKind;
|
||||
use collections::HashSet;
|
||||
use multi_buffer::MultiBufferOffset;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use multi_buffer::MultiBufferSnapshot;
|
||||
|
||||
fn format_diff(snapshot: &MultiBufferSnapshot) -> String {
|
||||
let text = snapshot.text();
|
||||
let row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::<Vec<_>>();
|
||||
let boundary_rows = snapshot
|
||||
.excerpt_boundaries_in_range(MultiBufferOffset(0)..)
|
||||
.map(|b| b.row)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
text.split('\n')
|
||||
.enumerate()
|
||||
.zip(row_infos)
|
||||
.map(|((ix, line), info)| {
|
||||
let marker = match info.diff_status.map(|status| status.kind) {
|
||||
Some(DiffHunkStatusKind::Added) => "+ ",
|
||||
Some(DiffHunkStatusKind::Deleted) => "- ",
|
||||
Some(DiffHunkStatusKind::Modified) => unreachable!(),
|
||||
None => {
|
||||
if !line.is_empty() {
|
||||
" "
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
};
|
||||
let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) {
|
||||
" ----------\n"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let expand = info
|
||||
.expand_info
|
||||
.map(|expand_info| match expand_info.direction {
|
||||
ExpandExcerptDirection::Up => " [↑]",
|
||||
ExpandExcerptDirection::Down => " [↓]",
|
||||
ExpandExcerptDirection::UpAndDown => " [↕]",
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
format!("{boundary_row}{marker}{line}{expand}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
let Some(secondary) = &self.secondary else {
|
||||
return;
|
||||
};
|
||||
|
||||
log::info!(
|
||||
"primary:\n\n{}",
|
||||
format_diff(&self.primary_multibuffer.read(cx).snapshot(cx))
|
||||
);
|
||||
|
||||
log::info!(
|
||||
"secondary:\n\n{}",
|
||||
format_diff(&secondary.multibuffer.read(cx).snapshot(cx))
|
||||
);
|
||||
|
||||
let primary_excerpts = self.primary_multibuffer.read(cx).excerpt_ids();
|
||||
let secondary_excerpts = secondary.multibuffer.read(cx).excerpt_ids();
|
||||
assert_eq!(primary_excerpts.len(), secondary_excerpts.len());
|
||||
|
||||
assert_eq!(
|
||||
secondary.primary_to_secondary.len(),
|
||||
primary_excerpts.len(),
|
||||
"primary_to_secondary mapping count should match excerpt count"
|
||||
);
|
||||
assert_eq!(
|
||||
secondary.secondary_to_primary.len(),
|
||||
secondary_excerpts.len(),
|
||||
"secondary_to_primary mapping count should match excerpt count"
|
||||
);
|
||||
|
||||
for primary_id in &primary_excerpts {
|
||||
assert!(
|
||||
secondary.primary_to_secondary.contains_key(primary_id),
|
||||
"primary excerpt {:?} should have a mapping to secondary",
|
||||
primary_id
|
||||
);
|
||||
}
|
||||
for secondary_id in &secondary_excerpts {
|
||||
assert!(
|
||||
secondary.secondary_to_primary.contains_key(secondary_id),
|
||||
"secondary excerpt {:?} should have a mapping to primary",
|
||||
secondary_id
|
||||
);
|
||||
}
|
||||
|
||||
for (primary_id, secondary_id) in &secondary.primary_to_secondary {
|
||||
assert_eq!(
|
||||
secondary.secondary_to_primary.get(secondary_id),
|
||||
Some(primary_id),
|
||||
"mappings should be bijective"
|
||||
);
|
||||
}
|
||||
|
||||
if quiesced {
|
||||
let primary_snapshot = self.primary_multibuffer.read(cx).snapshot(cx);
|
||||
let secondary_snapshot = secondary.multibuffer.read(cx).snapshot(cx);
|
||||
let primary_diff_hunks = primary_snapshot
|
||||
.diff_hunks()
|
||||
.map(|hunk| hunk.diff_base_byte_range)
|
||||
.collect::<Vec<_>>();
|
||||
let secondary_diff_hunks = secondary_snapshot
|
||||
.diff_hunks()
|
||||
.map(|hunk| hunk.diff_base_byte_range)
|
||||
.collect::<Vec<_>>();
|
||||
pretty_assertions::assert_eq!(primary_diff_hunks, secondary_diff_hunks);
|
||||
|
||||
// Filtering out empty lines is a bit of a hack, to work around a case where
|
||||
// the base text has a trailing newline but the current text doesn't, or vice versa.
|
||||
// In this case, we get the additional newline on one side, but that line is not
|
||||
// marked as added/deleted by rowinfos.
|
||||
let primary_unmodified_rows = primary_snapshot
|
||||
.text()
|
||||
.split("\n")
|
||||
.zip(primary_snapshot.row_infos(MultiBufferRow(0)))
|
||||
.filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
|
||||
.map(|(line, _)| line.to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
let secondary_unmodified_rows = secondary_snapshot
|
||||
.text()
|
||||
.split("\n")
|
||||
.zip(secondary_snapshot.row_infos(MultiBufferRow(0)))
|
||||
.filter(|(line, row_info)| !line.is_empty() && row_info.diff_status.is_none())
|
||||
.map(|(line, _)| line.to_owned())
|
||||
.collect::<Vec<_>>();
|
||||
pretty_assertions::assert_eq!(primary_unmodified_rows, secondary_unmodified_rows);
|
||||
}
|
||||
}
|
||||
|
||||
fn randomly_edit_excerpts(
|
||||
&mut self,
|
||||
rng: &mut impl rand::Rng,
|
||||
mutation_count: usize,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
use collections::HashSet;
|
||||
use rand::prelude::*;
|
||||
use std::env;
|
||||
use util::RandomCharIter;
|
||||
|
||||
let max_excerpts = env::var("MAX_EXCERPTS")
|
||||
.map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable"))
|
||||
.unwrap_or(5);
|
||||
|
||||
for _ in 0..mutation_count {
|
||||
let paths = self
|
||||
.primary_multibuffer
|
||||
.read(cx)
|
||||
.paths()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let excerpt_ids = self.primary_multibuffer.read(cx).excerpt_ids();
|
||||
|
||||
if rng.random_bool(0.1) && !excerpt_ids.is_empty() {
|
||||
let mut excerpts = HashSet::default();
|
||||
for _ in 0..rng.random_range(0..excerpt_ids.len()) {
|
||||
excerpts.extend(excerpt_ids.choose(rng).copied());
|
||||
}
|
||||
|
||||
let line_count = rng.random_range(0..5);
|
||||
|
||||
log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
|
||||
|
||||
self.expand_excerpts(
|
||||
excerpts.iter().cloned(),
|
||||
line_count,
|
||||
ExpandExcerptDirection::UpAndDown,
|
||||
cx,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
|
||||
let len = rng.random_range(100..500);
|
||||
let text = RandomCharIter::new(&mut *rng).take(len).collect::<String>();
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||||
log::info!(
|
||||
"Creating new buffer {} with text: {:?}",
|
||||
buffer.read(cx).remote_id(),
|
||||
buffer.read(cx).text()
|
||||
);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_snapshot, cx));
|
||||
// Create some initial diff hunks.
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.randomly_edit(rng, 1, cx);
|
||||
});
|
||||
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
||||
let ranges = diff.update(cx, |diff, cx| {
|
||||
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
||||
diff.snapshot(cx)
|
||||
.hunks(&buffer_snapshot)
|
||||
.map(|hunk| hunk.buffer_range.to_point(&buffer_snapshot))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
self.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
|
||||
} else {
|
||||
let remove_count = rng.random_range(1..=paths.len());
|
||||
let paths_to_remove = paths
|
||||
.choose_multiple(rng, remove_count)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for path in paths_to_remove {
|
||||
self.remove_excerpts_for_path(path.clone(), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for SplittableEditor {}
|
||||
@@ -265,3 +621,223 @@ impl Render for SplittableEditor {
|
||||
.child(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl SecondaryEditor {
|
||||
fn sync_path_excerpts(
|
||||
&mut self,
|
||||
path_key: PathKey,
|
||||
primary_multibuffer: &mut MultiBuffer,
|
||||
diff: Entity<BufferDiff>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(excerpt_id) = primary_multibuffer.excerpts_for_path(&path_key).next() else {
|
||||
self.remove_mappings_for_path(&path_key, cx);
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.remove_excerpts_for_path(path_key, cx);
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
let primary_excerpt_ids: Vec<ExcerptId> =
|
||||
primary_multibuffer.excerpts_for_path(&path_key).collect();
|
||||
|
||||
let primary_multibuffer_snapshot = primary_multibuffer.snapshot(cx);
|
||||
let main_buffer = primary_multibuffer_snapshot
|
||||
.buffer_for_excerpt(excerpt_id)
|
||||
.unwrap();
|
||||
let base_text_buffer = diff.read(cx).base_text_buffer();
|
||||
let diff_snapshot = diff.read(cx).snapshot(cx);
|
||||
let base_text_buffer_snapshot = base_text_buffer.read(cx).snapshot();
|
||||
let new = primary_multibuffer
|
||||
.excerpts_for_buffer(main_buffer.remote_id(), cx)
|
||||
.into_iter()
|
||||
.map(|(_, excerpt_range)| {
|
||||
let point_range_to_base_text_point_range = |range: Range<Point>| {
|
||||
let start_row = diff_snapshot.row_to_base_text_row(
|
||||
range.start.row,
|
||||
Bias::Left,
|
||||
main_buffer,
|
||||
);
|
||||
let end_row =
|
||||
diff_snapshot.row_to_base_text_row(range.end.row, Bias::Right, main_buffer);
|
||||
let end_column = diff_snapshot.base_text().line_len(end_row);
|
||||
Point::new(start_row, 0)..Point::new(end_row, end_column)
|
||||
};
|
||||
let primary = excerpt_range.primary.to_point(main_buffer);
|
||||
let context = excerpt_range.context.to_point(main_buffer);
|
||||
ExcerptRange {
|
||||
primary: point_range_to_base_text_point_range(primary),
|
||||
context: point_range_to_base_text_point_range(context),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let main_buffer = primary_multibuffer.buffer(main_buffer.remote_id()).unwrap();
|
||||
|
||||
self.remove_mappings_for_path(&path_key, cx);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
buffer.update_path_excerpts(
|
||||
path_key.clone(),
|
||||
base_text_buffer,
|
||||
&base_text_buffer_snapshot,
|
||||
new,
|
||||
cx,
|
||||
);
|
||||
buffer.add_inverted_diff(diff, main_buffer, cx);
|
||||
})
|
||||
});
|
||||
|
||||
let secondary_excerpt_ids: Vec<ExcerptId> = self
|
||||
.multibuffer
|
||||
.read(cx)
|
||||
.excerpts_for_path(&path_key)
|
||||
.collect();
|
||||
|
||||
for (primary_id, secondary_id) in primary_excerpt_ids.into_iter().zip(secondary_excerpt_ids)
|
||||
{
|
||||
self.primary_to_secondary.insert(primary_id, secondary_id);
|
||||
self.secondary_to_primary.insert(secondary_id, primary_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_mappings_for_path(&mut self, path_key: &PathKey, cx: &App) {
|
||||
let secondary_excerpt_ids: Vec<ExcerptId> = self
|
||||
.multibuffer
|
||||
.read(cx)
|
||||
.excerpts_for_path(path_key)
|
||||
.collect();
|
||||
|
||||
for secondary_id in secondary_excerpt_ids {
|
||||
if let Some(primary_id) = self.secondary_to_primary.remove(&secondary_id) {
|
||||
self.primary_to_secondary.remove(&primary_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::FakeFs;
|
||||
use gpui::AppContext as _;
|
||||
use language::Capability;
|
||||
use multi_buffer::{MultiBuffer, PathKey};
|
||||
use project::Project;
|
||||
use rand::rngs::StdRng;
|
||||
use settings::SettingsStore;
|
||||
use ui::VisualContext as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::SplittableEditor;
|
||||
|
||||
fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
crate::init(cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_split_editor(mut rng: StdRng, cx: &mut gpui::TestAppContext) {
|
||||
use rand::prelude::*;
|
||||
|
||||
init_test(cx);
|
||||
let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let primary_multibuffer = cx.new(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
|
||||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||||
multibuffer
|
||||
});
|
||||
let editor = cx.new_window_entity(|window, cx| {
|
||||
let mut editor =
|
||||
SplittableEditor::new_unsplit(primary_multibuffer, project, workspace, window, cx);
|
||||
editor.split(&Default::default(), window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let operations = std::env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(20);
|
||||
let rng = &mut rng;
|
||||
for _ in 0..operations {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let buffers = editor
|
||||
.primary_editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.all_buffers();
|
||||
|
||||
if buffers.is_empty() {
|
||||
editor.randomly_edit_excerpts(rng, 2, cx);
|
||||
editor.check_invariants(true, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let quiesced = match rng.random_range(0..100) {
|
||||
0..=69 if !buffers.is_empty() => {
|
||||
let buffer = buffers.iter().choose(rng).unwrap();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
if rng.random() {
|
||||
log::info!("randomly editing single buffer");
|
||||
buffer.randomly_edit(rng, 5, cx);
|
||||
} else {
|
||||
log::info!("randomly undoing/redoing in single buffer");
|
||||
buffer.randomly_undo_redo(rng, cx);
|
||||
}
|
||||
});
|
||||
false
|
||||
}
|
||||
70..=79 => {
|
||||
log::info!("mutating excerpts");
|
||||
editor.randomly_edit_excerpts(rng, 2, cx);
|
||||
false
|
||||
}
|
||||
80..=89 if !buffers.is_empty() => {
|
||||
log::info!("recalculating buffer diff");
|
||||
let buffer = buffers.iter().choose(rng).unwrap();
|
||||
let diff = editor
|
||||
.primary_multibuffer
|
||||
.read(cx)
|
||||
.diff_for(buffer.read(cx).remote_id())
|
||||
.unwrap();
|
||||
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
||||
});
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
log::info!("quiescing");
|
||||
for buffer in buffers {
|
||||
let buffer_snapshot = buffer.read(cx).text_snapshot();
|
||||
let diff = editor
|
||||
.primary_multibuffer
|
||||
.read(cx)
|
||||
.diff_for(buffer.read(cx).remote_id())
|
||||
.unwrap();
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.recalculate_diff_sync(&buffer_snapshot, cx);
|
||||
});
|
||||
let diff_snapshot = diff.read(cx).snapshot(cx);
|
||||
let ranges = diff_snapshot
|
||||
.hunks(&buffer_snapshot)
|
||||
.map(|hunk| hunk.range)
|
||||
.collect::<Vec<_>>();
|
||||
let path = PathKey::for_buffer(&buffer, cx);
|
||||
editor.set_excerpts_for_path(path, buffer, ranges, 2, diff, cx);
|
||||
}
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
editor.check_invariants(quiesced, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,11 +365,12 @@ impl ExampleContext {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let file = snapshot.file().unwrap();
|
||||
let diff = diff.read(cx);
|
||||
let base_text = diff.base_text().text();
|
||||
let base_text = diff.read(cx).base_text(cx).text();
|
||||
|
||||
let hunks = diff
|
||||
.hunks(&snapshot, cx)
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.hunks(&snapshot)
|
||||
.map(|hunk| FileEditHunk {
|
||||
base_text: base_text[hunk.diff_base_byte_range.clone()].to_string(),
|
||||
text: snapshot
|
||||
|
||||
@@ -389,49 +389,6 @@ pub trait ExtensionContextServerProxy: Send + Sync + 'static {
|
||||
fn unregister_context_server(&self, server_id: Arc<str>, cx: &mut App);
|
||||
}
|
||||
|
||||
/// A function that registers a language model provider with the registry.
|
||||
/// This allows extension_host to create the provider (which requires WasmExtension)
|
||||
/// and pass a registration closure to the language_models crate.
|
||||
pub type LanguageModelProviderRegistration = Box<dyn FnOnce(&mut App) + Send + Sync + 'static>;
|
||||
|
||||
pub trait ExtensionLanguageModelProviderProxy: Send + Sync + 'static {
|
||||
/// Register an LLM provider from an extension.
|
||||
/// The `register_fn` closure will be called with the App context and should
|
||||
/// register the provider with the LanguageModelRegistry.
|
||||
fn register_language_model_provider(
|
||||
&self,
|
||||
provider_id: Arc<str>,
|
||||
register_fn: LanguageModelProviderRegistration,
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
/// Unregister an LLM provider when an extension is unloaded.
|
||||
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App);
|
||||
}
|
||||
|
||||
impl ExtensionLanguageModelProviderProxy for ExtensionHostProxy {
|
||||
fn register_language_model_provider(
|
||||
&self,
|
||||
provider_id: Arc<str>,
|
||||
register_fn: LanguageModelProviderRegistration,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let Some(proxy) = self.language_model_provider_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.register_language_model_provider(provider_id, register_fn, cx)
|
||||
}
|
||||
|
||||
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App) {
|
||||
let Some(proxy) = self.language_model_provider_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.unregister_language_model_provider(provider_id, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionContextServerProxy for ExtensionHostProxy {
|
||||
fn register_context_server(
|
||||
&self,
|
||||
|
||||
@@ -298,58 +298,6 @@ pub struct LanguageModelProviderManifestEntry {
|
||||
/// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg").
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
/// Hardcoded models to always show (as opposed to a model list loaded over the network).
|
||||
#[serde(default)]
|
||||
pub models: Vec<LanguageModelManifestEntry>,
|
||||
/// Authentication configuration.
|
||||
#[serde(default)]
|
||||
pub auth: Option<LanguageModelAuthConfig>,
|
||||
}
|
||||
|
||||
/// Manifest entry for a language model.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct LanguageModelManifestEntry {
|
||||
/// Unique identifier for the model.
|
||||
pub id: String,
|
||||
/// Display name for the model.
|
||||
pub name: String,
|
||||
/// Maximum input token count.
|
||||
pub max_token_count: u64,
|
||||
/// Maximum output tokens (optional).
|
||||
pub max_output_tokens: Option<u64>,
|
||||
/// Whether the model supports image inputs.
|
||||
pub supports_images: bool,
|
||||
/// Whether the model supports tool/function calling.
|
||||
pub supports_tools: bool,
|
||||
/// Whether the model supports extended thinking/reasoning.
|
||||
pub supports_thinking: bool,
|
||||
}
|
||||
|
||||
/// Authentication configuration for a language model provider.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct LanguageModelAuthConfig {
|
||||
/// Human-readable name for the credential shown in the UI input field (e.g. "API Key", "Access Token").
|
||||
pub credential_label: Option<String>,
|
||||
/// Environment variable names for the API key (if env var auth supported).
|
||||
/// Multiple env vars can be specified; they will be checked in order.
|
||||
#[serde(default)]
|
||||
pub env_vars: Option<Vec<String>>,
|
||||
/// OAuth configuration for web-based authentication flows.
|
||||
#[serde(default)]
|
||||
pub oauth: Option<OAuthConfig>,
|
||||
}
|
||||
|
||||
/// OAuth configuration for web-based authentication.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct OAuthConfig {
|
||||
/// The text to display on the sign-in button (e.g. "Sign in with GitHub").
|
||||
pub sign_in_button_label: Option<String>,
|
||||
/// The Zed icon path to display on the sign-in button (e.g. "github").
|
||||
#[serde(default)]
|
||||
pub sign_in_button_icon: Option<String>,
|
||||
/// The description text shown next to the sign-in button in edit prediction settings.
|
||||
#[serde(default)]
|
||||
pub sign_in_description: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
|
||||
@@ -29,26 +29,6 @@ pub use wit::{
|
||||
GithubRelease, GithubReleaseAsset, GithubReleaseOptions, github_release_by_tag_name,
|
||||
latest_github_release,
|
||||
},
|
||||
zed::extension::llm_provider::{
|
||||
CacheConfiguration as LlmCacheConfiguration, CompletionEvent as LlmCompletionEvent,
|
||||
CompletionRequest as LlmCompletionRequest, CustomModelConfig as LlmCustomModelConfig,
|
||||
DeviceFlowPromptInfo as LlmDeviceFlowPromptInfo, ImageData as LlmImageData,
|
||||
MessageContent as LlmMessageContent, MessageRole as LlmMessageRole,
|
||||
ModelCapabilities as LlmModelCapabilities, ModelInfo as LlmModelInfo,
|
||||
OauthWebAuthConfig as LlmOauthWebAuthConfig, OauthWebAuthResult as LlmOauthWebAuthResult,
|
||||
ProviderInfo as LlmProviderInfo, ProviderSettings as LlmProviderSettings,
|
||||
RequestMessage as LlmRequestMessage, StopReason as LlmStopReason,
|
||||
ThinkingContent as LlmThinkingContent, TokenUsage as LlmTokenUsage,
|
||||
ToolChoice as LlmToolChoice, ToolDefinition as LlmToolDefinition,
|
||||
ToolInputFormat as LlmToolInputFormat, ToolResult as LlmToolResult,
|
||||
ToolResultContent as LlmToolResultContent, ToolUse as LlmToolUse,
|
||||
ToolUseJsonParseError as LlmToolUseJsonParseError,
|
||||
delete_credential as llm_delete_credential, get_credential as llm_get_credential,
|
||||
get_env_var as llm_get_env_var, get_provider_settings as llm_get_provider_settings,
|
||||
oauth_open_browser as llm_oauth_open_browser,
|
||||
oauth_send_http_request as llm_oauth_send_http_request,
|
||||
oauth_start_web_auth as llm_oauth_start_web_auth, store_credential as llm_store_credential,
|
||||
},
|
||||
zed::extension::nodejs::{
|
||||
node_binary_path, npm_install_package, npm_package_installed_version,
|
||||
npm_package_latest_version,
|
||||
@@ -279,93 +259,6 @@ pub trait Extension: Send + Sync {
|
||||
) -> Result<DebugRequest, String> {
|
||||
Err("`run_dap_locator` not implemented".to_string())
|
||||
}
|
||||
|
||||
/// Returns information about language model providers offered by this extension.
|
||||
fn llm_providers(&self) -> Vec<LlmProviderInfo> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Returns the models available for a provider.
|
||||
fn llm_provider_models(&self, _provider_id: &str) -> Result<Vec<LlmModelInfo>, String> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// Returns markdown content to display in the provider's settings UI.
|
||||
/// This can include setup instructions, links to documentation, etc.
|
||||
fn llm_provider_settings_markdown(&self, _provider_id: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if the provider is authenticated.
|
||||
fn llm_provider_is_authenticated(&self, _provider_id: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Start an OAuth device flow sign-in.
|
||||
/// This is called when the user explicitly clicks "Sign in with GitHub" or similar.
|
||||
/// Returns information needed to display the device flow prompt modal to the user.
|
||||
fn llm_provider_start_device_flow_sign_in(
|
||||
&mut self,
|
||||
_provider_id: &str,
|
||||
) -> Result<LlmDeviceFlowPromptInfo, String> {
|
||||
Err("`llm_provider_start_device_flow_sign_in` not implemented".to_string())
|
||||
}
|
||||
|
||||
/// Poll for device flow sign-in completion.
|
||||
/// This is called after llm_provider_start_device_flow_sign_in returns the user code.
|
||||
/// The extension should poll the OAuth provider until the user authorizes or the flow times out.
|
||||
fn llm_provider_poll_device_flow_sign_in(&mut self, _provider_id: &str) -> Result<(), String> {
|
||||
Err("`llm_provider_poll_device_flow_sign_in` not implemented".to_string())
|
||||
}
|
||||
|
||||
/// Reset credentials for the provider.
|
||||
fn llm_provider_reset_credentials(&mut self, _provider_id: &str) -> Result<(), String> {
|
||||
Err("`llm_provider_reset_credentials` not implemented".to_string())
|
||||
}
|
||||
|
||||
/// Count tokens for a request.
|
||||
fn llm_count_tokens(
|
||||
&self,
|
||||
_provider_id: &str,
|
||||
_model_id: &str,
|
||||
_request: &LlmCompletionRequest,
|
||||
) -> Result<u64, String> {
|
||||
Err("`llm_count_tokens` not implemented".to_string())
|
||||
}
|
||||
|
||||
/// Start streaming a completion from the model.
|
||||
/// Returns a stream ID that can be used with `llm_stream_completion_next` and `llm_stream_completion_close`.
|
||||
fn llm_stream_completion_start(
|
||||
&mut self,
|
||||
_provider_id: &str,
|
||||
_model_id: &str,
|
||||
_request: &LlmCompletionRequest,
|
||||
) -> Result<String, String> {
|
||||
Err("`llm_stream_completion_start` not implemented".to_string())
|
||||
}
|
||||
|
||||
/// Get the next event from a completion stream.
|
||||
/// Returns `Ok(None)` when the stream is complete.
|
||||
fn llm_stream_completion_next(
|
||||
&mut self,
|
||||
_stream_id: &str,
|
||||
) -> Result<Option<LlmCompletionEvent>, String> {
|
||||
Err("`llm_stream_completion_next` not implemented".to_string())
|
||||
}
|
||||
|
||||
/// Close a completion stream and release its resources.
|
||||
fn llm_stream_completion_close(&mut self, _stream_id: &str) {
|
||||
// Default implementation does nothing
|
||||
}
|
||||
|
||||
/// Get cache configuration for a model (if prompt caching is supported).
|
||||
fn llm_cache_configuration(
|
||||
&self,
|
||||
_provider_id: &str,
|
||||
_model_id: &str,
|
||||
) -> Option<LlmCacheConfiguration> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the provided type as a Zed extension.
|
||||
@@ -624,67 +517,6 @@ impl wit::Guest for Component {
|
||||
) -> Result<DebugRequest, String> {
|
||||
extension().run_dap_locator(locator_name, build_task)
|
||||
}
|
||||
|
||||
fn llm_providers() -> Vec<LlmProviderInfo> {
|
||||
extension().llm_providers()
|
||||
}
|
||||
|
||||
fn llm_provider_models(provider_id: String) -> Result<Vec<LlmModelInfo>, String> {
|
||||
extension().llm_provider_models(&provider_id)
|
||||
}
|
||||
|
||||
fn llm_provider_settings_markdown(provider_id: String) -> Option<String> {
|
||||
extension().llm_provider_settings_markdown(&provider_id)
|
||||
}
|
||||
|
||||
fn llm_provider_is_authenticated(provider_id: String) -> bool {
|
||||
extension().llm_provider_is_authenticated(&provider_id)
|
||||
}
|
||||
|
||||
fn llm_provider_start_device_flow_sign_in(
|
||||
provider_id: String,
|
||||
) -> Result<LlmDeviceFlowPromptInfo, String> {
|
||||
extension().llm_provider_start_device_flow_sign_in(&provider_id)
|
||||
}
|
||||
|
||||
fn llm_provider_poll_device_flow_sign_in(provider_id: String) -> Result<(), String> {
|
||||
extension().llm_provider_poll_device_flow_sign_in(&provider_id)
|
||||
}
|
||||
|
||||
fn llm_provider_reset_credentials(provider_id: String) -> Result<(), String> {
|
||||
extension().llm_provider_reset_credentials(&provider_id)
|
||||
}
|
||||
|
||||
fn llm_count_tokens(
|
||||
provider_id: String,
|
||||
model_id: String,
|
||||
request: LlmCompletionRequest,
|
||||
) -> Result<u64, String> {
|
||||
extension().llm_count_tokens(&provider_id, &model_id, &request)
|
||||
}
|
||||
|
||||
fn llm_stream_completion_start(
|
||||
provider_id: String,
|
||||
model_id: String,
|
||||
request: LlmCompletionRequest,
|
||||
) -> Result<String, String> {
|
||||
extension().llm_stream_completion_start(&provider_id, &model_id, &request)
|
||||
}
|
||||
|
||||
fn llm_stream_completion_next(stream_id: String) -> Result<Option<LlmCompletionEvent>, String> {
|
||||
extension().llm_stream_completion_next(&stream_id)
|
||||
}
|
||||
|
||||
fn llm_stream_completion_close(stream_id: String) {
|
||||
extension().llm_stream_completion_close(&stream_id)
|
||||
}
|
||||
|
||||
fn llm_cache_configuration(
|
||||
provider_id: String,
|
||||
model_id: String,
|
||||
) -> Option<LlmCacheConfiguration> {
|
||||
extension().llm_cache_configuration(&provider_id, &model_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// The ID of a language server.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! An HTTP client.
|
||||
|
||||
pub use crate::wit::zed::extension::http_client::{
|
||||
HttpMethod, HttpRequest, HttpResponse, HttpResponseStream, HttpResponseWithStatus,
|
||||
RedirectPolicy, fetch, fetch_fallible, fetch_stream,
|
||||
HttpMethod, HttpRequest, HttpResponse, HttpResponseStream, RedirectPolicy, fetch, fetch_stream,
|
||||
};
|
||||
|
||||
impl HttpRequest {
|
||||
@@ -16,11 +15,6 @@ impl HttpRequest {
|
||||
fetch(self)
|
||||
}
|
||||
|
||||
/// Like [`fetch`], except it doesn't treat any status codes as errors.
|
||||
pub fn fetch_fallible(&self) -> Result<HttpResponseWithStatus, String> {
|
||||
fetch_fallible(self)
|
||||
}
|
||||
|
||||
/// Executes the [`HttpRequest`] with [`fetch_stream`].
|
||||
pub fn fetch_stream(&self) -> Result<HttpResponseStream, String> {
|
||||
fetch_stream(self)
|
||||
|
||||
@@ -8,7 +8,6 @@ world extension {
|
||||
import platform;
|
||||
import process;
|
||||
import nodejs;
|
||||
import llm-provider;
|
||||
|
||||
use common.{env-vars, range};
|
||||
use context-server.{context-server-configuration};
|
||||
@@ -16,11 +15,6 @@ world extension {
|
||||
use lsp.{completion, symbol};
|
||||
use process.{command};
|
||||
use slash-command.{slash-command, slash-command-argument-completion, slash-command-output};
|
||||
use llm-provider.{
|
||||
provider-info, model-info, completion-request,
|
||||
cache-configuration, completion-event, token-usage,
|
||||
device-flow-prompt-info
|
||||
};
|
||||
|
||||
/// Initializes the extension.
|
||||
export init-extension: func();
|
||||
@@ -170,73 +164,4 @@ world extension {
|
||||
export dap-config-to-scenario: func(config: debug-config) -> result<debug-scenario, string>;
|
||||
export dap-locator-create-scenario: func(locator-name: string, build-config-template: build-task-template, resolved-label: string, debug-adapter-name: string) -> option<debug-scenario>;
|
||||
export run-dap-locator: func(locator-name: string, config: resolved-task) -> result<debug-request, string>;
|
||||
|
||||
/// Returns information about language model providers offered by this extension.
|
||||
export llm-providers: func() -> list<provider-info>;
|
||||
|
||||
/// Returns the models available for a provider.
|
||||
export llm-provider-models: func(provider-id: string) -> result<list<model-info>, string>;
|
||||
|
||||
/// Returns markdown content to display in the provider's settings UI.
|
||||
/// This can include setup instructions, links to documentation, etc.
|
||||
export llm-provider-settings-markdown: func(provider-id: string) -> option<string>;
|
||||
|
||||
/// Check if the provider is authenticated.
|
||||
export llm-provider-is-authenticated: func(provider-id: string) -> bool;
|
||||
|
||||
/// Start an OAuth device flow sign-in.
|
||||
/// This is called when the user explicitly clicks "Sign in with GitHub" or similar.
|
||||
///
|
||||
/// The device flow works as follows:
|
||||
/// 1. Extension requests a device code from the OAuth provider
|
||||
/// 2. Extension returns prompt info including user code and verification URL
|
||||
/// 3. Host displays a modal with the prompt info
|
||||
/// 4. Host calls llm-provider-poll-device-flow-sign-in
|
||||
/// 5. Extension polls for the access token while user authorizes in browser
|
||||
/// 6. Once authorized, extension stores the credential and returns success
|
||||
///
|
||||
/// Returns information needed to display the device flow prompt modal.
|
||||
export llm-provider-start-device-flow-sign-in: func(provider-id: string) -> result<device-flow-prompt-info, string>;
|
||||
|
||||
/// Poll for device flow sign-in completion.
|
||||
/// This is called after llm-provider-start-device-flow-sign-in returns the user code.
|
||||
/// The extension should poll the OAuth provider until the user authorizes or the flow times out.
|
||||
/// Returns Ok(()) on successful authentication, or an error message on failure.
|
||||
export llm-provider-poll-device-flow-sign-in: func(provider-id: string) -> result<_, string>;
|
||||
|
||||
/// Reset credentials for the provider.
|
||||
export llm-provider-reset-credentials: func(provider-id: string) -> result<_, string>;
|
||||
|
||||
/// Count tokens for a request.
|
||||
export llm-count-tokens: func(
|
||||
provider-id: string,
|
||||
model-id: string,
|
||||
request: completion-request
|
||||
) -> result<u64, string>;
|
||||
|
||||
/// Start streaming a completion from the model.
|
||||
/// Returns a stream ID that can be used with llm-stream-next and llm-stream-close.
|
||||
export llm-stream-completion-start: func(
|
||||
provider-id: string,
|
||||
model-id: string,
|
||||
request: completion-request
|
||||
) -> result<string, string>;
|
||||
|
||||
/// Get the next event from a completion stream.
|
||||
/// Returns None when the stream is complete.
|
||||
export llm-stream-completion-next: func(
|
||||
stream-id: string
|
||||
) -> result<option<completion-event>, string>;
|
||||
|
||||
/// Close a completion stream and release its resources.
|
||||
export llm-stream-completion-close: func(
|
||||
stream-id: string
|
||||
);
|
||||
|
||||
/// Get cache configuration for a model (if prompt caching is supported).
|
||||
export llm-cache-configuration: func(
|
||||
provider-id: string,
|
||||
model-id: string
|
||||
) -> option<cache-configuration>;
|
||||
|
||||
}
|
||||
|
||||
@@ -51,26 +51,9 @@ interface http-client {
|
||||
body: list<u8>,
|
||||
}
|
||||
|
||||
/// An HTTP response that includes the status code.
|
||||
///
|
||||
/// Used by `fetch-fallible` which returns responses for all status codes
|
||||
/// rather than treating some status codes as errors.
|
||||
record http-response-with-status {
|
||||
/// The HTTP status code.
|
||||
status: u16,
|
||||
/// The response headers.
|
||||
headers: list<tuple<string, string>>,
|
||||
/// The response body.
|
||||
body: list<u8>,
|
||||
}
|
||||
|
||||
/// Performs an HTTP request and returns the response.
|
||||
/// Returns an error if the response status is 4xx or 5xx.
|
||||
fetch: func(req: http-request) -> result<http-response, string>;
|
||||
|
||||
/// Performs an HTTP request and returns the response regardless of its status code.
|
||||
fetch-fallible: func(req: http-request) -> result<http-response-with-status, string>;
|
||||
|
||||
/// An HTTP response stream.
|
||||
resource http-response-stream {
|
||||
/// Retrieves the next chunk of data from the response stream.
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
interface llm-provider {
|
||||
use http-client.{http-request, http-response-with-status};
|
||||
|
||||
/// Information about a language model provider.
|
||||
record provider-info {
|
||||
/// Unique identifier for the provider (e.g. "my-extension.my-provider").
|
||||
id: string,
|
||||
/// Display name for the provider.
|
||||
name: string,
|
||||
/// Path to an SVG icon file relative to the extension root (e.g. "icons/provider.svg").
|
||||
icon: option<string>,
|
||||
}
|
||||
|
||||
/// Capabilities of a language model.
|
||||
record model-capabilities {
|
||||
/// Whether the model supports image inputs.
|
||||
supports-images: bool,
|
||||
/// Whether the model supports tool/function calling.
|
||||
supports-tools: bool,
|
||||
/// Whether the model supports the "auto" tool choice.
|
||||
supports-tool-choice-auto: bool,
|
||||
/// Whether the model supports the "any" tool choice.
|
||||
supports-tool-choice-any: bool,
|
||||
/// Whether the model supports the "none" tool choice.
|
||||
supports-tool-choice-none: bool,
|
||||
/// Whether the model supports extended thinking/reasoning.
|
||||
supports-thinking: bool,
|
||||
/// The format for tool input schemas.
|
||||
tool-input-format: tool-input-format,
|
||||
}
|
||||
|
||||
/// Format for tool input schemas.
|
||||
enum tool-input-format {
|
||||
/// Standard JSON Schema format.
|
||||
json-schema,
|
||||
/// A subset of JSON Schema supported by Google AI.
|
||||
/// See https://ai.google.dev/api/caching#Schema
|
||||
json-schema-subset,
|
||||
/// Simplified schema format for certain providers.
|
||||
simplified,
|
||||
}
|
||||
|
||||
/// Information about a specific model.
|
||||
record model-info {
|
||||
/// Unique identifier for the model.
|
||||
id: string,
|
||||
/// Display name for the model.
|
||||
name: string,
|
||||
/// Maximum input token count.
|
||||
max-token-count: u64,
|
||||
/// Maximum output tokens (optional).
|
||||
max-output-tokens: option<u64>,
|
||||
/// Model capabilities.
|
||||
capabilities: model-capabilities,
|
||||
/// Whether this is the default model for the provider.
|
||||
is-default: bool,
|
||||
/// Whether this is the default fast model.
|
||||
is-default-fast: bool,
|
||||
}
|
||||
|
||||
/// The role of a message participant.
|
||||
enum message-role {
|
||||
/// User message.
|
||||
user,
|
||||
/// Assistant message.
|
||||
assistant,
|
||||
/// System message.
|
||||
system,
|
||||
}
|
||||
|
||||
/// A message in a completion request.
|
||||
record request-message {
|
||||
/// The role of the message sender.
|
||||
role: message-role,
|
||||
/// The content of the message.
|
||||
content: list<message-content>,
|
||||
/// Whether to cache this message for prompt caching.
|
||||
cache: bool,
|
||||
}
|
||||
|
||||
/// Content within a message.
|
||||
variant message-content {
|
||||
/// Plain text content.
|
||||
text(string),
|
||||
/// Image content.
|
||||
image(image-data),
|
||||
/// A tool use request from the assistant.
|
||||
tool-use(tool-use),
|
||||
/// A tool result from the user.
|
||||
tool-result(tool-result),
|
||||
/// Thinking/reasoning content.
|
||||
thinking(thinking-content),
|
||||
/// Redacted/encrypted thinking content.
|
||||
redacted-thinking(string),
|
||||
}
|
||||
|
||||
/// Image data for vision models.
|
||||
record image-data {
|
||||
/// Base64-encoded image data.
|
||||
source: string,
|
||||
/// Image width in pixels (optional).
|
||||
width: option<u32>,
|
||||
/// Image height in pixels (optional).
|
||||
height: option<u32>,
|
||||
}
|
||||
|
||||
/// A tool use request from the model.
|
||||
record tool-use {
|
||||
/// Unique identifier for this tool use.
|
||||
id: string,
|
||||
/// The name of the tool being used.
|
||||
name: string,
|
||||
/// JSON string of the tool input arguments.
|
||||
input: string,
|
||||
/// Whether the input JSON is complete (false while streaming, true when done).
|
||||
is-input-complete: bool,
|
||||
/// Thought signature for providers that support it (e.g., Anthropic).
|
||||
thought-signature: option<string>,
|
||||
}
|
||||
|
||||
/// A tool result to send back to the model.
|
||||
record tool-result {
|
||||
/// The ID of the tool use this is a result for.
|
||||
tool-use-id: string,
|
||||
/// The name of the tool.
|
||||
tool-name: string,
|
||||
/// Whether this result represents an error.
|
||||
is-error: bool,
|
||||
/// The content of the result.
|
||||
content: tool-result-content,
|
||||
}
|
||||
|
||||
/// Content of a tool result.
|
||||
variant tool-result-content {
|
||||
/// Text result.
|
||||
text(string),
|
||||
/// Image result.
|
||||
image(image-data),
|
||||
}
|
||||
|
||||
/// Thinking/reasoning content from models that support extended thinking.
|
||||
record thinking-content {
|
||||
/// The thinking text.
|
||||
text: string,
|
||||
/// Signature for the thinking block (provider-specific).
|
||||
signature: option<string>,
|
||||
}
|
||||
|
||||
/// A tool definition for function calling.
|
||||
record tool-definition {
|
||||
/// The name of the tool.
|
||||
name: string,
|
||||
/// Description of what the tool does.
|
||||
description: string,
|
||||
/// JSON Schema for input parameters.
|
||||
input-schema: string,
|
||||
}
|
||||
|
||||
/// Tool choice preference for the model.
|
||||
enum tool-choice {
|
||||
/// Let the model decide whether to use tools.
|
||||
auto,
|
||||
/// Force the model to use at least one tool.
|
||||
any,
|
||||
/// Prevent the model from using tools.
|
||||
none,
|
||||
}
|
||||
|
||||
/// A completion request to send to the model.
|
||||
record completion-request {
|
||||
/// The messages in the conversation.
|
||||
messages: list<request-message>,
|
||||
/// Available tools for the model to use.
|
||||
tools: list<tool-definition>,
|
||||
/// Tool choice preference.
|
||||
tool-choice: option<tool-choice>,
|
||||
/// Stop sequences to end generation.
|
||||
stop-sequences: list<string>,
|
||||
/// Temperature for sampling (0.0-1.0).
|
||||
temperature: option<f32>,
|
||||
/// Whether thinking/reasoning is allowed.
|
||||
thinking-allowed: bool,
|
||||
/// Maximum tokens to generate.
|
||||
max-tokens: option<u64>,
|
||||
}
|
||||
|
||||
/// Events emitted during completion streaming.
|
||||
variant completion-event {
|
||||
/// Completion has started.
|
||||
started,
|
||||
/// Text content chunk.
|
||||
text(string),
|
||||
/// Thinking/reasoning content chunk.
|
||||
thinking(thinking-content),
|
||||
/// Redacted thinking (encrypted) chunk.
|
||||
redacted-thinking(string),
|
||||
/// Tool use request from the model.
|
||||
tool-use(tool-use),
|
||||
/// JSON parse error when parsing tool input.
|
||||
tool-use-json-parse-error(tool-use-json-parse-error),
|
||||
/// Completion stopped.
|
||||
stop(stop-reason),
|
||||
/// Token usage update.
|
||||
usage(token-usage),
|
||||
/// Reasoning details (provider-specific JSON).
|
||||
reasoning-details(string),
|
||||
}
|
||||
|
||||
/// Error information when tool use JSON parsing fails.
|
||||
record tool-use-json-parse-error {
|
||||
/// The tool use ID.
|
||||
id: string,
|
||||
/// The tool name.
|
||||
tool-name: string,
|
||||
/// The raw input that failed to parse.
|
||||
raw-input: string,
|
||||
/// The parse error message.
|
||||
error: string,
|
||||
}
|
||||
|
||||
/// Reason the completion stopped.
|
||||
enum stop-reason {
|
||||
/// The model finished generating.
|
||||
end-turn,
|
||||
/// Maximum tokens reached.
|
||||
max-tokens,
|
||||
/// The model wants to use a tool.
|
||||
tool-use,
|
||||
/// The model refused to respond.
|
||||
refusal,
|
||||
}
|
||||
|
||||
/// Token usage statistics.
|
||||
record token-usage {
|
||||
/// Number of input tokens used.
|
||||
input-tokens: u64,
|
||||
/// Number of output tokens generated.
|
||||
output-tokens: u64,
|
||||
/// Tokens used for cache creation (if supported).
|
||||
cache-creation-input-tokens: option<u64>,
|
||||
/// Tokens read from cache (if supported).
|
||||
cache-read-input-tokens: option<u64>,
|
||||
}
|
||||
|
||||
/// Cache configuration for prompt caching.
|
||||
record cache-configuration {
|
||||
/// Maximum number of cache anchors.
|
||||
max-cache-anchors: u32,
|
||||
/// Whether caching should be applied to tool definitions.
|
||||
should-cache-tool-definitions: bool,
|
||||
/// Minimum token count for a message to be cached.
|
||||
min-total-token-count: u64,
|
||||
}
|
||||
|
||||
/// Configuration for starting an OAuth web authentication flow.
|
||||
record oauth-web-auth-config {
|
||||
/// The URL to open in the user's browser to start authentication.
|
||||
/// This should include client_id, redirect_uri, scope, state, etc.
|
||||
/// Use `{port}` as a placeholder in the URL - it will be replaced with
|
||||
/// the actual localhost port before opening the browser.
|
||||
/// Example: "https://example.com/oauth?redirect_uri=http://127.0.0.1:{port}/callback"
|
||||
auth-url: string,
|
||||
/// The path to listen on for the OAuth callback (e.g., "/callback").
|
||||
/// A localhost server will be started to receive the redirect.
|
||||
callback-path: string,
|
||||
/// Timeout in seconds to wait for the callback (default: 300 = 5 minutes).
|
||||
timeout-secs: option<u32>,
|
||||
}
|
||||
|
||||
/// Result of an OAuth web authentication flow.
|
||||
record oauth-web-auth-result {
|
||||
/// The full callback URL that was received, including query parameters.
|
||||
/// The extension is responsible for parsing the code, state, etc.
|
||||
callback-url: string,
|
||||
/// The port that was used for the localhost callback server.
|
||||
port: u32,
|
||||
}
|
||||
|
||||
/// Get a stored credential for this provider.
|
||||
get-credential: func(provider-id: string) -> option<string>;
|
||||
|
||||
/// Store a credential for this provider.
|
||||
store-credential: func(provider-id: string, value: string) -> result<_, string>;
|
||||
|
||||
/// Delete a stored credential for this provider.
|
||||
delete-credential: func(provider-id: string) -> result<_, string>;
|
||||
|
||||
/// Read an environment variable.
|
||||
get-env-var: func(name: string) -> option<string>;
|
||||
|
||||
/// Start an OAuth web authentication flow.
|
||||
///
|
||||
/// This will:
|
||||
/// 1. Start a localhost server to receive the OAuth callback
|
||||
/// 2. Open the auth URL in the user's default browser
|
||||
/// 3. Wait for the callback (up to the timeout)
|
||||
/// 4. Return the callback URL with query parameters
|
||||
///
|
||||
/// The extension is responsible for:
|
||||
/// - Constructing the auth URL with client_id, redirect_uri, scope, state, etc.
|
||||
/// - Parsing the callback URL to extract the authorization code
|
||||
/// - Exchanging the code for tokens using fetch-fallible from http-client
|
||||
oauth-start-web-auth: func(config: oauth-web-auth-config) -> result<oauth-web-auth-result, string>;
|
||||
|
||||
/// Make an HTTP request for OAuth token exchange.
|
||||
///
|
||||
/// This is a convenience wrapper around http-client's fetch-fallible for OAuth flows.
|
||||
/// Unlike the standard fetch, this does not treat non-2xx responses as errors,
|
||||
/// allowing proper handling of OAuth error responses.
|
||||
oauth-send-http-request: func(request: http-request) -> result<http-response-with-status, string>;
|
||||
|
||||
/// Open a URL in the user's default browser.
|
||||
///
|
||||
/// Useful for OAuth flows that need to open a browser but handle the
|
||||
/// callback differently (e.g., polling-based flows).
|
||||
oauth-open-browser: func(url: string) -> result<_, string>;
|
||||
|
||||
/// Provider settings from user configuration.
|
||||
/// Extensions can use this to allow custom API URLs, custom models, etc.
|
||||
record provider-settings {
|
||||
/// Custom API URL override (if configured by the user).
|
||||
api-url: option<string>,
|
||||
/// Custom models configured by the user.
|
||||
available-models: list<custom-model-config>,
|
||||
}
|
||||
|
||||
/// Configuration for a custom model defined by the user.
|
||||
record custom-model-config {
|
||||
/// The model's API identifier.
|
||||
name: string,
|
||||
/// Display name for the UI.
|
||||
display-name: option<string>,
|
||||
/// Maximum input token count.
|
||||
max-tokens: u64,
|
||||
/// Maximum output tokens (optional).
|
||||
max-output-tokens: option<u64>,
|
||||
/// Thinking budget for models that support extended thinking (None = auto).
|
||||
thinking-budget: option<u32>,
|
||||
}
|
||||
|
||||
/// Get provider-specific settings configured by the user.
|
||||
/// Returns settings like custom API URLs and custom model configurations.
|
||||
get-provider-settings: func(provider-id: string) -> option<provider-settings>;
|
||||
|
||||
/// Information needed to display the device flow prompt modal to the user.
|
||||
record device-flow-prompt-info {
|
||||
/// The user code to display (e.g., "ABC-123").
|
||||
user-code: string,
|
||||
/// The URL the user needs to visit to authorize (for the "Connect" button).
|
||||
verification-url: string,
|
||||
/// The headline text for the modal (e.g., "Use GitHub Copilot in Zed.").
|
||||
headline: string,
|
||||
/// A description to show below the headline (e.g., "Using Copilot requires an active subscription on GitHub.").
|
||||
description: string,
|
||||
/// Label for the connect button (e.g., "Connect to GitHub").
|
||||
connect-button-label: string,
|
||||
/// Success headline shown when authorization completes.
|
||||
success-headline: string,
|
||||
/// Success message shown when authorization completes.
|
||||
success-message: string,
|
||||
}
|
||||
}
|
||||
@@ -255,21 +255,6 @@ async fn copy_extension_resources(
|
||||
}
|
||||
}
|
||||
|
||||
for (_, provider_entry) in &manifest.language_model_providers {
|
||||
if let Some(icon_path) = &provider_entry.icon {
|
||||
let source_icon = extension_path.join(icon_path);
|
||||
let dest_icon = output_dir.join(icon_path);
|
||||
|
||||
// Create parent directory if needed
|
||||
if let Some(parent) = dest_icon.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
fs::copy(&source_icon, &dest_icon)
|
||||
.with_context(|| format!("failed to copy LLM provider icon '{}'", icon_path))?;
|
||||
}
|
||||
}
|
||||
|
||||
if !manifest.languages.is_empty() {
|
||||
let output_languages_dir = output_dir.join("languages");
|
||||
fs::create_dir_all(&output_languages_dir)?;
|
||||
|
||||
@@ -22,9 +22,7 @@ async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
dap.workspace = true
|
||||
dirs.workspace = true
|
||||
extension.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
@@ -32,11 +30,8 @@ gpui.workspace = true
|
||||
gpui_tokio.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
markdown.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
moka.workspace = true
|
||||
node_runtime.workspace = true
|
||||
paths.workspace = true
|
||||
@@ -48,16 +43,11 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
telemetry.workspace = true
|
||||
tempfile.workspace = true
|
||||
theme.workspace = true
|
||||
toml.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
url.workspace = true
|
||||
workspace.workspace = true
|
||||
util.workspace = true
|
||||
wasmparser.workspace = true
|
||||
wasmtime-wasi.workspace = true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user