Compare commits
1 Commits
gdb-dap-wo
...
logging
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
824b430767 |
@@ -1,55 +0,0 @@
|
||||
# Phase 2: Explore Repository
|
||||
|
||||
You are analyzing a codebase to understand its structure before reviewing documentation impact.
|
||||
|
||||
## Objective
|
||||
Produce a structured overview of the repository to inform subsequent documentation analysis.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Identify Primary Languages and Frameworks**
|
||||
- Scan for Cargo.toml, package.json, or other manifest files
|
||||
- Note the primary language(s) and key dependencies
|
||||
|
||||
2. **Map Documentation Structure**
|
||||
- This project uses **mdBook** (https://rust-lang.github.io/mdBook/)
|
||||
- Documentation is in `docs/src/`
|
||||
- Table of contents: `docs/src/SUMMARY.md` (mdBook format: https://rust-lang.github.io/mdBook/format/summary.html)
|
||||
- Style guide: `docs/.rules`
|
||||
- Agent guidelines: `docs/AGENTS.md`
|
||||
- Formatting: Prettier (config in `docs/.prettierrc`)
|
||||
|
||||
3. **Identify Build and Tooling**
|
||||
- Note build systems (cargo, npm, etc.)
|
||||
- Identify documentation tooling (mdbook, etc.)
|
||||
|
||||
4. **Output Format**
|
||||
Produce a JSON summary:
|
||||
|
||||
```json
|
||||
{
|
||||
"primary_language": "Rust",
|
||||
"frameworks": ["GPUI"],
|
||||
"documentation": {
|
||||
"system": "mdBook",
|
||||
"location": "docs/src/",
|
||||
"toc_file": "docs/src/SUMMARY.md",
|
||||
"toc_format": "https://rust-lang.github.io/mdBook/format/summary.html",
|
||||
"style_guide": "docs/.rules",
|
||||
"agent_guidelines": "docs/AGENTS.md",
|
||||
"formatter": "prettier",
|
||||
"formatter_config": "docs/.prettierrc",
|
||||
"custom_preprocessor": "docs_preprocessor (handles {#kb action::Name} syntax)"
|
||||
},
|
||||
"key_directories": {
|
||||
"source": "crates/",
|
||||
"docs": "docs/src/",
|
||||
"extensions": "extensions/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Constraints
|
||||
- Read-only: Do not modify any files
|
||||
- Focus on structure, not content details
|
||||
- Complete within 2 minutes
|
||||
@@ -1,57 +0,0 @@
|
||||
# Phase 3: Analyze Changes
|
||||
|
||||
You are analyzing code changes to understand their nature and scope.
|
||||
|
||||
## Objective
|
||||
Produce a clear, neutral summary of what changed in the codebase.
|
||||
|
||||
## Input
|
||||
You will receive:
|
||||
- List of changed files from the triggering commit/PR
|
||||
- Repository structure from Phase 2
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Categorize Changed Files**
|
||||
- Source code (which crates/modules)
|
||||
- Configuration
|
||||
- Tests
|
||||
- Documentation (already existing)
|
||||
- Other
|
||||
|
||||
2. **Analyze Each Change**
|
||||
- Review diffs for files likely to impact documentation
|
||||
- Focus on: public APIs, settings, keybindings, commands, user-visible behavior
|
||||
|
||||
3. **Identify What Did NOT Change**
|
||||
- Note stable interfaces or behaviors
|
||||
- Important for avoiding unnecessary documentation updates
|
||||
|
||||
4. **Output Format**
|
||||
Produce a markdown summary:
|
||||
|
||||
```markdown
|
||||
## Change Analysis
|
||||
|
||||
### Changed Files Summary
|
||||
| Category | Files | Impact Level |
|
||||
| --- | --- | --- |
|
||||
| Source - [crate] | file1.rs, file2.rs | High/Medium/Low |
|
||||
| Settings | settings.json | Medium |
|
||||
| Tests | test_*.rs | None |
|
||||
|
||||
### Behavioral Changes
|
||||
- **[Feature/Area]**: Description of what changed from user perspective
|
||||
- **[Feature/Area]**: Description...
|
||||
|
||||
### Unchanged Areas
|
||||
- [Area]: Confirmed no changes to [specific behavior]
|
||||
|
||||
### Files Requiring Deeper Review
|
||||
- `path/to/file.rs`: Reason for deeper review
|
||||
```
|
||||
|
||||
## Constraints
|
||||
- Read-only: Do not modify any files
|
||||
- Neutral tone: Describe what changed, not whether it's good/bad
|
||||
- Do not propose documentation changes yet
|
||||
@@ -1,76 +0,0 @@
|
||||
# Phase 4: Plan Documentation Impact
|
||||
|
||||
You are determining whether and how documentation should be updated based on code changes.
|
||||
|
||||
## Objective
|
||||
Produce a structured documentation plan that will guide Phase 5 execution.
|
||||
|
||||
## Documentation System
|
||||
This is an **mdBook** site (https://rust-lang.github.io/mdBook/):
|
||||
- `docs/src/SUMMARY.md` defines book structure per https://rust-lang.github.io/mdBook/format/summary.html
|
||||
- If adding new pages, they MUST be added to SUMMARY.md
|
||||
- Use `{#kb action::ActionName}` syntax for keybindings (custom preprocessor expands these)
|
||||
- Prettier formatting (80 char width) will be applied automatically
|
||||
|
||||
## Input
|
||||
You will receive:
|
||||
- Change analysis from Phase 3
|
||||
- Repository structure from Phase 2
|
||||
- Documentation guidelines from `docs/AGENTS.md`
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Review AGENTS.md**
|
||||
- Load and apply all rules from `docs/AGENTS.md`
|
||||
- Respect scope boundaries (in-scope vs out-of-scope)
|
||||
|
||||
2. **Evaluate Documentation Impact**
|
||||
For each behavioral change from Phase 3:
|
||||
- Does existing documentation cover this area?
|
||||
- Is the documentation now inaccurate or incomplete?
|
||||
- Classify per AGENTS.md "Change Classification" section
|
||||
|
||||
3. **Identify Specific Updates**
|
||||
For each required update:
|
||||
- Exact file path
|
||||
- Specific section or heading
|
||||
- Type of change (update existing, add new, deprecate)
|
||||
- Description of the change
|
||||
|
||||
4. **Flag Uncertainty**
|
||||
Explicitly mark:
|
||||
- Assumptions you're making
|
||||
- Areas where human confirmation is needed
|
||||
- Ambiguous requirements
|
||||
|
||||
5. **Output Format**
|
||||
Use the exact format specified in `docs/AGENTS.md` Phase 4 section:
|
||||
|
||||
```markdown
|
||||
## Documentation Impact Assessment
|
||||
|
||||
### Summary
|
||||
Brief description of code changes analyzed.
|
||||
|
||||
### Documentation Updates Required: [Yes/No]
|
||||
|
||||
### Planned Changes
|
||||
|
||||
#### 1. [File Path]
|
||||
- **Section**: [Section name or "New section"]
|
||||
- **Change Type**: [Update/Add/Deprecate]
|
||||
- **Reason**: Why this change is needed
|
||||
- **Description**: What will be added/modified
|
||||
|
||||
### Uncertainty Flags
|
||||
- [ ] [Description of any assumptions or areas needing confirmation]
|
||||
|
||||
### No Changes Needed
|
||||
- [List files reviewed but not requiring updates, with brief reason]
|
||||
```
|
||||
|
||||
## Constraints
|
||||
- Read-only: Do not modify any files
|
||||
- Conservative: When uncertain, flag for human review rather than planning changes
|
||||
- Scoped: Only plan changes that trace directly to code changes from Phase 3
|
||||
- No scope expansion: Do not plan "improvements" unrelated to triggering changes
|
||||
@@ -1,67 +0,0 @@
|
||||
# Phase 5: Apply Documentation Plan
|
||||
|
||||
You are executing a pre-approved documentation plan for an **mdBook** documentation site.
|
||||
|
||||
## Objective
|
||||
Implement exactly the changes specified in the documentation plan from Phase 4.
|
||||
|
||||
## Documentation System
|
||||
- **mdBook**: https://rust-lang.github.io/mdBook/
|
||||
- **SUMMARY.md**: Follows mdBook format (https://rust-lang.github.io/mdBook/format/summary.html)
|
||||
- **Prettier**: Will be run automatically after this phase (80 char line width)
|
||||
- **Custom preprocessor**: Use `{#kb action::ActionName}` for keybindings instead of hardcoding
|
||||
|
||||
## Input
|
||||
You will receive:
|
||||
- Documentation plan from Phase 4
|
||||
- Documentation guidelines from `docs/AGENTS.md`
|
||||
- Style rules from `docs/.rules`
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Validate Plan**
|
||||
- Confirm all planned files are within scope per AGENTS.md
|
||||
- Verify no out-of-scope files are targeted
|
||||
|
||||
2. **Execute Each Planned Change**
|
||||
For each item in "Planned Changes":
|
||||
- Navigate to the specified file
|
||||
- Locate the specified section
|
||||
- Apply the described change
|
||||
- Follow style rules from `docs/.rules`
|
||||
|
||||
3. **Style Compliance**
|
||||
Every edit must follow `docs/.rules`:
|
||||
- Second person, present tense
|
||||
- No hedging words ("simply", "just", "easily")
|
||||
- Proper keybinding format (`Cmd+Shift+P`)
|
||||
- Settings Editor first, JSON second
|
||||
- Correct terminology (folder not directory, etc.)
|
||||
|
||||
4. **Preserve Context**
|
||||
- Maintain surrounding content structure
|
||||
- Keep consistent heading levels
|
||||
- Preserve existing cross-references
|
||||
|
||||
## Constraints
|
||||
- Execute ONLY changes listed in the plan
|
||||
- Do not discover new documentation targets
|
||||
- Do not make stylistic improvements outside planned sections
|
||||
- Do not expand scope beyond what Phase 4 specified
|
||||
- If a planned change cannot be applied (file missing, section not found), skip and note it
|
||||
|
||||
## Output
|
||||
After applying changes, output a summary:
|
||||
|
||||
```markdown
|
||||
## Applied Changes
|
||||
|
||||
### Successfully Applied
|
||||
- `path/to/file.md`: [Brief description of change]
|
||||
|
||||
### Skipped (Could Not Apply)
|
||||
- `path/to/file.md`: [Reason - e.g., "Section not found"]
|
||||
|
||||
### Warnings
|
||||
- [Any issues encountered during application]
|
||||
```
|
||||
@@ -1,54 +0,0 @@
|
||||
# Phase 6: Summarize Changes
|
||||
|
||||
You are generating a summary of documentation updates for PR review.
|
||||
|
||||
## Objective
|
||||
Create a clear, reviewable summary of all documentation changes made.
|
||||
|
||||
## Input
|
||||
You will receive:
|
||||
- Applied changes report from Phase 5
|
||||
- Original change analysis from Phase 3
|
||||
- Git diff of documentation changes
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Gather Change Information**
|
||||
- List all modified documentation files
|
||||
- Identify the corresponding code changes that triggered each update
|
||||
|
||||
2. **Generate Summary**
|
||||
Use the format specified in `docs/AGENTS.md` Phase 6 section:
|
||||
|
||||
```markdown
|
||||
## Documentation Update Summary
|
||||
|
||||
### Changes Made
|
||||
| File | Change | Related Code |
|
||||
| --- | --- | --- |
|
||||
| docs/src/path.md | Brief description | PR #123 or commit SHA |
|
||||
|
||||
### Rationale
|
||||
Brief explanation of why these updates were made, linking back to the triggering code changes.
|
||||
|
||||
### Review Notes
|
||||
- Items reviewers should pay special attention to
|
||||
- Any uncertainty flags from Phase 4 that were addressed
|
||||
- Assumptions made during documentation
|
||||
```
|
||||
|
||||
3. **Add Context for Reviewers**
|
||||
- Highlight any changes that might be controversial
|
||||
- Note if any planned changes were skipped and why
|
||||
- Flag areas where reviewer expertise is especially needed
|
||||
|
||||
## Output Format
|
||||
The summary should be suitable for:
|
||||
- PR description body
|
||||
- Commit message (condensed version)
|
||||
- Team communication
|
||||
|
||||
## Constraints
|
||||
- Read-only (documentation changes already applied in Phase 5)
|
||||
- Factual: Describe what was done, not justify why it's good
|
||||
- Complete: Account for all changes, including skipped items
|
||||
@@ -1,67 +0,0 @@
|
||||
# Phase 7: Commit and Open PR
|
||||
|
||||
You are creating a git branch, committing documentation changes, and opening a PR.
|
||||
|
||||
## Objective
|
||||
Package documentation updates into a reviewable pull request.
|
||||
|
||||
## Input
|
||||
You will receive:
|
||||
- Summary from Phase 6
|
||||
- List of modified files
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Create Branch**
|
||||
```sh
|
||||
git checkout -b docs/auto-update-{date}
|
||||
```
|
||||
Use format: `docs/auto-update-YYYY-MM-DD` or `docs/auto-update-{short-sha}`
|
||||
|
||||
2. **Stage and Commit**
|
||||
- Stage only documentation files in `docs/src/`
|
||||
- Do not stage any other files
|
||||
|
||||
Commit message format:
|
||||
```
|
||||
docs: auto-update documentation for [brief description]
|
||||
|
||||
[Summary from Phase 6, condensed]
|
||||
|
||||
Triggered by: [commit SHA or PR reference]
|
||||
|
||||
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
||||
```
|
||||
|
||||
3. **Push Branch**
|
||||
```sh
|
||||
git push -u origin docs/auto-update-{date}
|
||||
```
|
||||
|
||||
4. **Create Pull Request**
|
||||
Use the Phase 6 summary as the PR body.
|
||||
|
||||
PR Title: `docs: [Brief description of documentation updates]`
|
||||
|
||||
Labels (if available): `documentation`, `automated`
|
||||
|
||||
Base branch: `main`
|
||||
|
||||
## Constraints
|
||||
- Do NOT auto-merge
|
||||
- Do NOT request specific reviewers (let CODEOWNERS handle it)
|
||||
- Do NOT modify files outside `docs/src/`
|
||||
- If no changes to commit, exit gracefully with message "No documentation changes to commit"
|
||||
|
||||
## Output
|
||||
```markdown
|
||||
## PR Created
|
||||
|
||||
- **Branch**: docs/auto-update-{date}
|
||||
- **PR URL**: https://github.com/zed-industries/zed/pull/XXXX
|
||||
- **Status**: Ready for review
|
||||
|
||||
### Commit
|
||||
- SHA: {commit-sha}
|
||||
- Files: {count} documentation files modified
|
||||
```
|
||||
12
.github/actions/build_docs/action.yml
vendored
12
.github/actions/build_docs/action.yml
vendored
@@ -19,18 +19,6 @@ runs:
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: ./script/linux
|
||||
|
||||
- name: Install mold linker
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: ./script/install-mold
|
||||
|
||||
- name: Download WASI SDK
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: ./script/download-wasi-sdk
|
||||
|
||||
- name: Generate action metadata
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: ./script/generate-action-metadata
|
||||
|
||||
- name: Check for broken links (in MD)
|
||||
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
|
||||
with:
|
||||
|
||||
6
.github/workflows/autofix_pr.yml
vendored
6
.github/workflows/autofix_pr.yml
vendored
@@ -54,10 +54,6 @@ jobs:
|
||||
- name: autofix_pr::run_autofix::run_cargo_fmt
|
||||
run: cargo fmt --all
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: autofix_pr::run_autofix::run_cargo_fix
|
||||
if: ${{ inputs.run_clippy }}
|
||||
run: cargo fix --workspace --release --all-targets --all-features --allow-dirty --allow-staged
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: autofix_pr::run_autofix::run_clippy_fix
|
||||
if: ${{ inputs.run_clippy }}
|
||||
run: cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged
|
||||
@@ -94,7 +90,7 @@ jobs:
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
name: autofix_pr::commit_changes::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
|
||||
2
.github/workflows/cherry_pick.yml
vendored
2
.github/workflows/cherry_pick.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
name: cherry_pick::run_cherry_pick::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
|
||||
@@ -1,40 +1,29 @@
|
||||
name: "Close Stale Issues"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 2 * * 5"
|
||||
- cron: "0 8 31 DEC *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug-only:
|
||||
description: "Run in dry-run mode (no changes made)"
|
||||
type: boolean
|
||||
default: false
|
||||
operations-per-run:
|
||||
description: "Max number of issues to process (default: 1000)"
|
||||
type: number
|
||||
default: 1000
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: >
|
||||
Hi there!
|
||||
Zed development moves fast and a significant number of bugs become outdated.
|
||||
If you can reproduce this bug on the latest stable Zed, please let us know by leaving a comment with the Zed version.
|
||||
If the bug doesn't appear for you anymore, feel free to close the issue yourself; otherwise, the bot will close it in a couple of weeks.
|
||||
Hi there! 👋
|
||||
|
||||
We're working to clean up our issue tracker by closing older bugs that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and it will be kept open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, it will close automatically in 14 days.
|
||||
|
||||
Thanks for your help!
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please leave a comment with your Zed version so that we can reopen the issue."
|
||||
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
|
||||
days-before-stale: 60
|
||||
days-before-close: 14
|
||||
only-issue-types: "Bug,Crash"
|
||||
operations-per-run: ${{ inputs.operations-per-run || 1000 }}
|
||||
operations-per-run: 1000
|
||||
ascending: true
|
||||
enable-statistics: true
|
||||
debug-only: ${{ inputs.debug-only }}
|
||||
stale-issue-label: "stale"
|
||||
exempt-issue-labels: "never stale"
|
||||
|
||||
264
.github/workflows/docs_automation.yml
vendored
264
.github/workflows/docs_automation.yml
vendored
@@ -1,264 +0,0 @@
|
||||
name: Documentation Automation
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches: [main]
|
||||
# paths:
|
||||
# - 'crates/**'
|
||||
# - 'extensions/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to analyze (gets full PR diff)'
|
||||
required: false
|
||||
type: string
|
||||
trigger_sha:
|
||||
description: 'Commit SHA to analyze (ignored if pr_number is set)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
|
||||
DROID_MODEL: claude-opus-4-5-20251101
|
||||
|
||||
jobs:
|
||||
docs-automation:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Droid CLI
|
||||
id: install-droid
|
||||
run: |
|
||||
curl -fsSL https://app.factory.ai/cli | sh
|
||||
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
|
||||
echo "DROID_BIN=${HOME}/.local/bin/droid" >> "$GITHUB_ENV"
|
||||
# Verify installation
|
||||
"${HOME}/.local/bin/droid" --version
|
||||
|
||||
- name: Setup Node.js (for Prettier)
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install Prettier
|
||||
run: npm install -g prettier
|
||||
|
||||
- name: Get changed files
|
||||
id: changed
|
||||
run: |
|
||||
if [ -n "${{ inputs.pr_number }}" ]; then
|
||||
# Get full PR diff
|
||||
echo "Analyzing PR #${{ inputs.pr_number }}"
|
||||
echo "source=pr" >> "$GITHUB_OUTPUT"
|
||||
echo "ref=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT"
|
||||
gh pr diff "${{ inputs.pr_number }}" --name-only > /tmp/changed_files.txt
|
||||
elif [ -n "${{ inputs.trigger_sha }}" ]; then
|
||||
# Get single commit diff
|
||||
SHA="${{ inputs.trigger_sha }}"
|
||||
echo "Analyzing commit $SHA"
|
||||
echo "source=commit" >> "$GITHUB_OUTPUT"
|
||||
echo "ref=$SHA" >> "$GITHUB_OUTPUT"
|
||||
git diff --name-only "${SHA}^" "$SHA" > /tmp/changed_files.txt
|
||||
else
|
||||
# Default to current commit
|
||||
SHA="${{ github.sha }}"
|
||||
echo "Analyzing commit $SHA"
|
||||
echo "source=commit" >> "$GITHUB_OUTPUT"
|
||||
echo "ref=$SHA" >> "$GITHUB_OUTPUT"
|
||||
git diff --name-only "${SHA}^" "$SHA" > /tmp/changed_files.txt || git diff --name-only HEAD~1 HEAD > /tmp/changed_files.txt
|
||||
fi
|
||||
|
||||
echo "Changed files:"
|
||||
cat /tmp/changed_files.txt
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
# Phase 0: Guardrails are loaded via AGENTS.md in each phase
|
||||
|
||||
# Phase 2: Explore Repository (Read-Only - default)
|
||||
- name: "Phase 2: Explore Repository"
|
||||
id: phase2
|
||||
run: |
|
||||
"$DROID_BIN" exec \
|
||||
-m "$DROID_MODEL" \
|
||||
-f .factory/prompts/docs-automation/phase2-explore.md \
|
||||
> /tmp/phase2-output.txt 2>&1 || true
|
||||
echo "Repository exploration complete"
|
||||
cat /tmp/phase2-output.txt
|
||||
|
||||
# Phase 3: Analyze Changes (Read-Only - default)
|
||||
- name: "Phase 3: Analyze Changes"
|
||||
id: phase3
|
||||
run: |
|
||||
CHANGED_FILES=$(tr '\n' ' ' < /tmp/changed_files.txt)
|
||||
echo "Analyzing changes in: $CHANGED_FILES"
|
||||
|
||||
# Build prompt with context
|
||||
cat > /tmp/phase3-prompt.md << 'EOF'
|
||||
$(cat .factory/prompts/docs-automation/phase3-analyze.md)
|
||||
|
||||
## Context
|
||||
|
||||
### Changed Files
|
||||
$CHANGED_FILES
|
||||
|
||||
### Phase 2 Output
|
||||
$(cat /tmp/phase2-output.txt)
|
||||
EOF
|
||||
|
||||
"$DROID_BIN" exec \
|
||||
-m "$DROID_MODEL" \
|
||||
"$(cat .factory/prompts/docs-automation/phase3-analyze.md)
|
||||
|
||||
Changed files: $CHANGED_FILES" \
|
||||
> /tmp/phase3-output.md 2>&1 || true
|
||||
echo "Change analysis complete"
|
||||
cat /tmp/phase3-output.md
|
||||
|
||||
# Phase 4: Plan Documentation Impact (Read-Only - default)
|
||||
- name: "Phase 4: Plan Documentation Impact"
|
||||
id: phase4
|
||||
run: |
|
||||
"$DROID_BIN" exec \
|
||||
-m "$DROID_MODEL" \
|
||||
-f .factory/prompts/docs-automation/phase4-plan.md \
|
||||
> /tmp/phase4-plan.md 2>&1 || true
|
||||
echo "Documentation plan complete"
|
||||
cat /tmp/phase4-plan.md
|
||||
|
||||
# Check if updates are required
|
||||
if grep -q "NO_UPDATES_REQUIRED" /tmp/phase4-plan.md; then
|
||||
echo "updates_required=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "updates_required=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Phase 5: Apply Plan (Write-Enabled with --auto medium)
|
||||
- name: "Phase 5: Apply Documentation Plan"
|
||||
id: phase5
|
||||
if: steps.phase4.outputs.updates_required == 'true'
|
||||
run: |
|
||||
"$DROID_BIN" exec \
|
||||
-m "$DROID_MODEL" \
|
||||
--auto medium \
|
||||
-f .factory/prompts/docs-automation/phase5-apply.md \
|
||||
> /tmp/phase5-report.md 2>&1 || true
|
||||
echo "Documentation updates applied"
|
||||
cat /tmp/phase5-report.md
|
||||
|
||||
# Phase 5b: Format with Prettier
|
||||
- name: "Phase 5b: Format with Prettier"
|
||||
id: phase5b
|
||||
if: steps.phase4.outputs.updates_required == 'true'
|
||||
run: |
|
||||
echo "Formatting documentation with Prettier..."
|
||||
cd docs && prettier --write src/
|
||||
|
||||
echo "Verifying Prettier formatting passes..."
|
||||
cd docs && prettier --check src/
|
||||
|
||||
echo "Prettier formatting complete"
|
||||
|
||||
# Phase 6: Summarize Changes (Read-Only - default)
|
||||
- name: "Phase 6: Summarize Changes"
|
||||
id: phase6
|
||||
if: steps.phase4.outputs.updates_required == 'true'
|
||||
run: |
|
||||
# Get git diff of docs
|
||||
git diff docs/src/ > /tmp/docs-diff.txt || true
|
||||
|
||||
"$DROID_BIN" exec \
|
||||
-m "$DROID_MODEL" \
|
||||
-f .factory/prompts/docs-automation/phase6-summarize.md \
|
||||
> /tmp/phase6-summary.md 2>&1 || true
|
||||
echo "Summary generated"
|
||||
cat /tmp/phase6-summary.md
|
||||
|
||||
# Phase 7: Commit and Open PR
|
||||
- name: "Phase 7: Create PR"
|
||||
id: phase7
|
||||
if: steps.phase4.outputs.updates_required == 'true'
|
||||
run: |
|
||||
# Check if there are actual changes
|
||||
if git diff --quiet docs/src/; then
|
||||
echo "No documentation changes detected"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Configure git
|
||||
git config user.name "factory-droid[bot]"
|
||||
git config user.email "138933559+factory-droid[bot]@users.noreply.github.com"
|
||||
|
||||
# Daily batch branch - one branch per day, multiple commits accumulate
|
||||
BRANCH_NAME="docs/auto-update-$(date +%Y-%m-%d)"
|
||||
|
||||
# Stash local changes from phase 5
|
||||
git stash push -m "docs-automation-changes" -- docs/src/
|
||||
|
||||
# Check if branch already exists on remote
|
||||
if git ls-remote --exit-code --heads origin "$BRANCH_NAME" > /dev/null 2>&1; then
|
||||
echo "Branch $BRANCH_NAME exists, checking out and updating..."
|
||||
git fetch origin "$BRANCH_NAME"
|
||||
git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME"
|
||||
else
|
||||
echo "Creating new branch $BRANCH_NAME..."
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
fi
|
||||
|
||||
# Apply stashed changes
|
||||
git stash pop || true
|
||||
|
||||
# Stage and commit
|
||||
git add docs/src/
|
||||
SUMMARY=$(head -50 < /tmp/phase6-summary.md)
|
||||
git commit -m "docs: auto-update documentation
|
||||
|
||||
${SUMMARY}
|
||||
|
||||
Triggered by: ${{ steps.changed.outputs.source }} ${{ steps.changed.outputs.ref }}
|
||||
|
||||
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>"
|
||||
|
||||
# Push
|
||||
git push -u origin "$BRANCH_NAME"
|
||||
|
||||
# Check if PR already exists for this branch
|
||||
EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number --jq '.[0].number' || echo "")
|
||||
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "PR #$EXISTING_PR already exists for branch $BRANCH_NAME, updated with new commit"
|
||||
else
|
||||
# Create new PR
|
||||
gh pr create \
|
||||
--title "docs: automated documentation update ($(date +%Y-%m-%d))" \
|
||||
--body-file /tmp/phase6-summary.md \
|
||||
--base main || true
|
||||
echo "PR created on branch: $BRANCH_NAME"
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
# Summary output
|
||||
- name: "Summary"
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Documentation Automation Summary" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [ "${{ steps.phase4.outputs.updates_required }}" == "false" ]; then
|
||||
echo "No documentation updates required for this change." >> "$GITHUB_STEP_SUMMARY"
|
||||
elif [ -f /tmp/phase6-summary.md ]; then
|
||||
cat /tmp/phase6-summary.md >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "Workflow completed. Check individual phase outputs for details." >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -74,6 +74,12 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -472,17 +478,11 @@ jobs:
|
||||
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- id: get-app-token
|
||||
name: steps::authenticate_as_zippy
|
||||
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
|
||||
with:
|
||||
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
|
||||
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
|
||||
- name: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
|
||||
run: gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
notify_on_failure:
|
||||
needs:
|
||||
- upload_release_assets
|
||||
|
||||
17
.github/workflows/run_tests.yml
vendored
17
.github/workflows/run_tests.yml
vendored
@@ -74,12 +74,18 @@ jobs:
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
with:
|
||||
version: '9'
|
||||
- name: steps::prettier
|
||||
- name: ./script/prettier
|
||||
run: ./script/prettier
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=false
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: ./script/check-todos
|
||||
run: ./script/check-todos
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -160,6 +166,12 @@ jobs:
|
||||
- name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::trigger_autofix
|
||||
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -353,9 +365,6 @@ jobs:
|
||||
- name: steps::download_wasi_sdk
|
||||
run: ./script/download-wasi-sdk
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/generate-action-metadata
|
||||
run: ./script/generate-action-metadata
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: run_tests::check_docs::install_mdbook
|
||||
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08
|
||||
with:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -36,7 +36,6 @@
|
||||
DerivedData/
|
||||
Packages
|
||||
xcuserdata/
|
||||
crates/docs_preprocessor/actions.json
|
||||
|
||||
# Don't commit any secrets to the repo.
|
||||
.env
|
||||
|
||||
133
Cargo.lock
generated
133
Cargo.lock
generated
@@ -226,9 +226,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.9.2"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
|
||||
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
|
||||
dependencies = [
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
@@ -243,9 +243,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol-schema"
|
||||
version = "0.10.5"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
|
||||
checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_more 2.0.1",
|
||||
@@ -793,7 +793,7 @@ dependencies = [
|
||||
"url",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.32.9",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -2667,9 +2667,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-fs-ext"
|
||||
version = "3.4.4"
|
||||
version = "3.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e41cc18551193fe8fa6f15c1e3c799bc5ec9e2cfbfaa8ed46f37013e3e6c173c"
|
||||
checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654"
|
||||
dependencies = [
|
||||
"cap-primitives",
|
||||
"cap-std",
|
||||
@@ -2679,9 +2679,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-net-ext"
|
||||
version = "3.4.4"
|
||||
version = "3.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f83833816c66c986e913b22ac887cec216ea09301802054316fc5301809702c"
|
||||
checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7"
|
||||
dependencies = [
|
||||
"cap-primitives",
|
||||
"cap-std",
|
||||
@@ -2691,9 +2691,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-primitives"
|
||||
version = "3.4.4"
|
||||
version = "3.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a1e394ed14f39f8bc26f59d4c0c010dbe7f0a1b9bafff451b1f98b67c8af62a"
|
||||
checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a"
|
||||
dependencies = [
|
||||
"ambient-authority",
|
||||
"fs-set-times",
|
||||
@@ -2709,9 +2709,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-rand"
|
||||
version = "3.4.4"
|
||||
version = "3.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0acb89ccf798a28683f00089d0630dfaceec087234eae0d308c05ddeaa941b40"
|
||||
checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d"
|
||||
dependencies = [
|
||||
"ambient-authority",
|
||||
"rand 0.8.5",
|
||||
@@ -2719,9 +2719,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-std"
|
||||
version = "3.4.4"
|
||||
version = "3.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07c0355ca583dd58f176c3c12489d684163861ede3c9efa6fd8bba314c984189"
|
||||
checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a"
|
||||
dependencies = [
|
||||
"cap-primitives",
|
||||
"io-extras",
|
||||
@@ -2731,9 +2731,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cap-time-ext"
|
||||
version = "3.4.4"
|
||||
version = "3.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "491af520b8770085daa0466978c75db90368c71896523f2464214e38359b1a5b"
|
||||
checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80"
|
||||
dependencies = [
|
||||
"ambient-authority",
|
||||
"cap-primitives",
|
||||
@@ -2896,17 +2896,6 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chardetng"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.42"
|
||||
@@ -3525,33 +3514,6 @@ dependencies = [
|
||||
"theme",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "component_preview"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"component",
|
||||
"db",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"node_runtime",
|
||||
"notifications",
|
||||
"project",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
"session",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"uuid",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compression-codecs"
|
||||
version = "0.4.31"
|
||||
@@ -5048,6 +5010,8 @@ name = "docs_preprocessor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command_palette",
|
||||
"gpui",
|
||||
"mdbook",
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -5056,6 +5020,7 @@ dependencies = [
|
||||
"task",
|
||||
"theme",
|
||||
"util",
|
||||
"zed",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
@@ -7394,7 +7359,7 @@ dependencies = [
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-protocols-plasma",
|
||||
"wayland-protocols-wlr",
|
||||
"windows 0.61.3",
|
||||
@@ -8832,7 +8797,6 @@ dependencies = [
|
||||
"ctor",
|
||||
"diffy",
|
||||
"ec4rs",
|
||||
"encoding_rs",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -8956,8 +8920,6 @@ dependencies = [
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
"editor",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
@@ -12503,7 +12465,6 @@ dependencies = [
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"db",
|
||||
"encoding_rs",
|
||||
"extension",
|
||||
"fancy-regex",
|
||||
"fs",
|
||||
@@ -12597,7 +12558,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"notifications",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rayon",
|
||||
@@ -12675,8 +12635,6 @@ dependencies = [
|
||||
"paths",
|
||||
"rope",
|
||||
"serde",
|
||||
"strum 0.27.2",
|
||||
"tempfile",
|
||||
"text",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -18956,6 +18914,18 @@ dependencies = [
|
||||
"xcursor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.9"
|
||||
@@ -18970,14 +18940,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.3.9"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
|
||||
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
@@ -18990,7 +18960,7 @@ dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.32.9",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
@@ -19150,20 +19120,6 @@ dependencies = [
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which_key"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"command_palette",
|
||||
"gpui",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.6.1"
|
||||
@@ -20261,10 +20217,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-lock 2.8.0",
|
||||
"chardetng",
|
||||
"clock",
|
||||
"collections",
|
||||
"encoding_rs",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -20292,16 +20246,6 @@ dependencies = [
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "worktree_benchmarks"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"fs",
|
||||
"gpui",
|
||||
"settings",
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.1"
|
||||
@@ -20670,7 +20614,6 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette",
|
||||
"component",
|
||||
"component_preview",
|
||||
"copilot",
|
||||
"crashes",
|
||||
"dap",
|
||||
@@ -20776,6 +20719,7 @@ dependencies = [
|
||||
"tree-sitter-md",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"ui_prompt",
|
||||
"url",
|
||||
"urlencoding",
|
||||
@@ -20786,7 +20730,6 @@ dependencies = [
|
||||
"watch",
|
||||
"web_search",
|
||||
"web_search_providers",
|
||||
"which_key",
|
||||
"windows 0.61.3",
|
||||
"winresource",
|
||||
"workspace",
|
||||
|
||||
@@ -39,7 +39,6 @@ members = [
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/component",
|
||||
"crates/component_preview",
|
||||
"crates/context_server",
|
||||
"crates/copilot",
|
||||
"crates/crashes",
|
||||
@@ -193,13 +192,11 @@ members = [
|
||||
"crates/vercel",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/which_key",
|
||||
"crates/watch",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
"crates/worktree_benchmarks",
|
||||
"crates/x_ai",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
@@ -276,7 +273,6 @@ collections = { path = "crates/collections", version = "0.1.0" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
component = { path = "crates/component" }
|
||||
component_preview = { path = "crates/component_preview" }
|
||||
context_server = { path = "crates/context_server" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
crashes = { path = "crates/crashes" }
|
||||
@@ -419,7 +415,6 @@ util_macros = { path = "crates/util_macros" }
|
||||
vercel = { path = "crates/vercel" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
which_key = { path = "crates/which_key" }
|
||||
|
||||
watch = { path = "crates/watch" }
|
||||
web_search = { path = "crates/web_search" }
|
||||
@@ -441,7 +436,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
|
||||
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
@@ -481,7 +476,6 @@ bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
cfg-if = "1.0.3"
|
||||
chardetng = "0.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ciborium = "0.2"
|
||||
circular-buffer = "1.0"
|
||||
@@ -505,7 +499,6 @@ dotenvy = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
encoding_rs = "0.8"
|
||||
exec = "0.3.1"
|
||||
fancy-regex = "0.16.0"
|
||||
fork = "0.4.0"
|
||||
|
||||
@@ -20,6 +20,7 @@ Other platforms are not yet available:
|
||||
- [Building Zed for macOS](./docs/src/development/macos.md)
|
||||
- [Building Zed for Linux](./docs/src/development/linux.md)
|
||||
- [Building Zed for Windows](./docs/src/development/windows.md)
|
||||
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
|
||||
|
||||
### Contributing
|
||||
|
||||
|
||||
@@ -227,7 +227,6 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -294,7 +293,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -306,7 +304,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -908,8 +905,8 @@
|
||||
"bindings": {
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-y": "git::StageFile",
|
||||
"alt-shift-y": "git::UnstageFile",
|
||||
|
||||
@@ -267,7 +267,6 @@
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-k l": "agent::OpenRulesLibrary",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -336,7 +335,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -349,7 +347,6 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -984,12 +981,12 @@
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"cmd-up": "git_panel::FirstEntry",
|
||||
"cmd-down": "git_panel::LastEntry",
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"cmd-up": "menu::SelectFirst",
|
||||
"cmd-down": "menu::SelectLast",
|
||||
"enter": "menu::Confirm",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"space": "git::ToggleStaged",
|
||||
|
||||
@@ -227,7 +227,6 @@
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -297,7 +296,6 @@
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -310,7 +308,6 @@
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -911,10 +908,10 @@
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "git_panel::PreviousEntry",
|
||||
"down": "git_panel::NextEntry",
|
||||
"left": "git_panel::CollapseSelectedEntry",
|
||||
"right": "git_panel::ExpandSelectedEntry",
|
||||
"up": "menu::SelectPrevious",
|
||||
"down": "menu::SelectNext",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-y": "git::StageFile",
|
||||
"shift-alt-y": "git::UnstageFile",
|
||||
|
||||
@@ -14,6 +14,7 @@ The section you'll need to rewrite is marked with <rewrite_this></rewrite_this>
|
||||
The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.
|
||||
{{/if}}
|
||||
|
||||
{{#if rewrite_section}}
|
||||
And here's the section to rewrite based on that prompt again for reference:
|
||||
|
||||
<rewrite_this>
|
||||
@@ -32,6 +33,8 @@ Below are the diagnostic errors visible to the user. If the user requests probl
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {{content_type}} will be preserved.
|
||||
|
||||
Start at the indentation level in the original file in the rewritten {{content_type}}.
|
||||
|
||||
@@ -1178,10 +1178,6 @@
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
// Whether to start a new line with a comment when a previous line is a comment as well.
|
||||
"extend_comment_on_newline": true,
|
||||
// Whether to continue markdown lists when pressing enter.
|
||||
"extend_list_on_newline": true,
|
||||
// Whether to indent list items when pressing tab after a list marker.
|
||||
"indent_list_on_tab": true,
|
||||
// Removes any lines containing only whitespace at the end of the file and
|
||||
// ensures just one newline at the end.
|
||||
"ensure_final_newline_on_save": true,
|
||||
@@ -1325,14 +1321,6 @@
|
||||
"hidden_files": ["**/.*"],
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Global switch to enable or disable all git integration features.
|
||||
// If set to true, disables all git integration features.
|
||||
// If set to false, individual git integration features below will be independently enabled or disabled.
|
||||
"disable_git": false,
|
||||
// Whether to enable git status tracking.
|
||||
"enable_status": true,
|
||||
// Whether to enable git diff display.
|
||||
"enable_diff": true,
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
@@ -1717,12 +1705,7 @@
|
||||
// }
|
||||
//
|
||||
"file_types": {
|
||||
"JSONC": [
|
||||
"**/.zed/*.json",
|
||||
"**/.vscode/**/*.json",
|
||||
"**/{zed,Zed}/{settings,keymap,tasks,debug}.json",
|
||||
"tsconfig*.json",
|
||||
],
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"],
|
||||
"Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"],
|
||||
"Shell Script": [".env.*"],
|
||||
},
|
||||
@@ -2169,13 +2152,6 @@
|
||||
// The shape can be one of the following: "block", "bar", "underline", "hollow".
|
||||
"cursor_shape": {},
|
||||
},
|
||||
// Which-key popup settings
|
||||
"which_key": {
|
||||
// Whether to show the which-key popup when holding down key combinations.
|
||||
"enabled": false,
|
||||
// Delay in milliseconds before showing the which-key popup.
|
||||
"delay_ms": 1000,
|
||||
},
|
||||
// The server to connect to. If the environment variable
|
||||
// ZED_SERVER_URL is set, it will override this setting.
|
||||
"server_url": "https://zed.dev",
|
||||
|
||||
@@ -192,7 +192,6 @@ pub struct ToolCall {
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
pub resolved_locations: Vec<Option<AgentLocation>>,
|
||||
pub raw_input: Option<serde_json::Value>,
|
||||
pub raw_input_markdown: Option<Entity<Markdown>>,
|
||||
pub raw_output: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
@@ -223,11 +222,6 @@ impl ToolCall {
|
||||
}
|
||||
}
|
||||
|
||||
let raw_input_markdown = tool_call
|
||||
.raw_input
|
||||
.as_ref()
|
||||
.and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
|
||||
|
||||
let result = Self {
|
||||
id: tool_call.tool_call_id,
|
||||
label: cx
|
||||
@@ -238,7 +232,6 @@ impl ToolCall {
|
||||
resolved_locations: Vec::default(),
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
raw_input_markdown,
|
||||
raw_output: tool_call.raw_output,
|
||||
};
|
||||
Ok(result)
|
||||
@@ -314,7 +307,6 @@ impl ToolCall {
|
||||
}
|
||||
|
||||
if let Some(raw_input) = raw_input {
|
||||
self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
|
||||
self.raw_input = Some(raw_input);
|
||||
}
|
||||
|
||||
@@ -1363,7 +1355,6 @@ impl AcpThread {
|
||||
locations: Vec::new(),
|
||||
resolved_locations: Vec::new(),
|
||||
raw_input: None,
|
||||
raw_input_markdown: None,
|
||||
raw_output: None,
|
||||
};
|
||||
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
|
||||
|
||||
@@ -210,21 +210,12 @@ pub trait AgentModelSelector: 'static {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
pub struct AgentModelInfo {
|
||||
pub id: acp::ModelId,
|
||||
pub name: SharedString,
|
||||
pub description: Option<SharedString>,
|
||||
pub icon: Option<AgentModelIcon>,
|
||||
pub icon: Option<IconName>,
|
||||
}
|
||||
|
||||
impl From<acp::ModelInfo> for AgentModelInfo {
|
||||
|
||||
@@ -6,7 +6,7 @@ use futures::{FutureExt, StreamExt, channel::mpsc};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language::{Anchor, Buffer, BufferEvent, Point, ToPoint};
|
||||
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
|
||||
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
|
||||
use std::{cmp, ops::Range, sync::Arc};
|
||||
use text::{Edit, Patch, Rope};
|
||||
@@ -150,7 +150,7 @@ impl ActionLog {
|
||||
if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.is_some_and(|file| file.disk_state().is_deleted())
|
||||
.is_some_and(|file| file.disk_state() == DiskState::Deleted)
|
||||
{
|
||||
// If the buffer had been edited by a tool, but it got
|
||||
// deleted externally, we want to stop tracking it.
|
||||
@@ -162,7 +162,7 @@ impl ActionLog {
|
||||
if buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.is_some_and(|file| !file.disk_state().is_deleted())
|
||||
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
|
||||
{
|
||||
// If the buffer had been deleted by a tool, but it got
|
||||
// resurrected externally, we want to clear the edits we
|
||||
@@ -769,7 +769,7 @@ impl ActionLog {
|
||||
tracked.version != buffer.version
|
||||
&& buffer
|
||||
.file()
|
||||
.is_some_and(|file| !file.disk_state().is_deleted())
|
||||
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
|
||||
})
|
||||
.map(|(buffer, _)| buffer)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ use futures::{StreamExt, future};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language_model::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use prompt_store::{
|
||||
ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
|
||||
@@ -93,7 +93,7 @@ impl LanguageModels {
|
||||
fn refresh_list(&mut self, cx: &App) {
|
||||
let providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -153,10 +153,7 @@ impl LanguageModels {
|
||||
id: Self::model_id(model),
|
||||
name: model.name().0,
|
||||
description: None,
|
||||
icon: Some(match provider.icon() {
|
||||
IconOrSvg::Svg(path) => acp_thread::AgentModelIcon::Path(path),
|
||||
IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),
|
||||
}),
|
||||
icon: Some(provider.icon()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +164,7 @@ impl LanguageModels {
|
||||
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -429,7 +426,7 @@ impl NativeAgent {
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(UserRulesContext {
|
||||
uuid: prompt_metadata.id.as_user()?,
|
||||
uuid: prompt_metadata.id.user_id()?,
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
@@ -1633,9 +1630,7 @@ mod internal_tests {
|
||||
id: acp::ModelId::new("fake/fake"),
|
||||
name: "Fake".into(),
|
||||
description: None,
|
||||
icon: Some(acp_thread::AgentModelIcon::Named(
|
||||
ui::IconName::ZedAssistant
|
||||
)),
|
||||
icon: Some(ui::IconName::ZedAssistant),
|
||||
}]
|
||||
)])
|
||||
);
|
||||
|
||||
@@ -216,10 +216,14 @@ impl HistoryStore {
|
||||
}
|
||||
|
||||
pub fn reload(&self, cx: &mut Context<Self>) {
|
||||
let database_connection = ThreadsDatabase::connect(cx);
|
||||
let database_future = ThreadsDatabase::connect(cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let database = database_connection.await;
|
||||
let threads = database.map_err(|err| anyhow!(err))?.list_threads().await?;
|
||||
let threads = database_future
|
||||
.await
|
||||
.map_err(|err| anyhow!(err))?
|
||||
.list_threads()
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES {
|
||||
for thread in threads
|
||||
@@ -340,8 +344,7 @@ impl HistoryStore {
|
||||
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
|
||||
cx.background_spawn(async move {
|
||||
if cfg!(any(feature = "test-support", test)) {
|
||||
log::warn!("history store does not persist in tests");
|
||||
return Ok(VecDeque::new());
|
||||
anyhow::bail!("history store does not persist in tests");
|
||||
}
|
||||
let json = KEY_VALUE_STORE
|
||||
.read_kvp(RECENTLY_OPENED_THREADS_KEY)?
|
||||
|
||||
@@ -13,7 +13,7 @@ path = "src/agent_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support", "agent/test-support"]
|
||||
test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support"]
|
||||
unit-eval = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -34,7 +34,7 @@ use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::{Chat, PasteRaw};
|
||||
use zed_actions::agent::Chat;
|
||||
|
||||
pub struct MessageEditor {
|
||||
mention_set: Entity<MentionSet>,
|
||||
@@ -543,9 +543,6 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
@@ -556,127 +553,133 @@ impl MessageEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
// 2. Have an associated file path
|
||||
// 3. Span multiple lines (not single-line selections)
|
||||
// 4. Belong to a file that exists in the current project
|
||||
let should_insert_creases = util::maybe!({
|
||||
let selections = editor_clipboard_selections.as_ref()?;
|
||||
if selections.len() > 1 {
|
||||
return Some(false);
|
||||
}
|
||||
let selection = selections.first()?;
|
||||
let file_path = selection.file_path.as_ref()?;
|
||||
let line_range = selection.line_range.as_ref()?;
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
|
||||
if line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_insert_creases && let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
let insertion_target = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let crease_text =
|
||||
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||
|
||||
let mention_uri = MentionUri::Selection {
|
||||
abs_path: Some(file_path.clone()),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
let (excerpt_id, text_anchor, content_len) =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
|
||||
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(*excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
crease_text.into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = cx
|
||||
.spawn({
|
||||
let project = project.clone();
|
||||
async move |_, cx| {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&file_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "project path not found".to_string())?;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let start = Point::new(*line_range.start(), 0)
|
||||
.min(buffer.max_point());
|
||||
let end = Point::new(*line_range.end() + 1, 0)
|
||||
.min(buffer.max_point());
|
||||
let content = buffer.text_for_range(start..end).collect();
|
||||
Mention::Text {
|
||||
content,
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _cx| {
|
||||
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||
});
|
||||
if has_file_context {
|
||||
if let Some((workspace, selections)) =
|
||||
self.workspace.upgrade().zip(editor_clipboard_selections)
|
||||
{
|
||||
let Some(first_selection) = selections.first() else {
|
||||
return;
|
||||
};
|
||||
if let Some(file_path) = &first_selection.file_path {
|
||||
// In case someone pastes selections from another window
|
||||
// with a different project, we don't want to insert the
|
||||
// crease (containing the absolute path) since the agent
|
||||
// cannot access files outside the project.
|
||||
let is_in_project = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some();
|
||||
if !is_in_project {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cx.stop_propagation();
|
||||
let insertion_target = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.start
|
||||
.text_anchor;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let crease_text =
|
||||
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
|
||||
|
||||
let mention_uri = MentionUri::Selection {
|
||||
abs_path: Some(file_path.clone()),
|
||||
line_range: line_range.clone(),
|
||||
};
|
||||
|
||||
let mention_text = mention_uri.as_link().to_string();
|
||||
let (excerpt_id, text_anchor, content_len) =
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let (excerpt_id, _, buffer_snapshot) =
|
||||
snapshot.as_singleton().unwrap();
|
||||
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
|
||||
|
||||
editor.insert(&mention_text, window, cx);
|
||||
editor.insert(" ", window, cx);
|
||||
|
||||
(*excerpt_id, text_anchor, mention_text.len())
|
||||
});
|
||||
|
||||
let Some((crease_id, tx)) = insert_crease_for_mention(
|
||||
excerpt_id,
|
||||
text_anchor,
|
||||
content_len,
|
||||
crease_text.into(),
|
||||
mention_uri.icon_path(cx),
|
||||
None,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
drop(tx);
|
||||
|
||||
let mention_task = cx
|
||||
.spawn({
|
||||
let project = project.clone();
|
||||
async move |_, cx| {
|
||||
let project_path = project
|
||||
.update(cx, |project, cx| {
|
||||
project.project_path_for_absolute_path(&file_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "project path not found".to_string())?;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path, cx)
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
buffer
|
||||
.update(cx, |buffer, cx| {
|
||||
let start = Point::new(*line_range.start(), 0)
|
||||
.min(buffer.max_point());
|
||||
let end = Point::new(*line_range.end() + 1, 0)
|
||||
.min(buffer.max_point());
|
||||
let content =
|
||||
buffer.text_for_range(start..end).collect();
|
||||
Mention::Text {
|
||||
content,
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
}
|
||||
})
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
self.mention_set.update(cx, |mention_set, _cx| {
|
||||
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if self.prompt_capabilities.borrow().image
|
||||
@@ -687,13 +690,6 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let editor = self.editor.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn insert_dragged_files(
|
||||
&mut self,
|
||||
paths: Vec<project::ProjectPath>,
|
||||
@@ -971,7 +967,6 @@ impl Render for MessageEditor {
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::chat_with_follow))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::paste_raw))
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.flex_1()
|
||||
.child({
|
||||
|
||||
@@ -186,17 +186,6 @@ impl Render for ModeSelector {
|
||||
move |_window, cx| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Toggle Mode Menu"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pb_1()
|
||||
@@ -211,6 +200,17 @@ impl Render for ModeSelector {
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Toggle Mode Menu"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use agent_client_protocol::ModelId;
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
@@ -221,7 +221,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let favorites = if self.selector.supports_favorites() {
|
||||
AgentSettings::get_global(cx).favorite_model_ids()
|
||||
Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
@@ -242,7 +242,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries =
|
||||
info_list_to_picker_entries(filtered_models, &favorites);
|
||||
info_list_to_picker_entries(filtered_models, favorites);
|
||||
// Finds the currently selected model in the list
|
||||
let new_index = this
|
||||
.delegate
|
||||
@@ -350,11 +350,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
})
|
||||
.child(
|
||||
ModelSelectorListItem::new(ix, model_info.name.clone())
|
||||
.map(|this| match &model_info.icon {
|
||||
Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),
|
||||
Some(AgentModelIcon::Named(icon)) => this.icon(*icon),
|
||||
None => this,
|
||||
})
|
||||
.when_some(model_info.icon, |this, icon| this.icon(icon))
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.when(supports_favorites, |this| {
|
||||
@@ -410,7 +406,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
|
||||
fn info_list_to_picker_entries(
|
||||
model_list: AgentModelList,
|
||||
favorites: &HashSet<ModelId>,
|
||||
favorites: Arc<HashSet<ModelId>>,
|
||||
) -> Vec<AcpModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
@@ -576,11 +572,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
|
||||
models
|
||||
.into_iter()
|
||||
.map(|m| ModelId::new(m.to_string()))
|
||||
.collect()
|
||||
fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
|
||||
Arc::new(
|
||||
models
|
||||
.into_iter()
|
||||
.map(|m| ModelId::new(m.to_string()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
|
||||
@@ -611,7 +609,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
@@ -627,7 +625,7 @@ mod tests {
|
||||
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
|
||||
let favorites = create_favorites(vec![]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
@@ -643,7 +641,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/claude"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
|
||||
for entry in &entries {
|
||||
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
|
||||
@@ -664,7 +662,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
let model_ids = get_entry_model_ids(&entries);
|
||||
|
||||
assert_eq!(model_ids[0], "zed/gemini");
|
||||
@@ -685,7 +683,7 @@ mod tests {
|
||||
|
||||
let favorites = create_favorites(vec!["zed/claude"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
let labels = get_entry_labels(&entries);
|
||||
|
||||
assert_eq!(
|
||||
@@ -725,7 +723,7 @@ mod tests {
|
||||
]);
|
||||
let favorites = create_favorites(vec!["zed/gemini"]);
|
||||
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
let entries = info_list_to_picker_entries(models, favorites);
|
||||
|
||||
assert!(matches!(
|
||||
entries.first(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
|
||||
use acp_thread::{AgentModelInfo, AgentModelSelector};
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use fs::Fs;
|
||||
@@ -70,7 +70,7 @@ impl Render for AcpModelSelectorPopover {
|
||||
.map(|model| model.name.clone())
|
||||
.unwrap_or_else(|| SharedString::from("Select a Model"));
|
||||
|
||||
let model_icon = model.as_ref().and_then(|model| model.icon.clone());
|
||||
let model_icon = model.as_ref().and_then(|model| model.icon);
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
@@ -125,14 +125,7 @@ impl Render for AcpModelSelectorPopover {
|
||||
ButtonLike::new("active-model")
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.when_some(model_icon, |this, icon| {
|
||||
this.child(
|
||||
match icon {
|
||||
AgentModelIcon::Path(path) => Icon::from_external_svg(path),
|
||||
AgentModelIcon::Named(icon_name) => Icon::new(icon_name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
})
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::acp::AcpThreadView;
|
||||
use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -402,22 +402,7 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
|
||||
let display_text = match format {
|
||||
EntryTimeFormat::DateAndTime => {
|
||||
let entry_time = entry.updated_at();
|
||||
let now = Utc::now();
|
||||
let duration = now.signed_duration_since(entry_time);
|
||||
let days = duration.num_days();
|
||||
|
||||
format!("{}d", days)
|
||||
}
|
||||
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
|
||||
};
|
||||
|
||||
let title = entry.title().clone();
|
||||
let full_date =
|
||||
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -438,14 +423,11 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(display_text)
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
|
||||
})
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = Some(ix);
|
||||
|
||||
@@ -34,7 +34,7 @@ use language::Buffer;
|
||||
|
||||
use language_model::LanguageModelRegistry;
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
|
||||
use project::{Project, ProjectEntryId};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
use rope::Point;
|
||||
use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
|
||||
@@ -260,7 +260,6 @@ impl ThreadFeedbackState {
|
||||
|
||||
pub struct AcpThreadView {
|
||||
agent: Rc<dyn AgentServer>,
|
||||
agent_server_store: Entity<AgentServerStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
thread_state: ThreadState,
|
||||
@@ -338,13 +337,7 @@ impl AcpThreadView {
|
||||
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
|
||||
let available_commands = Rc::new(RefCell::new(vec![]));
|
||||
|
||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||
let agent_display_name = agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(agent.name()))
|
||||
.unwrap_or_else(|| agent.name());
|
||||
|
||||
let placeholder = placeholder_text(agent_display_name.as_ref(), false);
|
||||
let placeholder = placeholder_text(agent.name().as_ref(), false);
|
||||
|
||||
let message_editor = cx.new(|cx| {
|
||||
let mut editor = MessageEditor::new(
|
||||
@@ -383,6 +376,7 @@ impl AcpThreadView {
|
||||
)
|
||||
});
|
||||
|
||||
let agent_server_store = project.read(cx).agent_server_store().clone();
|
||||
let subscriptions = [
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
|
||||
cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
|
||||
@@ -412,7 +406,6 @@ impl AcpThreadView {
|
||||
|
||||
Self {
|
||||
agent: agent.clone(),
|
||||
agent_server_store,
|
||||
workspace: workspace.clone(),
|
||||
project: project.clone(),
|
||||
entry_view_state,
|
||||
@@ -744,7 +737,7 @@ impl AcpThreadView {
|
||||
cx: &mut App,
|
||||
) {
|
||||
let agent_name = agent.name();
|
||||
let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
|
||||
let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
|
||||
let sub = window.subscribe(®istry, cx, {
|
||||
@@ -786,6 +779,7 @@ impl AcpThreadView {
|
||||
configuration_view,
|
||||
description: err
|
||||
.description
|
||||
.clone()
|
||||
.map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
|
||||
_subscription: subscription,
|
||||
};
|
||||
@@ -1094,7 +1088,10 @@ impl AcpThreadView {
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired::new(),
|
||||
AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
@@ -1503,13 +1500,7 @@ impl AcpThreadView {
|
||||
let has_commands = !available_commands.is_empty();
|
||||
self.available_commands.replace(available_commands);
|
||||
|
||||
let agent_display_name = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
|
||||
.unwrap_or_else(|| self.agent.name());
|
||||
|
||||
let new_placeholder = placeholder_text(agent_display_name.as_ref(), has_commands);
|
||||
let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands);
|
||||
|
||||
self.message_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text(&new_placeholder, window, cx);
|
||||
@@ -1672,6 +1663,44 @@ impl AcpThreadView {
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if method.0.as_ref() == "anthropic-api-key" {
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
let provider = registry
|
||||
.read(cx)
|
||||
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
|
||||
.unwrap();
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
if !provider.is_authenticated(cx) {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
description: Some("ANTHROPIC_API_KEY must be set".to_owned()),
|
||||
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
this.project.clone(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else if method.0.as_ref() == "vertex-ai"
|
||||
&& std::env::var("GOOGLE_API_KEY").is_err()
|
||||
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
|
||||
@@ -2124,7 +2153,6 @@ impl AcpThreadView {
|
||||
chunks,
|
||||
indented: _,
|
||||
}) => {
|
||||
let mut is_blank = true;
|
||||
let is_last = entry_ix + 1 == total_entries;
|
||||
|
||||
let style = default_markdown_style(false, false, window, cx);
|
||||
@@ -2134,55 +2162,36 @@ impl AcpThreadView {
|
||||
.children(chunks.iter().enumerate().filter_map(
|
||||
|(chunk_ix, chunk)| match chunk {
|
||||
AssistantMessageChunk::Message { block } => {
|
||||
block.markdown().and_then(|md| {
|
||||
let this_is_blank = md.read(cx).source().trim().is_empty();
|
||||
is_blank = is_blank && this_is_blank;
|
||||
if this_is_blank {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_markdown(md.clone(), style.clone())
|
||||
.into_any_element(),
|
||||
)
|
||||
block.markdown().map(|md| {
|
||||
self.render_markdown(md.clone(), style.clone())
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
AssistantMessageChunk::Thought { block } => {
|
||||
block.markdown().and_then(|md| {
|
||||
let this_is_blank = md.read(cx).source().trim().is_empty();
|
||||
is_blank = is_blank && this_is_blank;
|
||||
if this_is_blank {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_thinking_block(
|
||||
entry_ix,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
block.markdown().map(|md| {
|
||||
self.render_thinking_block(
|
||||
entry_ix,
|
||||
chunk_ix,
|
||||
md.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
},
|
||||
))
|
||||
.into_any();
|
||||
|
||||
if is_blank {
|
||||
Empty.into_any()
|
||||
} else {
|
||||
v_flex()
|
||||
.px_5()
|
||||
.py_1p5()
|
||||
.when(is_last, |this| this.pb_4())
|
||||
.w_full()
|
||||
.text_ui(cx)
|
||||
.child(message_body)
|
||||
.into_any()
|
||||
}
|
||||
v_flex()
|
||||
.px_5()
|
||||
.py_1p5()
|
||||
.when(is_first_indented, |this| this.pt_0p5())
|
||||
.when(is_last, |this| this.pb_4())
|
||||
.w_full()
|
||||
.text_ui(cx)
|
||||
.child(message_body)
|
||||
.into_any()
|
||||
}
|
||||
AgentThreadEntry::ToolCall(tool_call) => {
|
||||
let has_terminals = tool_call.terminals().next().is_some();
|
||||
@@ -2214,7 +2223,7 @@ impl AcpThreadView {
|
||||
div()
|
||||
.relative()
|
||||
.w_full()
|
||||
.pl_5()
|
||||
.pl(rems_from_px(20.0))
|
||||
.bg(cx.theme().colors().panel_background.opacity(0.2))
|
||||
.child(
|
||||
div()
|
||||
@@ -2431,12 +2440,6 @@ impl AcpThreadView {
|
||||
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
||||
|
||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
let input_output_header = |label: SharedString| {
|
||||
Label::new(label)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx)
|
||||
};
|
||||
|
||||
let tool_output_display =
|
||||
if is_open {
|
||||
@@ -2478,25 +2481,7 @@ impl AcpThreadView {
|
||||
| ToolCallStatus::Completed
|
||||
| ToolCallStatus::Failed
|
||||
| ToolCallStatus::Canceled => v_flex()
|
||||
.when(!is_edit && !is_terminal_tool, |this| {
|
||||
this.mt_1p5().w_full().child(
|
||||
v_flex()
|
||||
.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.pb_1()
|
||||
.gap_1()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(input_output_header("Raw Input:".into()))
|
||||
.children(tool_call.raw_input_markdown.clone().map(|input| {
|
||||
self.render_markdown(
|
||||
input,
|
||||
default_markdown_style(false, false, window, cx),
|
||||
)
|
||||
}))
|
||||
.child(input_output_header("Output:".into())),
|
||||
)
|
||||
})
|
||||
.w_full()
|
||||
.children(tool_call.content.iter().enumerate().map(
|
||||
|(content_ix, content)| {
|
||||
div().child(self.render_tool_call_content(
|
||||
@@ -2595,7 +2580,7 @@ impl AcpThreadView {
|
||||
.gap_px()
|
||||
.when(is_collapsible, |this| {
|
||||
this.child(
|
||||
Disclosure::new(("expand-output", entry_ix), is_open)
|
||||
Disclosure::new(("expand", entry_ix), is_open)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.visible_on_hover(&card_header_id)
|
||||
@@ -2718,7 +2703,7 @@ impl AcpThreadView {
|
||||
..default_markdown_style(false, true, window, cx)
|
||||
},
|
||||
))
|
||||
.tooltip(Tooltip::text("Go to File"))
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.open_tool_call_location(entry_ix, 0, window, cx);
|
||||
}))
|
||||
@@ -2781,20 +2766,20 @@ impl AcpThreadView {
|
||||
let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id));
|
||||
|
||||
v_flex()
|
||||
.mt_1p5()
|
||||
.gap_2()
|
||||
.map(|this| {
|
||||
if card_layout {
|
||||
this.when(context_ix > 0, |this| {
|
||||
this.pt_2()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
})
|
||||
} else {
|
||||
this.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.border_l_1()
|
||||
.when(!card_layout, |this| {
|
||||
this.ml(rems(0.4))
|
||||
.px_3p5()
|
||||
.border_l_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
})
|
||||
.when(card_layout, |this| {
|
||||
this.px_2().pb_2().when(context_ix > 0, |this| {
|
||||
this.border_t_1()
|
||||
.pt_2()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
}
|
||||
})
|
||||
})
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
@@ -3515,119 +3500,138 @@ impl AcpThreadView {
|
||||
pending_auth_method: Option<&acp::AuthMethodId>,
|
||||
window: &mut Window,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
) -> Div {
|
||||
let show_description =
|
||||
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
|
||||
|
||||
let auth_methods = connection.auth_methods();
|
||||
|
||||
let agent_display_name = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.agent_display_name(&ExternalAgentServerName(self.agent.name()))
|
||||
.unwrap_or_else(|| self.agent.name());
|
||||
|
||||
let show_fallback_description = auth_methods.len() > 1
|
||||
&& configuration_view.is_none()
|
||||
&& description.is_none()
|
||||
&& pending_auth_method.is_none();
|
||||
|
||||
let auth_buttons = || {
|
||||
h_flex().justify_end().flex_wrap().gap_1().children(
|
||||
connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.map(|(ix, method)| {
|
||||
let (method_id, name) = if self.project.read(cx).is_via_remote_server()
|
||||
&& method.id.0.as_ref() == "oauth-personal"
|
||||
&& method.name == "Log in with Google"
|
||||
{
|
||||
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
|
||||
} else {
|
||||
(method.id.0.clone(), method.name.clone())
|
||||
};
|
||||
|
||||
let agent_telemetry_id = connection.telemetry_id();
|
||||
|
||||
Button::new(method_id.clone(), name)
|
||||
.label_size(LabelSize::Small)
|
||||
.map(|this| {
|
||||
if ix == 0 {
|
||||
this.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
} else {
|
||||
this.style(ButtonStyle::Outlined)
|
||||
}
|
||||
})
|
||||
.when_some(method.description.clone(), |this, description| {
|
||||
this.tooltip(Tooltip::text(description))
|
||||
})
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = agent_telemetry_id,
|
||||
method = method_id
|
||||
);
|
||||
|
||||
this.authenticate(
|
||||
acp::AuthMethodId::new(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
if pending_auth_method.is_some() {
|
||||
return Callout::new()
|
||||
.icon(IconName::Info)
|
||||
.title(format!("Authenticating to {}…", agent_display_name))
|
||||
.actions_slot(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
v_flex().flex_1().size_full().justify_end().child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
.pr_3()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().status().warning.opacity(0.04))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.color(Color::Warning)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(Label::new("Authentication Required").size(LabelSize::Small)),
|
||||
)
|
||||
.into_any_element();
|
||||
}
|
||||
.children(description.map(|desc| {
|
||||
div().text_ui(cx).child(self.render_markdown(
|
||||
desc.clone(),
|
||||
default_markdown_style(false, false, window, cx),
|
||||
))
|
||||
}))
|
||||
.children(
|
||||
configuration_view
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.when(show_description, |el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}.{}",
|
||||
self.agent.name(),
|
||||
if auth_methods.len() > 1 {
|
||||
" Please choose one of the following options:"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
})
|
||||
.when_some(pending_auth_method, |el, _| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.py_4()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
.child(Label::new("Authenticating…").size(LabelSize::Small)),
|
||||
)
|
||||
})
|
||||
.when(!auth_methods.is_empty(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.when(!show_description, |this| {
|
||||
this.border_t_1()
|
||||
.mt_1()
|
||||
.pt_2()
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
})
|
||||
.children(connection.auth_methods().iter().enumerate().rev().map(
|
||||
|(ix, method)| {
|
||||
let (method_id, name) = if self
|
||||
.project
|
||||
.read(cx)
|
||||
.is_via_remote_server()
|
||||
&& method.id.0.as_ref() == "oauth-personal"
|
||||
&& method.name == "Log in with Google"
|
||||
{
|
||||
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
|
||||
} else {
|
||||
(method.id.0.clone(), method.name.clone())
|
||||
};
|
||||
|
||||
Callout::new()
|
||||
.icon(IconName::Info)
|
||||
.title(format!("Authenticate to {}", agent_display_name))
|
||||
.when(auth_methods.len() == 1, |this| {
|
||||
this.actions_slot(auth_buttons())
|
||||
})
|
||||
.description_slot(
|
||||
v_flex()
|
||||
.text_ui(cx)
|
||||
.map(|this| {
|
||||
if show_fallback_description {
|
||||
this.child(
|
||||
Label::new("Choose one of the following authentication options:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
this.children(
|
||||
configuration_view
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.children(description.map(|desc| {
|
||||
self.render_markdown(
|
||||
desc.clone(),
|
||||
default_markdown_style(false, false, window, cx),
|
||||
)
|
||||
}))
|
||||
}
|
||||
})
|
||||
.when(auth_methods.len() > 1, |this| {
|
||||
this.gap_1().child(auth_buttons())
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
let agent_telemetry_id = connection.telemetry_id();
|
||||
|
||||
Button::new(method_id.clone(), name)
|
||||
.label_size(LabelSize::Small)
|
||||
.map(|this| {
|
||||
if ix == 0 {
|
||||
this.style(ButtonStyle::Tinted(TintColor::Warning))
|
||||
} else {
|
||||
this.style(ButtonStyle::Outlined)
|
||||
}
|
||||
})
|
||||
.when_some(
|
||||
method.description.clone(),
|
||||
|this, description| {
|
||||
this.tooltip(Tooltip::text(description))
|
||||
},
|
||||
)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = agent_telemetry_id,
|
||||
method = method_id
|
||||
);
|
||||
|
||||
this.authenticate(
|
||||
acp::AuthMethodId::new(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_load_error(
|
||||
@@ -5876,6 +5880,10 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let err = AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
};
|
||||
this.clear_thread_error(cx);
|
||||
if let Some(message) = this.in_flight_prompt.take() {
|
||||
this.message_editor.update(cx, |editor, cx| {
|
||||
@@ -5884,14 +5892,7 @@ impl AcpThreadView {
|
||||
}
|
||||
let this = cx.weak_entity();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired::new(),
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
})
|
||||
}
|
||||
}))
|
||||
@@ -5904,10 +5905,14 @@ impl AcpThreadView {
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
let err = AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
};
|
||||
self.clear_thread_error(cx);
|
||||
let this = cx.weak_entity();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(this, AuthRequired::new(), agent, connection, window, cx);
|
||||
Self::handle_auth_required(this, err, agent, connection, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6010,19 +6015,16 @@ impl Render for AcpThreadView {
|
||||
configuration_view,
|
||||
pending_auth_method,
|
||||
..
|
||||
} => v_flex()
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.justify_end()
|
||||
.child(self.render_auth_required_state(
|
||||
} => self
|
||||
.render_auth_required_state(
|
||||
connection,
|
||||
description.as_ref(),
|
||||
configuration_view.as_ref(),
|
||||
pending_auth_method.as_ref(),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any(),
|
||||
ThreadState::Loading { .. } => v_flex()
|
||||
.flex_1()
|
||||
.child(self.render_recent_history(cx))
|
||||
|
||||
@@ -22,8 +22,7 @@ use gpui::{
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_models::AllLanguageModelSettings;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
@@ -118,7 +117,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
for provider in providers {
|
||||
self.add_provider_configuration_view(&provider, window, cx);
|
||||
}
|
||||
@@ -262,12 +261,9 @@ impl AgentConfiguration {
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
match provider.icon() {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
Icon::new(provider.icon())
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -420,7 +416,7 @@ impl AgentConfiguration {
|
||||
&mut self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
|
||||
let popover_menu = PopoverMenu::new("add-provider-popover")
|
||||
.trigger(
|
||||
|
||||
@@ -17,7 +17,7 @@ use gpui::{
|
||||
Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
};
|
||||
|
||||
use language::{Buffer, Capability, OffsetRangeExt, Point};
|
||||
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
||||
use multi_buffer::PathKey;
|
||||
use project::{Project, ProjectItem, ProjectPath};
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -192,7 +192,7 @@ impl AgentDiffPane {
|
||||
&& buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.is_some_and(|file| file.disk_state().is_deleted())
|
||||
.is_some_and(|file| file.disk_state() == DiskState::Deleted)
|
||||
{
|
||||
editor.fold_buffer(snapshot.text.remote_id(), cx)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use crate::{
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
use language_model::IconOrSvg;
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
@@ -104,14 +103,7 @@ impl Render for AgentModelSelector {
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.when_some(provider_icon, |this, icon| {
|
||||
this.child(
|
||||
match icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
})
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
@@ -123,7 +115,7 @@ impl Render for AgentModelSelector {
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
|
||||
@@ -2428,7 +2428,7 @@ impl AgentPanel {
|
||||
let history_is_empty = self.history_store.read(cx).is_empty(cx);
|
||||
|
||||
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.any(|provider| {
|
||||
provider.is_authenticated(cx)
|
||||
|
||||
@@ -348,8 +348,7 @@ fn init_language_model_settings(cx: &mut App) {
|
||||
|_, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
update_active_language_model_from_settings(cx);
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -75,9 +75,6 @@ pub struct BufferCodegen {
|
||||
session_id: Uuid,
|
||||
}
|
||||
|
||||
pub const REWRITE_SECTION_TOOL_NAME: &str = "rewrite_section";
|
||||
pub const FAILURE_MESSAGE_TOOL_NAME: &str = "failure_message";
|
||||
|
||||
impl BufferCodegen {
|
||||
pub fn new(
|
||||
buffer: Entity<MultiBuffer>,
|
||||
@@ -525,12 +522,12 @@ impl CodegenAlternative {
|
||||
|
||||
let tools = vec![
|
||||
LanguageModelRequestTool {
|
||||
name: REWRITE_SECTION_TOOL_NAME.to_string(),
|
||||
name: "rewrite_section".to_string(),
|
||||
description: "Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
|
||||
},
|
||||
LanguageModelRequestTool {
|
||||
name: FAILURE_MESSAGE_TOOL_NAME.to_string(),
|
||||
name: "failure_message".to_string(),
|
||||
description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
|
||||
input_schema: language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
|
||||
},
|
||||
@@ -1170,7 +1167,7 @@ impl CodegenAlternative {
|
||||
let process_tool_use = move |tool_use: LanguageModelToolUse| -> Option<ToolUseOutput> {
|
||||
let mut chars_read_so_far = chars_read_so_far.lock();
|
||||
match tool_use.name.as_ref() {
|
||||
REWRITE_SECTION_TOOL_NAME => {
|
||||
"rewrite_section" => {
|
||||
let Ok(input) =
|
||||
serde_json::from_value::<RewriteSectionInput>(tool_use.input)
|
||||
else {
|
||||
@@ -1183,7 +1180,7 @@ impl CodegenAlternative {
|
||||
description: None,
|
||||
})
|
||||
}
|
||||
FAILURE_MESSAGE_TOOL_NAME => {
|
||||
"failure_message" => {
|
||||
let Ok(mut input) =
|
||||
serde_json::from_value::<FailureMessageInput>(tool_use.input)
|
||||
else {
|
||||
@@ -1496,10 +1493,7 @@ mod tests {
|
||||
use indoc::indoc;
|
||||
use language::{Buffer, Point};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use language_model::{
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelRegistry,
|
||||
LanguageModelToolUse, StopReason, TokenUsage,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use languages::rust_lang;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
@@ -1811,51 +1805,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// When not streaming tool calls, we strip backticks as part of parsing the model's
|
||||
// plain text response. This is a regression test for a bug where we stripped
|
||||
// backticks incorrectly.
|
||||
#[gpui::test]
|
||||
async fn test_allows_model_to_output_backticks(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let text = "- Improved; `cmd+click` behavior. Now requires `cmd` to be pressed before the click starts or it doesn't run. ([#44579](https://github.com/zed-industries/zed/pull/44579); thanks [Zachiah](https://github.com/Zachiah))";
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(0, 0))
|
||||
});
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let codegen = cx.new(|cx| {
|
||||
CodegenAlternative::new(
|
||||
buffer.clone(),
|
||||
range.clone(),
|
||||
true,
|
||||
prompt_builder,
|
||||
Uuid::new_v4(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let events_tx = simulate_tool_based_completion(&codegen, cx);
|
||||
let chunk_len = text.find('`').unwrap();
|
||||
events_tx
|
||||
.unbounded_send(rewrite_tool_use("tool_1", &text[..chunk_len], false))
|
||||
.unwrap();
|
||||
events_tx
|
||||
.unbounded_send(rewrite_tool_use("tool_2", &text, true))
|
||||
.unwrap();
|
||||
events_tx
|
||||
.unbounded_send(LanguageModelCompletionEvent::Stop(StopReason::EndTurn))
|
||||
.unwrap();
|
||||
drop(events_tx);
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
text
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_chunks("Lorem ipsum dolor", "Lorem ipsum dolor").await;
|
||||
@@ -1921,39 +1870,4 @@ mod tests {
|
||||
});
|
||||
chunks_tx
|
||||
}
|
||||
|
||||
fn simulate_tool_based_completion(
|
||||
codegen: &Entity<CodegenAlternative>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> mpsc::UnboundedSender<LanguageModelCompletionEvent> {
|
||||
let (events_tx, events_rx) = mpsc::unbounded();
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
codegen.update(cx, |codegen, cx| {
|
||||
let completion_stream = Task::ready(Ok(events_rx.map(Ok).boxed()
|
||||
as BoxStream<
|
||||
'static,
|
||||
Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>));
|
||||
codegen.generation = codegen.handle_completion(model, completion_stream, cx);
|
||||
});
|
||||
events_tx
|
||||
}
|
||||
|
||||
fn rewrite_tool_use(
|
||||
id: &str,
|
||||
replacement_text: &str,
|
||||
is_complete: bool,
|
||||
) -> LanguageModelCompletionEvent {
|
||||
let input = RewriteSectionInput {
|
||||
replacement_text: replacement_text.into(),
|
||||
};
|
||||
LanguageModelCompletionEvent::ToolUse(LanguageModelToolUse {
|
||||
id: id.into(),
|
||||
name: REWRITE_SECTION_TOOL_NAME.into(),
|
||||
raw_input: serde_json::to_string(&input).unwrap(),
|
||||
input: serde_json::to_value(&input).unwrap(),
|
||||
is_input_complete: is_complete,
|
||||
thought_signature: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1586,7 +1586,7 @@ pub(crate) fn search_rules(
|
||||
None
|
||||
} else {
|
||||
Some(RulesContextEntry {
|
||||
prompt_id: metadata.id.as_user()?,
|
||||
prompt_id: metadata.id.user_id()?,
|
||||
title: metadata.title?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1259,26 +1259,28 @@ impl InlineAssistant {
|
||||
let bottom = top + 1.0;
|
||||
(top, bottom)
|
||||
});
|
||||
let height_in_lines = editor.visible_line_count().unwrap_or(0.);
|
||||
let vertical_scroll_margin = editor.vertical_scroll_margin() as ScrollOffset;
|
||||
let scroll_target_top = (scroll_target_range.0 - vertical_scroll_margin)
|
||||
// Don't scroll up too far in the case of a large vertical_scroll_margin.
|
||||
.max(scroll_target_range.0 - height_in_lines / 2.0);
|
||||
let scroll_target_bottom = (scroll_target_range.1 + vertical_scroll_margin)
|
||||
// Don't scroll down past where the top would still be visible.
|
||||
.min(scroll_target_top + height_in_lines);
|
||||
let mut scroll_target_top = scroll_target_range.0;
|
||||
let mut scroll_target_bottom = scroll_target_range.1;
|
||||
|
||||
scroll_target_top -= editor.vertical_scroll_margin() as ScrollOffset;
|
||||
scroll_target_bottom += editor.vertical_scroll_margin() as ScrollOffset;
|
||||
|
||||
let height_in_lines = editor.visible_line_count().unwrap_or(0.);
|
||||
let scroll_top = editor.scroll_position(cx).y;
|
||||
let scroll_bottom = scroll_top + height_in_lines;
|
||||
|
||||
if scroll_target_top < scroll_top {
|
||||
editor.set_scroll_position(point(0., scroll_target_top), window, cx);
|
||||
} else if scroll_target_bottom > scroll_bottom {
|
||||
editor.set_scroll_position(
|
||||
point(0., scroll_target_bottom - height_in_lines),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if (scroll_target_bottom - scroll_target_top) <= height_in_lines {
|
||||
editor.set_scroll_position(
|
||||
point(0., scroll_target_bottom - height_in_lines),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
editor.set_scroll_position(point(0., scroll_target_top), window, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2269,36 +2271,6 @@ pub mod evals {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(feature = "unit-eval"), ignore)]
|
||||
fn eval_empty_buffer() {
|
||||
run_eval(
|
||||
20,
|
||||
1.0,
|
||||
"Write a Python hello, world program".to_string(),
|
||||
"ˇ".to_string(),
|
||||
|output| match output {
|
||||
InlineAssistantOutput::Success {
|
||||
full_buffer_text, ..
|
||||
} => {
|
||||
if full_buffer_text.is_empty() {
|
||||
EvalOutput::failed("expected some output".to_string())
|
||||
} else {
|
||||
EvalOutput::passed(format!("Produced {full_buffer_text}"))
|
||||
}
|
||||
}
|
||||
o @ InlineAssistantOutput::Failure { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
o @ InlineAssistantOutput::Malformed { .. } => EvalOutput::failed(format!(
|
||||
"Assistant output does not match expected output: {:?}",
|
||||
o
|
||||
)),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn run_eval(
|
||||
iterations: usize,
|
||||
expected_pass_ratio: f32,
|
||||
|
||||
@@ -7,8 +7,8 @@ use gpui::{
|
||||
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProvider,
|
||||
LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
@@ -55,7 +55,7 @@ pub fn language_model_selector(
|
||||
|
||||
fn all_models(cx: &App) -> GroupedModels {
|
||||
let lm_registry = LanguageModelRegistry::global(cx).read(cx);
|
||||
let providers = lm_registry.visible_providers();
|
||||
let providers = lm_registry.providers();
|
||||
|
||||
let mut favorites_index = FavoritesIndex::default();
|
||||
|
||||
@@ -94,7 +94,7 @@ type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>
|
||||
#[derive(Clone)]
|
||||
struct ModelInfo {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
icon: IconOrSvg,
|
||||
icon: IconName,
|
||||
is_favorite: bool,
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ impl LanguageModelPickerDelegate {
|
||||
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
|
||||
let authenticate_all_providers = LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -474,7 +474,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
|
||||
let configured_providers = language_model_registry
|
||||
.read(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -566,10 +566,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
|
||||
Some(
|
||||
ModelSelectorListItem::new(ix, model_info.model.name().0)
|
||||
.map(|this| match &model_info.icon {
|
||||
IconOrSvg::Icon(icon_name) => this.icon(*icon_name),
|
||||
IconOrSvg::Svg(icon_path) => this.icon_path(icon_path.clone()),
|
||||
})
|
||||
.icon(model_info.icon)
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.is_favorite(is_favorite)
|
||||
@@ -705,7 +702,7 @@ mod tests {
|
||||
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
|
||||
ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconOrSvg::Icon(IconName::Ai),
|
||||
icon: IconName::Ai,
|
||||
is_favorite,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -191,9 +191,6 @@ impl Render for ProfileSelector {
|
||||
let container = || h_flex().gap_1().justify_between();
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(container().child(Label::new("Toggle Profile Menu")).child(
|
||||
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
|
||||
))
|
||||
.child(
|
||||
container()
|
||||
.pb_1()
|
||||
@@ -206,6 +203,9 @@ impl Render for ProfileSelector {
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(container().child(Label::new("Toggle Profile Menu")).child(
|
||||
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
|
||||
))
|
||||
.into_any()
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -33,8 +33,7 @@ use language::{
|
||||
language_settings::{SoftWrap, all_language_settings},
|
||||
};
|
||||
use language_model::{
|
||||
ConfigurationError, IconOrSvg, LanguageModelExt, LanguageModelImage, LanguageModelRegistry,
|
||||
Role,
|
||||
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::{Picker, popover_menu::PickerPopoverMenu};
|
||||
@@ -72,7 +71,7 @@ use workspace::{
|
||||
pane,
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
};
|
||||
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
|
||||
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
|
||||
@@ -1699,9 +1698,6 @@ impl TextThreadEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
@@ -1712,101 +1708,84 @@ impl TextThreadEditor {
|
||||
_ => None,
|
||||
});
|
||||
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
// 2. Have an associated file path
|
||||
// 3. Span multiple lines (not single-line selections)
|
||||
// 4. Belong to a file that exists in the current project
|
||||
let should_insert_creases = util::maybe!({
|
||||
let selections = editor_clipboard_selections.as_ref()?;
|
||||
if selections.len() > 1 {
|
||||
return Some(false);
|
||||
}
|
||||
let selection = selections.first()?;
|
||||
let file_path = selection.file_path.as_ref()?;
|
||||
let line_range = selection.line_range.as_ref()?;
|
||||
let has_file_context = editor_clipboard_selections
|
||||
.as_ref()
|
||||
.is_some_and(|selections| {
|
||||
selections
|
||||
.iter()
|
||||
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
|
||||
});
|
||||
|
||||
if line_range.start() == line_range.end() {
|
||||
return Some(false);
|
||||
}
|
||||
if has_file_context {
|
||||
if let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) =
|
||||
clipboard_item.entries().first()
|
||||
{
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
|
||||
Some(
|
||||
workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(file_path, cx)
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
|
||||
if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let selected_text =
|
||||
&text[current_offset..current_offset + selection.len];
|
||||
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||
file_path.to_str(),
|
||||
Some(line_range.clone()),
|
||||
);
|
||||
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||
|
||||
let text = clipboard_text.text();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let mut current_offset = 0;
|
||||
let weak_editor = cx.entity().downgrade();
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
|
||||
for selection in selections {
|
||||
if let (Some(file_path), Some(line_range)) =
|
||||
(selection.file_path, selection.line_range)
|
||||
{
|
||||
let selected_text =
|
||||
&text[current_offset..current_offset + selection.len];
|
||||
let fence = assistant_slash_commands::codeblock_fence_for_path(
|
||||
file_path.to_str(),
|
||||
Some(line_range.clone()),
|
||||
);
|
||||
let formatted_text = format!("{fence}{selected_text}\n```");
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
|
||||
let insert_point = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx))
|
||||
.head();
|
||||
let start_row = MultiBufferRow(insert_point.row);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_before = snapshot.anchor_after(insert_point);
|
||||
let anchor_after = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.bias_left(&snapshot);
|
||||
|
||||
editor.insert(&formatted_text, window, cx);
|
||||
editor.insert("\n", window, cx);
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_before = snapshot.anchor_after(insert_point);
|
||||
let anchor_after = editor
|
||||
.selections
|
||||
.newest_anchor()
|
||||
.head()
|
||||
.bias_left(&snapshot);
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
|
||||
editor.insert("\n", window, cx);
|
||||
let fold_placeholder = quote_selection_fold_placeholder(
|
||||
crease_text,
|
||||
weak_editor.clone(),
|
||||
);
|
||||
let crease = Crease::inline(
|
||||
anchor_before..anchor_after,
|
||||
fold_placeholder,
|
||||
render_quote_selection_output_toggle,
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
|
||||
let crease_text = acp_thread::selection_name(
|
||||
Some(file_path.as_ref()),
|
||||
&line_range,
|
||||
);
|
||||
|
||||
let fold_placeholder = quote_selection_fold_placeholder(
|
||||
crease_text,
|
||||
weak_editor.clone(),
|
||||
);
|
||||
let crease = Crease::inline(
|
||||
anchor_before..anchor_after,
|
||||
fold_placeholder,
|
||||
render_quote_selection_output_toggle,
|
||||
|_, _, _, _| Empty.into_any(),
|
||||
);
|
||||
editor.insert_creases(vec![crease], cx);
|
||||
editor.fold_at(start_row, window, cx);
|
||||
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
current_offset += selection.len;
|
||||
if !selection.is_entire_line && current_offset < text.len() {
|
||||
current_offset += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1965,12 +1944,6 @@ impl TextThreadEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.paste(&editor::actions::Paste, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
@@ -2232,10 +2205,10 @@ impl TextThreadEditor {
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let provider_icon = active_provider
|
||||
.as_ref()
|
||||
.map(|p| p.icon())
|
||||
.unwrap_or(IconOrSvg::Icon(IconName::Ai));
|
||||
let provider_icon = match active_provider {
|
||||
Some(provider) => provider.icon(),
|
||||
None => IconName::Ai,
|
||||
};
|
||||
|
||||
let focus_handle = self.editor().focus_handle(cx);
|
||||
|
||||
@@ -2245,13 +2218,6 @@ impl TextThreadEditor {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
let provider_icon_element = match provider_icon {
|
||||
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
|
||||
IconOrSvg::Icon(name) => Icon::new(name),
|
||||
}
|
||||
.color(color)
|
||||
.size(IconSize::XSmall);
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
@@ -2299,7 +2265,7 @@ impl TextThreadEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(provider_icon_element)
|
||||
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(color)
|
||||
@@ -2661,7 +2627,6 @@ impl Render for TextThreadEditor {
|
||||
.capture_action(cx.listener(TextThreadEditor::copy))
|
||||
.capture_action(cx.listener(TextThreadEditor::cut))
|
||||
.capture_action(cx.listener(TextThreadEditor::paste))
|
||||
.on_action(cx.listener(TextThreadEditor::paste_raw))
|
||||
.capture_action(cx.listener(TextThreadEditor::cycle_message_role))
|
||||
.capture_action(cx.listener(TextThreadEditor::confirm_command))
|
||||
.on_action(cx.listener(TextThreadEditor::assist))
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
use gpui::{Action, FocusHandle, prelude::*};
|
||||
use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
|
||||
enum ModelIcon {
|
||||
Name(IconName),
|
||||
Path(SharedString),
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelectorHeader {
|
||||
title: SharedString,
|
||||
@@ -44,7 +39,7 @@ impl RenderOnce for ModelSelectorHeader {
|
||||
pub struct ModelSelectorListItem {
|
||||
index: usize,
|
||||
title: SharedString,
|
||||
icon: Option<ModelIcon>,
|
||||
icon: Option<IconName>,
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
is_favorite: bool,
|
||||
@@ -65,12 +60,7 @@ impl ModelSelectorListItem {
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(ModelIcon::Name(icon));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon_path(mut self, path: SharedString) -> Self {
|
||||
self.icon = Some(ModelIcon::Path(path));
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -115,12 +105,9 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
.gap_1p5()
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.child(
|
||||
match icon {
|
||||
ModelIcon::Name(icon_name) => Icon::new(icon_name),
|
||||
ModelIcon::Path(icon_path) => Icon::from_external_svg(icon_path),
|
||||
}
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
Icon::new(icon)
|
||||
.color(model_icon_color)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
})
|
||||
.child(Label::new(self.title).truncate()),
|
||||
|
||||
@@ -12,10 +12,6 @@ workspace = true
|
||||
path = "src/agent_ui_v2.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["agent/test-support"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
agent_servers.workspace = true
|
||||
@@ -42,6 +38,3 @@ time_format.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
agent = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1 +1 @@
|
||||
../../LICENSE-GPL
|
||||
LICENSE-GPL
|
||||
@@ -1,5 +1,5 @@
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
@@ -411,22 +411,7 @@ impl AcpThreadHistory {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
|
||||
let display_text = match format {
|
||||
EntryTimeFormat::DateAndTime => {
|
||||
let entry_time = entry.updated_at();
|
||||
let now = Utc::now();
|
||||
let duration = now.signed_duration_since(entry_time);
|
||||
let days = duration.num_days();
|
||||
|
||||
format!("{}d", days)
|
||||
}
|
||||
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
|
||||
};
|
||||
|
||||
let title = entry.title().clone();
|
||||
let full_date =
|
||||
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -447,14 +432,11 @@ impl AcpThreadHistory {
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(display_text)
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
|
||||
})
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = Some(ix);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
|
||||
use language_model::{IconOrSvg, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||
use ui::{Divider, List, ListBulletItem, prelude::*};
|
||||
|
||||
pub struct ApiKeysWithProviders {
|
||||
configured_providers: Vec<(IconOrSvg, SharedString)>,
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
}
|
||||
|
||||
impl ApiKeysWithProviders {
|
||||
@@ -13,8 +13,7 @@ impl ApiKeysWithProviders {
|
||||
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
this.configured_providers = Self::compute_configured_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
@@ -27,9 +26,9 @@ impl ApiKeysWithProviders {
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconOrSvg, SharedString)> {
|
||||
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
@@ -48,14 +47,7 @@ impl Render for ApiKeysWithProviders {
|
||||
.map(|(icon, name)| {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
match icon {
|
||||
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
|
||||
IconOrSvg::Svg(icon_path) => Icon::from_external_svg(icon_path),
|
||||
}
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
||||
.child(Label::new(name))
|
||||
});
|
||||
div()
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
|
||||
pub struct AgentPanelOnboarding {
|
||||
user_store: Entity<UserStore>,
|
||||
client: Arc<Client>,
|
||||
has_configured_providers: bool,
|
||||
configured_providers: Vec<(IconName, SharedString)>,
|
||||
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
@@ -27,9 +27,8 @@ impl AgentPanelOnboarding {
|
||||
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
|
||||
language_model::Event::ProviderStateChanged(_)
|
||||
| language_model::Event::AddedProvider(_)
|
||||
| language_model::Event::RemovedProvider(_)
|
||||
| language_model::Event::ProvidersChanged => {
|
||||
this.has_configured_providers = Self::has_configured_providers(cx)
|
||||
| language_model::Event::RemovedProvider(_) => {
|
||||
this.configured_providers = Self::compute_available_providers(cx)
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
@@ -39,16 +38,20 @@ impl AgentPanelOnboarding {
|
||||
Self {
|
||||
user_store,
|
||||
client,
|
||||
has_configured_providers: Self::has_configured_providers(cx),
|
||||
configured_providers: Self::compute_available_providers(cx),
|
||||
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_configured_providers(cx: &App) -> bool {
|
||||
fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.visible_providers()
|
||||
.providers()
|
||||
.iter()
|
||||
.any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
|
||||
.filter(|provider| {
|
||||
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map(|provider| (provider.icon(), provider.name().0))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +81,7 @@ impl Render for AgentPanelOnboarding {
|
||||
}),
|
||||
)
|
||||
.map(|this| {
|
||||
if enrolled_in_trial || is_pro_user || self.has_configured_providers {
|
||||
if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
|
||||
this
|
||||
} else {
|
||||
this.child(ApiKeysWithoutProviders::new())
|
||||
|
||||
@@ -1159,34 +1159,6 @@ impl BufferDiff {
|
||||
new_index_text
|
||||
}
|
||||
|
||||
pub fn stage_or_unstage_all_hunks(
|
||||
&mut self,
|
||||
stage: bool,
|
||||
buffer: &text::BufferSnapshot,
|
||||
file_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let hunks = self
|
||||
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
|
||||
.collect::<Vec<_>>();
|
||||
let Some(secondary) = self.secondary_diff.as_ref() else {
|
||||
return;
|
||||
};
|
||||
self.inner.stage_or_unstage_hunks_impl(
|
||||
&secondary.read(cx).inner,
|
||||
stage,
|
||||
&hunks,
|
||||
buffer,
|
||||
file_exists,
|
||||
);
|
||||
if let Some((first, last)) = hunks.first().zip(hunks.last()) {
|
||||
let changed_range = first.buffer_range.start..last.buffer_range.end;
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(changed_range),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range_to_hunk_range(
|
||||
&self,
|
||||
range: Range<Anchor>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use edit_prediction_context::{EditPredictionExcerpt, EditPredictionExcerptOptions};
|
||||
use edit_prediction_types::{EditPrediction, EditPredictionDelegate};
|
||||
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{App, Context, Entity, Task};
|
||||
use http_client::HttpClient;
|
||||
@@ -300,6 +300,16 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate {
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_cursor_position: Anchor,
|
||||
_direction: Direction,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
// Codestral doesn't support multiple completions, so cycling does nothing
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut Context<Self>) {
|
||||
log::debug!("Codestral: Completion accepted");
|
||||
self.pending_request = None;
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
[package]
|
||||
name = "component_preview"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/component_preview.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
preview = []
|
||||
test-support = ["db/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
db.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
node_runtime.workspace = true
|
||||
notifications.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
session.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "component_preview"
|
||||
path = "examples/component_preview.rs"
|
||||
required-features = ["preview"]
|
||||
@@ -1 +0,0 @@
|
||||
LICENSE-GPL
|
||||
@@ -1,18 +0,0 @@
|
||||
//! Component Preview Example
|
||||
//!
|
||||
//! Run with: `cargo run -p component_preview --example component_preview --features="preview"`
|
||||
//!
|
||||
//! To use this in other projects, add the following to your `Cargo.toml`:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! component_preview = { path = "../component_preview", features = ["preview"] }
|
||||
//!
|
||||
//! [[example]]
|
||||
//! name = "component_preview"
|
||||
//! path = "examples/component_preview.rs"
|
||||
//! ```
|
||||
|
||||
fn main() {
|
||||
component_preview::run_component_preview();
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/// Run the component preview application.
|
||||
///
|
||||
/// This initializes the application with minimal required infrastructure
|
||||
/// and opens a workspace with the ComponentPreview item.
|
||||
#[cfg(feature = "preview")]
|
||||
pub fn run_component_preview() {
|
||||
use fs::RealFs;
|
||||
use gpui::{
|
||||
AppContext as _, Application, Bounds, KeyBinding, WindowBounds, WindowOptions, actions,
|
||||
size,
|
||||
};
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::Project;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use session::{AppSession, Session};
|
||||
use std::sync::Arc;
|
||||
use ui::{App, px};
|
||||
use workspace::{AppState, Workspace, WorkspaceStore};
|
||||
|
||||
use crate::{ComponentPreview, init};
|
||||
|
||||
actions!(zed, [Quit]);
|
||||
|
||||
fn quit(_: &Quit, cx: &mut App) {
|
||||
cx.quit();
|
||||
}
|
||||
|
||||
Application::new().run(|cx| {
|
||||
component::init();
|
||||
|
||||
cx.on_action(quit);
|
||||
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
|
||||
let version = release_channel::AppVersion::load(env!("CARGO_PKG_VERSION"), None, None);
|
||||
release_channel::init(version, cx);
|
||||
|
||||
let http_client =
|
||||
ReqwestClient::user_agent("component_preview").expect("Failed to create HTTP client");
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
|
||||
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
|
||||
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
||||
|
||||
settings::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
|
||||
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
|
||||
let client = Client::production(cx);
|
||||
client::init(&client, cx);
|
||||
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let session_id = uuid::Uuid::new_v4().to_string();
|
||||
let session = cx.background_executor().block(Session::new(session_id));
|
||||
let session = cx.new(|cx| AppSession::new(session, cx));
|
||||
let node_runtime = NodeRuntime::unavailable();
|
||||
|
||||
let app_state = Arc::new(AppState {
|
||||
languages,
|
||||
client,
|
||||
user_store,
|
||||
workspace_store,
|
||||
fs,
|
||||
build_window_options: |_, _| Default::default(),
|
||||
node_runtime,
|
||||
session,
|
||||
});
|
||||
AppState::set_global(Arc::downgrade(&app_state), cx);
|
||||
|
||||
workspace::init(app_state.clone(), cx);
|
||||
init(app_state.clone(), cx);
|
||||
|
||||
let size = size(px(1200.), px(800.));
|
||||
let bounds = Bounds::centered(None, size, cx);
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
{
|
||||
move |window, cx| {
|
||||
let app_state = app_state;
|
||||
theme::setup_ui_font(window, cx);
|
||||
|
||||
let project = Project::local(
|
||||
app_state.client.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
|
||||
let workspace = cx.new(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
project.clone(),
|
||||
app_state.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let weak_workspace = cx.entity().downgrade();
|
||||
let language_registry = app_state.languages.clone();
|
||||
let user_store = app_state.user_store.clone();
|
||||
|
||||
let component_preview = cx.new(|cx| {
|
||||
ComponentPreview::new(
|
||||
weak_workspace,
|
||||
project,
|
||||
language_registry,
|
||||
user_store,
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.expect("Failed to create component preview")
|
||||
});
|
||||
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(component_preview),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
workspace
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("Failed to open component preview window");
|
||||
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,6 @@ pub mod copilot_responses;
|
||||
pub mod request;
|
||||
mod sign_in;
|
||||
|
||||
use crate::request::NextEditSuggestions;
|
||||
use crate::sign_in::initiate_sign_out;
|
||||
use ::fs::Fs;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -19,7 +18,7 @@ use http_client::HttpClient;
|
||||
use language::language_settings::CopilotSettings;
|
||||
use language::{
|
||||
Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,
|
||||
language_settings::{EditPredictionProvider, all_language_settings},
|
||||
language_settings::{EditPredictionProvider, all_language_settings, language_settings},
|
||||
point_from_lsp, point_to_lsp,
|
||||
};
|
||||
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
|
||||
@@ -41,7 +40,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::Dimensions;
|
||||
use util::{ResultExt, fs::remove_matching};
|
||||
use util::{ResultExt, fs::remove_matching, rel_path::RelPath};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;
|
||||
@@ -316,15 +315,6 @@ struct GlobalCopilot(Entity<Copilot>);
|
||||
|
||||
impl Global for GlobalCopilot {}
|
||||
|
||||
/// Copilot's NextEditSuggestion response, with coordinates converted to Anchors.
|
||||
struct CopilotEditPrediction {
|
||||
buffer: Entity<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
text: String,
|
||||
command: Option<lsp::Command>,
|
||||
snapshot: BufferSnapshot,
|
||||
}
|
||||
|
||||
impl Copilot {
|
||||
pub fn global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<GlobalCopilot>()
|
||||
@@ -883,19 +873,101 @@ impl Copilot {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn completions(
|
||||
pub fn completions<T>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: Anchor,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<CopilotEditPrediction>>> {
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
self.request_completions::<request::GetCompletions, _>(buffer, position, cx)
|
||||
}
|
||||
|
||||
pub fn completions_cycling<T>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
self.request_completions::<request::GetCompletionsCycling, _>(buffer, position, cx)
|
||||
}
|
||||
|
||||
pub fn accept_completion(
|
||||
&mut self,
|
||||
completion: &Completion,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let request =
|
||||
server
|
||||
.lsp
|
||||
.request::<request::NotifyAccepted>(request::NotifyAcceptedParams {
|
||||
uuid: completion.uuid.clone(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify accepted")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn discard_completions(
|
||||
&mut self,
|
||||
completions: &[Completion],
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(_) => return Task::ready(Ok(())),
|
||||
};
|
||||
let request =
|
||||
server
|
||||
.lsp
|
||||
.request::<request::NotifyRejected>(request::NotifyRejectedParams {
|
||||
uuids: completions
|
||||
.iter()
|
||||
.map(|completion| completion.uuid.clone())
|
||||
.collect(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify rejected")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn request_completions<R, T>(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: T,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<Completion>>>
|
||||
where
|
||||
R: 'static
|
||||
+ lsp::request::Request<
|
||||
Params = request::GetCompletionsParams,
|
||||
Result = request::GetCompletionsResult,
|
||||
>,
|
||||
T: ToPointUtf16,
|
||||
{
|
||||
self.register_buffer(buffer, cx);
|
||||
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
let buffer_entity = buffer.clone();
|
||||
let lsp = server.lsp.clone();
|
||||
let registered_buffer = server
|
||||
.registered_buffers
|
||||
@@ -905,31 +977,46 @@ impl Copilot {
|
||||
let buffer = buffer.read(cx);
|
||||
let uri = registered_buffer.uri.clone();
|
||||
let position = position.to_point_utf16(buffer);
|
||||
let settings = language_settings(
|
||||
buffer.language_at(position).map(|l| l.name()),
|
||||
buffer.file(),
|
||||
cx,
|
||||
);
|
||||
let tab_size = settings.tab_size;
|
||||
let hard_tabs = settings.hard_tabs;
|
||||
let relative_path = buffer
|
||||
.file()
|
||||
.map_or(RelPath::empty().into(), |file| file.path().clone());
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let (version, snapshot) = snapshot.await?;
|
||||
let result = lsp
|
||||
.request::<NextEditSuggestions>(request::NextEditSuggestionsParams {
|
||||
text_document: lsp::VersionedTextDocumentIdentifier { uri, version },
|
||||
position: point_to_lsp(position),
|
||||
.request::<R>(request::GetCompletionsParams {
|
||||
doc: request::GetCompletionsDocument {
|
||||
uri,
|
||||
tab_size: tab_size.into(),
|
||||
indent_size: 1,
|
||||
insert_spaces: !hard_tabs,
|
||||
relative_path: relative_path.to_proto(),
|
||||
position: point_to_lsp(position),
|
||||
version: version.try_into().unwrap(),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: get completions")?;
|
||||
let completions = result
|
||||
.edits
|
||||
.completions
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
let start = snapshot
|
||||
.clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
|
||||
let end =
|
||||
snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
|
||||
CopilotEditPrediction {
|
||||
buffer: buffer_entity.clone(),
|
||||
Completion {
|
||||
uuid: completion.uuid,
|
||||
range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
|
||||
text: completion.text,
|
||||
command: completion.command,
|
||||
snapshot: snapshot.clone(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@@ -937,35 +1024,6 @@ impl Copilot {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn accept_completion(
|
||||
&mut self,
|
||||
completion: &CopilotEditPrediction,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let server = match self.server.as_authenticated() {
|
||||
Ok(server) => server,
|
||||
Err(error) => return Task::ready(Err(error)),
|
||||
};
|
||||
if let Some(command) = &completion.command {
|
||||
let request = server
|
||||
.lsp
|
||||
.request::<lsp::ExecuteCommand>(lsp::ExecuteCommandParams {
|
||||
command: command.command.clone(),
|
||||
arguments: command.arguments.clone().unwrap_or_default(),
|
||||
..Default::default()
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify accepted")?;
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Status {
|
||||
match &self.server {
|
||||
CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
|
||||
@@ -1202,11 +1260,7 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use util::{
|
||||
path,
|
||||
paths::PathStyle,
|
||||
rel_path::{RelPath, rel_path},
|
||||
};
|
||||
use util::{path, paths::PathStyle, rel_path::rel_path};
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_management(cx: &mut TestAppContext) {
|
||||
|
||||
@@ -1,29 +1,49 @@
|
||||
use crate::{Copilot, CopilotEditPrediction};
|
||||
use crate::{Completion, Copilot};
|
||||
use anyhow::Result;
|
||||
use edit_prediction_types::{EditPrediction, EditPredictionDelegate, interpolate_edits};
|
||||
use gpui::{App, Context, Entity, Task};
|
||||
use language::{Anchor, Buffer, EditPreview, OffsetRangeExt};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate};
|
||||
use gpui::{App, Context, Entity, EntityId, Task};
|
||||
use language::{Buffer, OffsetRangeExt, ToOffset, language_settings::AllLanguageSettings};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
|
||||
|
||||
pub struct CopilotEditPredictionDelegate {
|
||||
completion: Option<(CopilotEditPrediction, EditPreview)>,
|
||||
cycled: bool,
|
||||
buffer_id: Option<EntityId>,
|
||||
completions: Vec<Completion>,
|
||||
active_completion_index: usize,
|
||||
file_extension: Option<String>,
|
||||
pending_refresh: Option<Task<Result<()>>>,
|
||||
pending_cycling_refresh: Option<Task<Result<()>>>,
|
||||
copilot: Entity<Copilot>,
|
||||
}
|
||||
|
||||
impl CopilotEditPredictionDelegate {
|
||||
pub fn new(copilot: Entity<Copilot>) -> Self {
|
||||
Self {
|
||||
completion: None,
|
||||
cycled: false,
|
||||
buffer_id: None,
|
||||
completions: Vec::new(),
|
||||
active_completion_index: 0,
|
||||
file_extension: None,
|
||||
pending_refresh: None,
|
||||
pending_cycling_refresh: None,
|
||||
copilot,
|
||||
}
|
||||
}
|
||||
|
||||
fn active_completion(&self) -> Option<&(CopilotEditPrediction, EditPreview)> {
|
||||
self.completion.as_ref()
|
||||
fn active_completion(&self) -> Option<&Completion> {
|
||||
self.completions.get(self.active_completion_index)
|
||||
}
|
||||
|
||||
fn push_completion(&mut self, new_completion: Completion) {
|
||||
for completion in &self.completions {
|
||||
if completion.text == new_completion.text && completion.range == new_completion.range {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.completions.push(new_completion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +64,12 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_jump_to_edit() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_refreshing(&self, _cx: &App) -> bool {
|
||||
self.pending_refresh.is_some() && self.completion.is_none()
|
||||
self.pending_refresh.is_some() && self.completions.is_empty()
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
@@ -78,96 +102,160 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
if let Some(mut completion) = completions.into_iter().next()
|
||||
&& let Some(trimmed_completion) = cx
|
||||
.update(|cx| trim_completion(&completion, cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
let preview = buffer
|
||||
.update(cx, |this, cx| {
|
||||
this.preview_edits(Arc::from(std::slice::from_ref(&trimmed_completion)), cx)
|
||||
})?
|
||||
.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
if !completions.is_empty() {
|
||||
this.cycled = false;
|
||||
this.pending_refresh = None;
|
||||
completion.range = trimmed_completion.0;
|
||||
completion.text = trimmed_completion.1.to_string();
|
||||
this.completion = Some((completion, preview));
|
||||
this.pending_cycling_refresh = None;
|
||||
this.completions.clear();
|
||||
this.active_completion_index = 0;
|
||||
this.buffer_id = Some(buffer.entity_id());
|
||||
this.file_extension = buffer.read(cx).file().and_then(|file| {
|
||||
Some(
|
||||
Path::new(file.file_name(cx))
|
||||
.extension()?
|
||||
.to_str()?
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
|
||||
for completion in completions {
|
||||
this.push_completion(completion);
|
||||
}
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.cycled {
|
||||
match direction {
|
||||
Direction::Prev => {
|
||||
self.active_completion_index = if self.active_completion_index == 0 {
|
||||
self.completions.len().saturating_sub(1)
|
||||
} else {
|
||||
self.active_completion_index - 1
|
||||
};
|
||||
}
|
||||
Direction::Next => {
|
||||
if self.completions.is_empty() {
|
||||
self.active_completion_index = 0
|
||||
} else {
|
||||
self.active_completion_index =
|
||||
(self.active_completion_index + 1) % self.completions.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
} else {
|
||||
let copilot = self.copilot.clone();
|
||||
self.pending_cycling_refresh = Some(cx.spawn(async move |this, cx| {
|
||||
let completions = copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.completions_cycling(&buffer, cursor_position, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.cycled = true;
|
||||
this.file_extension = buffer.read(cx).file().and_then(|file| {
|
||||
Some(
|
||||
Path::new(file.file_name(cx))
|
||||
.extension()?
|
||||
.to_str()?
|
||||
.to_string(),
|
||||
)
|
||||
});
|
||||
for completion in completions {
|
||||
this.push_completion(completion);
|
||||
}
|
||||
this.cycle(buffer, cursor_position, direction, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some((completion, _)) = self.active_completion() {
|
||||
if let Some(completion) = self.active_completion() {
|
||||
self.copilot
|
||||
.update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn discard(&mut self, _: &mut Context<Self>) {}
|
||||
fn discard(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
let copilot_enabled = settings.show_edit_predictions(None, cx);
|
||||
|
||||
if !copilot_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.discard_completions(&self.completions, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn suggest(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
_: language::Anchor,
|
||||
cursor_position: language::Anchor,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<EditPrediction> {
|
||||
let buffer_id = buffer.entity_id();
|
||||
let buffer = buffer.read(cx);
|
||||
let (completion, edit_preview) = self.active_completion()?;
|
||||
|
||||
if Some(buffer_id) != Some(completion.buffer.entity_id())
|
||||
let completion = self.active_completion()?;
|
||||
if Some(buffer_id) != self.buffer_id
|
||||
|| !completion.range.start.is_valid(buffer)
|
||||
|| !completion.range.end.is_valid(buffer)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let edits = vec![(
|
||||
completion.range.clone(),
|
||||
Arc::from(completion.text.as_ref()),
|
||||
)];
|
||||
let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits)
|
||||
.filter(|edits| !edits.is_empty())?;
|
||||
|
||||
Some(EditPrediction::Local {
|
||||
id: None,
|
||||
edits,
|
||||
edit_preview: Some(edit_preview.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
let mut completion_range = completion.range.to_offset(buffer);
|
||||
let prefix_len = common_prefix(
|
||||
buffer.chars_for_range(completion_range.clone()),
|
||||
completion.text.chars(),
|
||||
);
|
||||
completion_range.start += prefix_len;
|
||||
let suffix_len = common_prefix(
|
||||
buffer.reversed_chars_for_range(completion_range.clone()),
|
||||
completion.text[prefix_len..].chars().rev(),
|
||||
);
|
||||
completion_range.end = completion_range.end.saturating_sub(suffix_len);
|
||||
|
||||
fn trim_completion(
|
||||
completion: &CopilotEditPrediction,
|
||||
cx: &mut App,
|
||||
) -> Option<(Range<Anchor>, Arc<str>)> {
|
||||
let buffer = completion.buffer.read(cx);
|
||||
let mut completion_range = completion.range.to_offset(buffer);
|
||||
let prefix_len = common_prefix(
|
||||
buffer.chars_for_range(completion_range.clone()),
|
||||
completion.text.chars(),
|
||||
);
|
||||
completion_range.start += prefix_len;
|
||||
let suffix_len = common_prefix(
|
||||
buffer.reversed_chars_for_range(completion_range.clone()),
|
||||
completion.text[prefix_len..].chars().rev(),
|
||||
);
|
||||
completion_range.end = completion_range.end.saturating_sub(suffix_len);
|
||||
let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
|
||||
if completion_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
let completion_range =
|
||||
buffer.anchor_after(completion_range.start)..buffer.anchor_after(completion_range.end);
|
||||
|
||||
Some((completion_range, Arc::from(completion_text)))
|
||||
if completion_range.is_empty()
|
||||
&& completion_range.start == cursor_position.to_offset(buffer)
|
||||
{
|
||||
let completion_text = &completion.text[prefix_len..completion.text.len() - suffix_len];
|
||||
if completion_text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
let position = cursor_position.bias_right(buffer);
|
||||
Some(EditPrediction::Local {
|
||||
id: None,
|
||||
edits: vec![(position..position, completion_text.into())],
|
||||
edit_preview: None,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +282,6 @@ mod tests {
|
||||
Point,
|
||||
language_settings::{CompletionSettingsContent, LspInsertMode, WordsCompletionMode},
|
||||
};
|
||||
use lsp::Uri;
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use settings::{AllLanguageSettingsContent, SettingsStore};
|
||||
@@ -250,15 +337,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -299,15 +383,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -331,15 +412,12 @@ mod tests {
|
||||
// After debouncing, new Copilot completions should be requested.
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot2".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -401,6 +479,45 @@ mod tests {
|
||||
assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.set_text("fn foo() {\n \n}", window, cx);
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
|
||||
});
|
||||
});
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::Completion {
|
||||
text: " let x = 4;".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
|
||||
// Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
|
||||
editor.tab(&Default::default(), window, cx);
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n \n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
|
||||
// Using AcceptEditPrediction again accepts the suggestion.
|
||||
editor.accept_edit_prediction(&Default::default(), window, cx);
|
||||
assert!(!editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -453,15 +570,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.copilot1".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -500,15 +614,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "one.123. copilot\n 456".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -575,18 +686,15 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_edit_prediction(&Default::default(), window, cx)
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -595,22 +703,15 @@ mod tests {
|
||||
assert_eq!(editor.text(cx), "one\ntw\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\nt\nthree\n");
|
||||
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
assert!(editor.has_active_edit_prediction());
|
||||
assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
|
||||
assert_eq!(editor.text(cx), "one\n\nthree\n");
|
||||
|
||||
// Deleting across the original suggestion range invalidates it.
|
||||
editor.backspace(&Default::default(), window, cx);
|
||||
assert!(!editor.has_active_edit_prediction());
|
||||
@@ -664,22 +765,19 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "b = 2 + a".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Ensure copilot suggestions are shown for the first excerpt.
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
|
||||
});
|
||||
editor.show_edit_prediction(&Default::default(), window, cx);
|
||||
editor.next_edit_prediction(&Default::default(), window, cx);
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
_ = editor.update(cx, |editor, _, cx| {
|
||||
@@ -693,15 +791,12 @@ mod tests {
|
||||
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "d = 4 + c".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
// Move to another excerpt, ensuring the suggestion gets cleared.
|
||||
@@ -778,18 +873,15 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_edit_prediction(&Default::default(), window, cx)
|
||||
editor.next_edit_prediction(&Default::default(), window, cx)
|
||||
});
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -811,15 +903,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 3)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -841,15 +930,12 @@ mod tests {
|
||||
));
|
||||
handle_copilot_completion_request(
|
||||
&copilot_lsp,
|
||||
vec![crate::request::NextEditSuggestion {
|
||||
vec![crate::request::Completion {
|
||||
text: "two.foo()".into(),
|
||||
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 4)),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
vec![],
|
||||
);
|
||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||
cx.update_editor(|editor, _, cx| {
|
||||
@@ -925,20 +1011,16 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let mut copilot_requests = copilot_lsp
|
||||
.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
|
||||
.set_request_handler::<crate::request::GetCompletions, _, _>(
|
||||
move |_params, _cx| async move {
|
||||
Ok(crate::request::NextEditSuggestionsResult {
|
||||
edits: vec![crate::request::NextEditSuggestion {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: vec![crate::request::Completion {
|
||||
text: "next line".into(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(1, 0),
|
||||
lsp::Position::new(1, 0),
|
||||
),
|
||||
command: None,
|
||||
text_document: lsp::VersionedTextDocumentIdentifier {
|
||||
uri: Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
|
||||
version: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
})
|
||||
},
|
||||
@@ -967,14 +1049,23 @@ mod tests {
|
||||
|
||||
fn handle_copilot_completion_request(
|
||||
lsp: &lsp::FakeLanguageServer,
|
||||
completions: Vec<crate::request::NextEditSuggestion>,
|
||||
completions: Vec<crate::request::Completion>,
|
||||
completions_cycling: Vec<crate::request::Completion>,
|
||||
) {
|
||||
lsp.set_request_handler::<crate::request::NextEditSuggestions, _, _>(
|
||||
lsp.set_request_handler::<crate::request::GetCompletions, _, _>(move |_params, _cx| {
|
||||
let completions = completions.clone();
|
||||
async move {
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions.clone(),
|
||||
})
|
||||
}
|
||||
});
|
||||
lsp.set_request_handler::<crate::request::GetCompletionsCycling, _, _>(
|
||||
move |_params, _cx| {
|
||||
let completions = completions.clone();
|
||||
let completions_cycling = completions_cycling.clone();
|
||||
async move {
|
||||
Ok(crate::request::NextEditSuggestionsResult {
|
||||
edits: completions.clone(),
|
||||
Ok(crate::request::GetCompletionsResult {
|
||||
completions: completions_cycling.clone(),
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use lsp::VersionedTextDocumentIdentifier;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub enum CheckStatus {}
|
||||
@@ -89,6 +88,72 @@ impl lsp::request::Request for SignOut {
|
||||
const METHOD: &'static str = "signOut";
|
||||
}
|
||||
|
||||
pub enum GetCompletions {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsParams {
|
||||
pub doc: GetCompletionsDocument,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsDocument {
|
||||
pub tab_size: u32,
|
||||
pub indent_size: u32,
|
||||
pub insert_spaces: bool,
|
||||
pub uri: lsp::Uri,
|
||||
pub relative_path: String,
|
||||
pub position: lsp::Position,
|
||||
pub version: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsResult {
|
||||
pub completions: Vec<Completion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Completion {
|
||||
pub text: String,
|
||||
pub position: lsp::Position,
|
||||
pub uuid: String,
|
||||
pub range: lsp::Range,
|
||||
pub display_text: String,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for GetCompletions {
|
||||
type Params = GetCompletionsParams;
|
||||
type Result = GetCompletionsResult;
|
||||
const METHOD: &'static str = "getCompletions";
|
||||
}
|
||||
|
||||
pub enum GetCompletionsCycling {}
|
||||
|
||||
impl lsp::request::Request for GetCompletionsCycling {
|
||||
type Params = GetCompletionsParams;
|
||||
type Result = GetCompletionsResult;
|
||||
const METHOD: &'static str = "getCompletionsCycling";
|
||||
}
|
||||
|
||||
pub enum LogMessage {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogMessageParams {
|
||||
pub level: u8,
|
||||
pub message: String,
|
||||
pub metadata_str: String,
|
||||
pub extra: Vec<String>,
|
||||
}
|
||||
|
||||
impl lsp::notification::Notification for LogMessage {
|
||||
type Params = LogMessageParams;
|
||||
const METHOD: &'static str = "LogMessage";
|
||||
}
|
||||
|
||||
pub enum StatusNotification {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -158,36 +223,3 @@ impl lsp::request::Request for NotifyRejected {
|
||||
type Result = String;
|
||||
const METHOD: &'static str = "notifyRejected";
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestions;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestionsParams {
|
||||
pub(crate) text_document: VersionedTextDocumentIdentifier,
|
||||
pub(crate) position: lsp::Position,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestion {
|
||||
pub text: String,
|
||||
pub text_document: VersionedTextDocumentIdentifier,
|
||||
pub range: lsp::Range,
|
||||
pub command: Option<lsp::Command>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NextEditSuggestionsResult {
|
||||
pub edits: Vec<NextEditSuggestion>,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for NextEditSuggestions {
|
||||
type Params = NextEditSuggestionsParams;
|
||||
type Result = NextEditSuggestionsResult;
|
||||
|
||||
const METHOD: &'static str = "textDocument/copilotInlineEdit";
|
||||
}
|
||||
|
||||
@@ -103,9 +103,8 @@ impl Model {
|
||||
|
||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||
match self {
|
||||
// Their API treats this max against the context window, which means we hit the limit a lot
|
||||
// Using the default value of None in the API instead
|
||||
Self::Chat | Self::Reasoner => None,
|
||||
Self::Chat => Some(8_192),
|
||||
Self::Reasoner => Some(64_000),
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => *max_output_tokens,
|
||||
|
||||
@@ -7,6 +7,8 @@ license = "GPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
command_palette.workspace = true
|
||||
gpui.workspace = true
|
||||
# We are specifically pinning this version of mdbook, as later versions introduce issues with double-nested subdirectories.
|
||||
# Ask @maxdeviant about this before bumping.
|
||||
mdbook = "= 0.4.40"
|
||||
@@ -15,6 +17,7 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
util.workspace = true
|
||||
zed.workspace = true
|
||||
zlog.workspace = true
|
||||
task.workspace = true
|
||||
theme.workspace = true
|
||||
@@ -24,4 +27,4 @@ workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "docs_preprocessor"
|
||||
path = "src/main.rs"
|
||||
path = "src/main.rs"
|
||||
|
||||
@@ -22,13 +22,16 @@ static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
|
||||
});
|
||||
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(load_all_actions);
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||
|
||||
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
zlog::init();
|
||||
zlog::init_output_stderr();
|
||||
// call a zed:: function so everything in `zed` crate is linked and
|
||||
// all actions in the actual app are registered
|
||||
zed::stdout_is_a_pty();
|
||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
|
||||
match args.get(0).map(String::as_str) {
|
||||
@@ -69,8 +72,8 @@ enum PreprocessorError {
|
||||
impl PreprocessorError {
|
||||
fn new_for_not_found_action(action_name: String) -> Self {
|
||||
for action in &*ALL_ACTIONS {
|
||||
for alias in &action.deprecated_aliases {
|
||||
if alias == action_name.as_str() {
|
||||
for alias in action.deprecated_aliases {
|
||||
if alias == &action_name {
|
||||
return PreprocessorError::DeprecatedActionUsed {
|
||||
used: action_name,
|
||||
should_be: action.name.to_string(),
|
||||
@@ -211,7 +214,7 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Prepr
|
||||
chapter.content = regex
|
||||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||
let action = caps[1].trim();
|
||||
if is_missing_action(action) {
|
||||
if find_action_by_name(action).is_none() {
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
action.to_string(),
|
||||
));
|
||||
@@ -241,12 +244,10 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
|
||||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||
let name = caps[1].trim();
|
||||
let Some(action) = find_action_by_name(name) else {
|
||||
if actions_available() {
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
name.to_string(),
|
||||
));
|
||||
}
|
||||
return format!("<code class=\"hljs\">{}</code>", name);
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
name.to_string(),
|
||||
));
|
||||
return String::new();
|
||||
};
|
||||
format!("<code class=\"hljs\">{}</code>", &action.human_name)
|
||||
})
|
||||
@@ -256,19 +257,11 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
|
||||
|
||||
fn find_action_by_name(name: &str) -> Option<&ActionDef> {
|
||||
ALL_ACTIONS
|
||||
.binary_search_by(|action| action.name.as_str().cmp(name))
|
||||
.binary_search_by(|action| action.name.cmp(name))
|
||||
.ok()
|
||||
.map(|index| &ALL_ACTIONS[index])
|
||||
}
|
||||
|
||||
fn actions_available() -> bool {
|
||||
!ALL_ACTIONS.is_empty()
|
||||
}
|
||||
|
||||
fn is_missing_action(name: &str) -> bool {
|
||||
actions_available() && find_action_by_name(name).is_none()
|
||||
}
|
||||
|
||||
fn find_binding(os: &str, action: &str) -> Option<String> {
|
||||
let keymap = match os {
|
||||
"macos" => &KEYMAP_MACOS,
|
||||
@@ -391,13 +384,18 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<Pre
|
||||
let keymap = settings::KeymapFile::parse(&snippet_json_fixed)
|
||||
.context("Failed to parse keymap JSON")?;
|
||||
for section in keymap.sections() {
|
||||
for (_keystrokes, action) in section.bindings() {
|
||||
for (keystrokes, action) in section.bindings() {
|
||||
keystrokes
|
||||
.split_whitespace()
|
||||
.map(|source| gpui::Keystroke::parse(source))
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.context("Failed to parse keystroke")?;
|
||||
if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
|
||||
.map_err(|err| anyhow::format_err!(err))
|
||||
.context("Failed to parse action")?
|
||||
{
|
||||
anyhow::ensure!(
|
||||
!is_missing_action(action_name),
|
||||
find_action_by_name(action_name).is_some(),
|
||||
"Action not found: {}",
|
||||
action_name
|
||||
);
|
||||
@@ -493,35 +491,27 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct ActionDef {
|
||||
name: String,
|
||||
name: &'static str,
|
||||
human_name: String,
|
||||
deprecated_aliases: Vec<String>,
|
||||
#[serde(rename = "documentation")]
|
||||
docs: Option<String>,
|
||||
deprecated_aliases: &'static [&'static str],
|
||||
docs: Option<&'static str>,
|
||||
}
|
||||
|
||||
fn load_all_actions() -> Vec<ActionDef> {
|
||||
let asset_path = concat!(env!("CARGO_MANIFEST_DIR"), "/actions.json");
|
||||
match std::fs::read_to_string(asset_path) {
|
||||
Ok(content) => {
|
||||
let mut actions: Vec<ActionDef> =
|
||||
serde_json::from_str(&content).expect("Failed to parse actions.json");
|
||||
actions.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
actions
|
||||
}
|
||||
Err(err) => {
|
||||
if std::env::var("CI").is_ok() {
|
||||
panic!("actions.json not found at {}: {}", asset_path, err);
|
||||
}
|
||||
eprintln!(
|
||||
"Warning: actions.json not found, action validation will be skipped: {}",
|
||||
err
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
fn dump_all_gpui_actions() -> Vec<ActionDef> {
|
||||
let mut actions = gpui::generate_list_of_all_registered_actions()
|
||||
.map(|action| ActionDef {
|
||||
name: action.name,
|
||||
human_name: command_palette::humanize_action_name(action.name),
|
||||
deprecated_aliases: action.deprecated_aliases,
|
||||
docs: action.documentation,
|
||||
})
|
||||
.collect::<Vec<ActionDef>>();
|
||||
|
||||
actions.sort_by_key(|a| a.name);
|
||||
|
||||
actions
|
||||
}
|
||||
|
||||
fn handle_postprocessing() -> Result<()> {
|
||||
@@ -657,7 +647,7 @@ fn generate_big_table_of_actions() -> String {
|
||||
let mut output = String::new();
|
||||
|
||||
let mut actions_sorted = actions.iter().collect::<Vec<_>>();
|
||||
actions_sorted.sort_by_key(|a| a.name.as_str());
|
||||
actions_sorted.sort_by_key(|a| a.name);
|
||||
|
||||
// Start the definition list with custom styling for better spacing
|
||||
output.push_str("<dl style=\"line-height: 1.8;\">\n");
|
||||
@@ -674,7 +664,7 @@ fn generate_big_table_of_actions() -> String {
|
||||
output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
|
||||
|
||||
// Add the description, escaping HTML if needed
|
||||
if let Some(description) = action.docs.as_ref() {
|
||||
if let Some(description) = action.docs {
|
||||
output.push_str(
|
||||
&description
|
||||
.replace("&", "&")
|
||||
@@ -684,7 +674,7 @@ fn generate_big_table_of_actions() -> String {
|
||||
output.push_str("<br>\n");
|
||||
}
|
||||
output.push_str("Keymap Name: <code>");
|
||||
output.push_str(&action.name);
|
||||
output.push_str(action.name);
|
||||
output.push_str("</code><br>\n");
|
||||
if !action.deprecated_aliases.is_empty() {
|
||||
output.push_str("Deprecated Alias(es): ");
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
use anyhow::{Context as _, Result};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
|
||||
@@ -300,19 +300,14 @@ pub const MERCURY_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
|
||||
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
|
||||
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
|
||||
|
||||
struct GlobalMercuryApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalMercuryApiKey {}
|
||||
pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
let entity =
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
|
||||
cx.set_global(GlobalMercuryApiKey(entity.clone()));
|
||||
entity
|
||||
MERCURY_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{
|
||||
App, AppContext as _, Entity, Global, SharedString, Task,
|
||||
App, AppContext as _, Entity, SharedString, Task,
|
||||
http_client::{self, AsyncBody, Method},
|
||||
};
|
||||
use language::{Point, ToOffset as _};
|
||||
@@ -272,19 +272,14 @@ pub const SWEEP_CREDENTIALS_URL: SharedString =
|
||||
SharedString::new_static("https://autocomplete.sweep.dev");
|
||||
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
|
||||
pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
|
||||
|
||||
struct GlobalSweepApiKey(Entity<ApiKeyState>);
|
||||
|
||||
impl Global for GlobalSweepApiKey {}
|
||||
pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
|
||||
|
||||
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
|
||||
if let Some(global) = cx.try_global::<GlobalSweepApiKey>() {
|
||||
return global.0.clone();
|
||||
}
|
||||
let entity =
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
|
||||
cx.set_global(GlobalSweepApiKey(entity.clone()));
|
||||
entity
|
||||
SWEEP_API_KEY
|
||||
.get_or_init(|| {
|
||||
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{cmp, sync::Arc};
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use cloud_llm_client::EditPredictionRejectReason;
|
||||
use edit_prediction_types::{DataCollectionState, EditPredictionDelegate};
|
||||
use edit_prediction_types::{DataCollectionState, Direction, EditPredictionDelegate};
|
||||
use gpui::{App, Entity, prelude::*};
|
||||
use language::{Buffer, ToPoint as _};
|
||||
use project::Project;
|
||||
@@ -139,6 +139,15 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate {
|
||||
});
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: Direction,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
self.store.update(cx, |store, cx| {
|
||||
store.accept_current_prediction(&self.project, cx);
|
||||
|
||||
@@ -95,6 +95,13 @@ pub trait EditPredictionDelegate: 'static + Sized {
|
||||
debounce: bool,
|
||||
cx: &mut Context<Self>,
|
||||
);
|
||||
fn cycle(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut Context<Self>,
|
||||
);
|
||||
fn accept(&mut self, cx: &mut Context<Self>);
|
||||
fn discard(&mut self, cx: &mut Context<Self>);
|
||||
fn did_show(&mut self, _cx: &mut Context<Self>) {}
|
||||
@@ -129,6 +136,13 @@ pub trait EditPredictionDelegateHandle {
|
||||
debounce: bool,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn cycle(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut App,
|
||||
);
|
||||
fn did_show(&self, cx: &mut App);
|
||||
fn accept(&self, cx: &mut App);
|
||||
fn discard(&self, cx: &mut App);
|
||||
@@ -201,6 +215,18 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_position: language::Anchor,
|
||||
direction: Direction,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.update(cx, |this, cx| {
|
||||
this.cycle(buffer, cursor_position, direction, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn accept(&self, cx: &mut App) {
|
||||
self.update(cx, |this, cx| this.accept(cx))
|
||||
}
|
||||
|
||||
@@ -348,61 +348,6 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_bracket_colorization_after_language_swap(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.colorize_brackets = Some(true);
|
||||
});
|
||||
|
||||
let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
|
||||
language_registry.add(markdown_lang());
|
||||
language_registry.add(rust_lang());
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language_registry(language_registry.clone());
|
||||
buffer.set_language(Some(markdown_lang()), cx);
|
||||
});
|
||||
|
||||
cx.set_state(indoc! {r#"
|
||||
fn main() {
|
||||
let v: Vec<Stringˇ> = vec![];
|
||||
}
|
||||
"#});
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
r#"fn main«1()1» «1{
|
||||
let v: Vec<String> = vec!«2[]2»;
|
||||
}1»
|
||||
|
||||
1 hsla(207.80, 16.20%, 69.19%, 1.00)
|
||||
2 hsla(29.00, 54.00%, 65.88%, 1.00)
|
||||
"#,
|
||||
&bracket_colors_markup(&mut cx),
|
||||
"Markdown does not colorize <> brackets"
|
||||
);
|
||||
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language(Some(rust_lang()), cx);
|
||||
});
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
r#"fn main«1()1» «1{
|
||||
let v: Vec«2<String>2» = vec!«2[]2»;
|
||||
}1»
|
||||
|
||||
1 hsla(207.80, 16.20%, 69.19%, 1.00)
|
||||
2 hsla(29.00, 54.00%, 65.88%, 1.00)
|
||||
"#,
|
||||
&bracket_colors_markup(&mut cx),
|
||||
"After switching to Rust, <> brackets are now colorized"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
|
||||
@@ -51,8 +51,6 @@ pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
|
||||
pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
|
||||
pub const COMPLETION_MENU_MIN_WIDTH: Pixels = px(280.);
|
||||
pub const COMPLETION_MENU_MAX_WIDTH: Pixels = px(540.);
|
||||
pub const CODE_ACTION_MENU_MIN_WIDTH: Pixels = px(220.);
|
||||
pub const CODE_ACTION_MENU_MAX_WIDTH: Pixels = px(540.);
|
||||
|
||||
// Constants for the markdown cache. The purpose of this cache is to reduce flickering due to
|
||||
// documentation not yet being parsed.
|
||||
@@ -181,7 +179,7 @@ impl CodeContextMenu {
|
||||
) -> Option<AnyElement> {
|
||||
match self {
|
||||
CodeContextMenu::Completions(menu) => menu.render_aside(max_size, window, cx),
|
||||
CodeContextMenu::CodeActions(menu) => menu.render_aside(max_size, window, cx),
|
||||
CodeContextMenu::CodeActions(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -893,7 +891,7 @@ impl CompletionsMenu {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(text.trim().to_string())
|
||||
Label::new(text.clone())
|
||||
.ml_4()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
@@ -1421,6 +1419,26 @@ pub enum CodeActionsItem {
|
||||
}
|
||||
|
||||
impl CodeActionsItem {
|
||||
fn as_task(&self) -> Option<&ResolvedTask> {
|
||||
let Self::Task(_, task) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(task)
|
||||
}
|
||||
|
||||
fn as_code_action(&self) -> Option<&CodeAction> {
|
||||
let Self::CodeAction { action, .. } = self else {
|
||||
return None;
|
||||
};
|
||||
Some(action)
|
||||
}
|
||||
fn as_debug_scenario(&self) -> Option<&DebugScenario> {
|
||||
let Self::DebugScenario(scenario) = self else {
|
||||
return None;
|
||||
};
|
||||
Some(scenario)
|
||||
}
|
||||
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
|
||||
@@ -1428,14 +1446,6 @@ impl CodeActionsItem {
|
||||
Self::DebugScenario(scenario) => scenario.label.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn menu_label(&self) -> String {
|
||||
match self {
|
||||
Self::CodeAction { action, .. } => action.lsp_action.title().replace("\n", ""),
|
||||
Self::Task(_, task) => task.resolved_label.replace("\n", ""),
|
||||
Self::DebugScenario(scenario) => format!("debug: {}", scenario.label),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodeActionsMenu {
|
||||
@@ -1545,33 +1555,60 @@ impl CodeActionsMenu {
|
||||
let item_ix = range.start + ix;
|
||||
let selected = item_ix == selected_item;
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
ListItem::new(item_ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.overflow_x()
|
||||
.child(
|
||||
div()
|
||||
.min_w(CODE_ACTION_MENU_MIN_WIDTH)
|
||||
.max_w(CODE_ACTION_MENU_MAX_WIDTH)
|
||||
.overflow_hidden()
|
||||
.text_ellipsis()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.when(selected, |this| this.text_color(colors.text_accent))
|
||||
.child(action.menu_label()),
|
||||
)
|
||||
.on_click(cx.listener(move |editor, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
}))
|
||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
||||
ListItem::new(item_ix)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.when_some(action.as_code_action(), |this, action| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(
|
||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||
action.lsp_action.title().replace("\n", ""),
|
||||
)
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(action.as_task(), |this, task| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(task.resolved_label.replace("\n", ""))
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(action.as_debug_scenario(), |this, scenario| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child("debug: ")
|
||||
.child(scenario.label.clone())
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(move |editor, _, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if let Some(task) = editor.confirm_code_action(
|
||||
&ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx)
|
||||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
@@ -1598,46 +1635,4 @@ impl CodeActionsMenu {
|
||||
|
||||
Popover::new().child(list).into_any_element()
|
||||
}
|
||||
|
||||
fn render_aside(
|
||||
&mut self,
|
||||
max_size: Size<Pixels>,
|
||||
window: &mut Window,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> Option<AnyElement> {
|
||||
let Some(action) = self.actions.get(self.selected_item) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let label = action.menu_label();
|
||||
let text_system = window.text_system();
|
||||
let mut line_wrapper = text_system.line_wrapper(
|
||||
window.text_style().font(),
|
||||
window.text_style().font_size.to_pixels(window.rem_size()),
|
||||
);
|
||||
let is_truncated = line_wrapper.should_truncate_line(
|
||||
&label,
|
||||
CODE_ACTION_MENU_MAX_WIDTH,
|
||||
"…",
|
||||
gpui::TruncateFrom::End,
|
||||
);
|
||||
|
||||
if is_truncated.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
Popover::new()
|
||||
.child(
|
||||
div()
|
||||
.child(label)
|
||||
.id("code_actions_menu_extended")
|
||||
.px(MENU_ASIDE_X_PADDING / 2.)
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.occlude(),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,6 +485,15 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate {
|
||||
) {
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: gpui::Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: edit_prediction_types::Direction,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
@@ -552,6 +561,15 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate {
|
||||
) {
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: gpui::Entity<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: edit_prediction_types::Direction,
|
||||
_cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
|
||||
|
||||
@@ -73,7 +73,11 @@ pub use multi_buffer::{
|
||||
pub use split::SplittableEditor;
|
||||
pub use text::Bias;
|
||||
|
||||
use ::git::{Restore, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
|
||||
use ::git::{
|
||||
Restore,
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
status::FileStatus,
|
||||
};
|
||||
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, BuildError};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use blink_manager::BlinkManager;
|
||||
@@ -163,7 +167,6 @@ use project::{
|
||||
project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter, ProjectSettings},
|
||||
};
|
||||
use rand::seq::SliceRandom;
|
||||
use regex::Regex;
|
||||
use rpc::{ErrorCode, ErrorExt, proto::PeerId};
|
||||
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager};
|
||||
use selections_collection::{MutableSelectionsCollection, SelectionsCollection};
|
||||
@@ -4788,146 +4791,82 @@ impl Editor {
|
||||
let end = selection.end;
|
||||
let selection_is_empty = start == end;
|
||||
let language_scope = buffer.language_scope_at(start);
|
||||
let (delimiter, newline_config) = if let Some(language) = &language_scope {
|
||||
let needs_extra_newline = NewlineConfig::insert_extra_newline_brackets(
|
||||
&buffer,
|
||||
start..end,
|
||||
language,
|
||||
)
|
||||
|| NewlineConfig::insert_extra_newline_tree_sitter(
|
||||
&buffer,
|
||||
start..end,
|
||||
);
|
||||
let (comment_delimiter, doc_delimiter, newline_formatting) =
|
||||
if let Some(language) = &language_scope {
|
||||
let mut newline_formatting =
|
||||
NewlineFormatting::new(&buffer, start..end, language);
|
||||
|
||||
let mut newline_config = NewlineConfig::Newline {
|
||||
additional_indent: IndentSize::spaces(0),
|
||||
extra_line_additional_indent: if needs_extra_newline {
|
||||
Some(IndentSize::spaces(0))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
prevent_auto_indent: false,
|
||||
// Comment extension on newline is allowed only for cursor selections
|
||||
let comment_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
return comment_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
);
|
||||
});
|
||||
|
||||
let doc_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
return documentation_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
&mut newline_formatting,
|
||||
);
|
||||
});
|
||||
|
||||
(comment_delimiter, doc_delimiter, newline_formatting)
|
||||
} else {
|
||||
(None, None, NewlineFormatting::default())
|
||||
};
|
||||
|
||||
let comment_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
let prevent_auto_indent = doc_delimiter.is_some();
|
||||
let delimiter = comment_delimiter.or(doc_delimiter);
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
let capacity_for_delimiter =
|
||||
delimiter.as_deref().map(str::len).unwrap_or_default();
|
||||
let mut new_text = String::with_capacity(
|
||||
1 + capacity_for_delimiter
|
||||
+ existing_indent.len as usize
|
||||
+ newline_formatting.indent_on_newline.len as usize
|
||||
+ newline_formatting.indent_on_extra_newline.len as usize,
|
||||
);
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(newline_formatting.indent_on_newline.chars());
|
||||
|
||||
return comment_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
);
|
||||
});
|
||||
if let Some(delimiter) = &delimiter {
|
||||
new_text.push_str(delimiter);
|
||||
}
|
||||
|
||||
let doc_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_comment_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
return documentation_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
&mut newline_config,
|
||||
);
|
||||
});
|
||||
|
||||
let list_delimiter = maybe!({
|
||||
if !selection_is_empty {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !multi_buffer.language_settings(cx).extend_list_on_newline {
|
||||
return None;
|
||||
}
|
||||
|
||||
return list_delimiter_for_newline(
|
||||
&start_point,
|
||||
&buffer,
|
||||
language,
|
||||
&mut newline_config,
|
||||
);
|
||||
});
|
||||
|
||||
(
|
||||
comment_delimiter.or(doc_delimiter).or(list_delimiter),
|
||||
newline_config,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
None,
|
||||
NewlineConfig::Newline {
|
||||
additional_indent: IndentSize::spaces(0),
|
||||
extra_line_additional_indent: None,
|
||||
prevent_auto_indent: false,
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let (edit_start, new_text, prevent_auto_indent) = match &newline_config {
|
||||
NewlineConfig::ClearCurrentLine => {
|
||||
let row_start =
|
||||
buffer.point_to_offset(Point::new(start_point.row, 0));
|
||||
(row_start, String::new(), false)
|
||||
}
|
||||
NewlineConfig::UnindentCurrentLine { continuation } => {
|
||||
let row_start =
|
||||
buffer.point_to_offset(Point::new(start_point.row, 0));
|
||||
let tab_size = buffer.language_settings_at(start, cx).tab_size;
|
||||
let tab_size_indent = IndentSize::spaces(tab_size.get());
|
||||
let reduced_indent =
|
||||
existing_indent.with_delta(Ordering::Less, tab_size_indent);
|
||||
let mut new_text = String::new();
|
||||
new_text.extend(reduced_indent.chars());
|
||||
new_text.push_str(continuation);
|
||||
(row_start, new_text, true)
|
||||
}
|
||||
NewlineConfig::Newline {
|
||||
additional_indent,
|
||||
extra_line_additional_indent,
|
||||
prevent_auto_indent,
|
||||
} => {
|
||||
let capacity_for_delimiter =
|
||||
delimiter.as_deref().map(str::len).unwrap_or_default();
|
||||
let extra_line_len = extra_line_additional_indent
|
||||
.map(|i| 1 + existing_indent.len as usize + i.len as usize)
|
||||
.unwrap_or(0);
|
||||
let mut new_text = String::with_capacity(
|
||||
1 + capacity_for_delimiter
|
||||
+ existing_indent.len as usize
|
||||
+ additional_indent.len as usize
|
||||
+ extra_line_len,
|
||||
);
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(additional_indent.chars());
|
||||
if let Some(delimiter) = &delimiter {
|
||||
new_text.push_str(delimiter);
|
||||
}
|
||||
if let Some(extra_indent) = extra_line_additional_indent {
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(extra_indent.chars());
|
||||
}
|
||||
(start, new_text, *prevent_auto_indent)
|
||||
}
|
||||
};
|
||||
if newline_formatting.insert_extra_newline {
|
||||
new_text.push('\n');
|
||||
new_text.extend(existing_indent.chars());
|
||||
new_text.extend(newline_formatting.indent_on_extra_newline.chars());
|
||||
}
|
||||
|
||||
let anchor = buffer.anchor_after(end);
|
||||
let new_selection = selection.map(|_| anchor);
|
||||
(
|
||||
((edit_start..end, new_text), prevent_auto_indent),
|
||||
(newline_config.has_extra_line(), new_selection),
|
||||
((start..end, new_text), prevent_auto_indent),
|
||||
(newline_formatting.insert_extra_newline, new_selection),
|
||||
)
|
||||
})
|
||||
.unzip()
|
||||
@@ -7529,6 +7468,26 @@ impl Editor {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn cycle_edit_prediction(
|
||||
&mut self,
|
||||
direction: Direction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
let provider = self.edit_prediction_provider()?;
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
let (buffer, cursor_buffer_position) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||
if self.edit_predictions_hidden_for_vim_mode || !self.should_show_edit_predictions() {
|
||||
return None;
|
||||
}
|
||||
|
||||
provider.cycle(buffer, cursor_buffer_position, direction, cx);
|
||||
self.update_visible_edit_prediction(window, cx);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn show_edit_prediction(
|
||||
&mut self,
|
||||
_: &ShowEditPrediction,
|
||||
@@ -7566,6 +7525,42 @@ impl Editor {
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn next_edit_prediction(
|
||||
&mut self,
|
||||
_: &NextEditPrediction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.has_active_edit_prediction() {
|
||||
self.cycle_edit_prediction(Direction::Next, window, cx);
|
||||
} else {
|
||||
let is_copilot_disabled = self
|
||||
.refresh_edit_prediction(false, true, window, cx)
|
||||
.is_none();
|
||||
if is_copilot_disabled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn previous_edit_prediction(
|
||||
&mut self,
|
||||
_: &PreviousEditPrediction,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.has_active_edit_prediction() {
|
||||
self.cycle_edit_prediction(Direction::Prev, window, cx);
|
||||
} else {
|
||||
let is_copilot_disabled = self
|
||||
.refresh_edit_prediction(false, true, window, cx)
|
||||
.is_none();
|
||||
if is_copilot_disabled {
|
||||
cx.propagate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept_partial_edit_prediction(
|
||||
&mut self,
|
||||
granularity: EditPredictionGranularity,
|
||||
@@ -10452,22 +10447,6 @@ impl Editor {
|
||||
}
|
||||
prev_edited_row = selection.end.row;
|
||||
|
||||
// If cursor is after a list prefix, make selection non-empty to trigger line indent
|
||||
if selection.is_empty() {
|
||||
let cursor = selection.head();
|
||||
let settings = buffer.language_settings_at(cursor, cx);
|
||||
if settings.indent_list_on_tab {
|
||||
if let Some(language) = snapshot.language_scope_at(Point::new(cursor.row, 0)) {
|
||||
if is_list_prefix_row(MultiBufferRow(cursor.row), &snapshot, &language) {
|
||||
row_delta = Self::indent_selection(
|
||||
buffer, &snapshot, selection, &mut edits, row_delta, cx,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the selection is non-empty, then increase the indentation of the selected lines.
|
||||
if !selection.is_empty() {
|
||||
row_delta =
|
||||
@@ -20475,7 +20454,7 @@ impl Editor {
|
||||
EditorSettings::get_global(cx).gutter.line_numbers
|
||||
}
|
||||
|
||||
pub fn relative_line_numbers(&self, cx: &App) -> RelativeLineNumbers {
|
||||
pub fn relative_line_numbers(&self, cx: &mut App) -> RelativeLineNumbers {
|
||||
match (
|
||||
self.use_relative_line_numbers,
|
||||
EditorSettings::get_global(cx).relative_line_numbers,
|
||||
@@ -23436,7 +23415,7 @@ fn documentation_delimiter_for_newline(
|
||||
start_point: &Point,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
newline_config: &mut NewlineConfig,
|
||||
newline_formatting: &mut NewlineFormatting,
|
||||
) -> Option<Arc<str>> {
|
||||
let BlockCommentConfig {
|
||||
start: start_tag,
|
||||
@@ -23488,9 +23467,6 @@ fn documentation_delimiter_for_newline(
|
||||
}
|
||||
};
|
||||
|
||||
let mut needs_extra_line = false;
|
||||
let mut extra_line_additional_indent = IndentSize::spaces(0);
|
||||
|
||||
let cursor_is_before_end_tag_if_exists = {
|
||||
let mut char_position = 0u32;
|
||||
let mut end_tag_offset = None;
|
||||
@@ -23508,11 +23484,11 @@ fn documentation_delimiter_for_newline(
|
||||
let cursor_is_before_end_tag = column <= end_tag_offset;
|
||||
if cursor_is_after_start_tag {
|
||||
if cursor_is_before_end_tag {
|
||||
needs_extra_line = true;
|
||||
newline_formatting.insert_extra_newline = true;
|
||||
}
|
||||
let cursor_is_at_start_of_end_tag = column == end_tag_offset;
|
||||
if cursor_is_at_start_of_end_tag {
|
||||
extra_line_additional_indent.len = *len;
|
||||
newline_formatting.indent_on_extra_newline.len = *len;
|
||||
}
|
||||
}
|
||||
cursor_is_before_end_tag
|
||||
@@ -23524,240 +23500,39 @@ fn documentation_delimiter_for_newline(
|
||||
if (cursor_is_after_start_tag || cursor_is_after_delimiter)
|
||||
&& cursor_is_before_end_tag_if_exists
|
||||
{
|
||||
let additional_indent = if cursor_is_after_start_tag {
|
||||
IndentSize::spaces(*len)
|
||||
} else {
|
||||
IndentSize::spaces(0)
|
||||
};
|
||||
|
||||
*newline_config = NewlineConfig::Newline {
|
||||
additional_indent,
|
||||
extra_line_additional_indent: if needs_extra_line {
|
||||
Some(extra_line_additional_indent)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
prevent_auto_indent: true,
|
||||
};
|
||||
if cursor_is_after_start_tag {
|
||||
newline_formatting.indent_on_newline.len = *len;
|
||||
}
|
||||
Some(delimiter.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
const ORDERED_LIST_MAX_MARKER_LEN: usize = 16;
|
||||
|
||||
fn list_delimiter_for_newline(
|
||||
start_point: &Point,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
newline_config: &mut NewlineConfig,
|
||||
) -> Option<Arc<str>> {
|
||||
let (snapshot, range) = buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?;
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
|
||||
let task_list_entries: Vec<_> = language
|
||||
.task_list()
|
||||
.into_iter()
|
||||
.flat_map(|config| {
|
||||
config
|
||||
.prefixes
|
||||
.iter()
|
||||
.map(|prefix| (prefix.as_ref(), config.continuation.as_ref()))
|
||||
})
|
||||
.collect();
|
||||
let unordered_list_entries: Vec<_> = language
|
||||
.unordered_list()
|
||||
.iter()
|
||||
.map(|marker| (marker.as_ref(), marker.as_ref()))
|
||||
.collect();
|
||||
|
||||
let all_entries: Vec<_> = task_list_entries
|
||||
.into_iter()
|
||||
.chain(unordered_list_entries)
|
||||
.collect();
|
||||
|
||||
if let Some(max_prefix_len) = all_entries.iter().map(|(p, _)| p.len()).max() {
|
||||
let candidate: String = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_prefix_len)
|
||||
.collect();
|
||||
|
||||
if let Some((prefix, continuation)) = all_entries
|
||||
.iter()
|
||||
.filter(|(prefix, _)| candidate.starts_with(*prefix))
|
||||
.max_by_key(|(prefix, _)| prefix.len())
|
||||
{
|
||||
let end_of_prefix = num_of_whitespaces + prefix.len();
|
||||
let cursor_is_after_prefix = end_of_prefix <= start_point.column as usize;
|
||||
let has_content_after_marker = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(end_of_prefix)
|
||||
.any(|c| !c.is_whitespace());
|
||||
|
||||
if has_content_after_marker && cursor_is_after_prefix {
|
||||
return Some((*continuation).into());
|
||||
}
|
||||
|
||||
if start_point.column as usize == end_of_prefix {
|
||||
if num_of_whitespaces == 0 {
|
||||
*newline_config = NewlineConfig::ClearCurrentLine;
|
||||
} else {
|
||||
*newline_config = NewlineConfig::UnindentCurrentLine {
|
||||
continuation: (*continuation).into(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let candidate: String = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(ORDERED_LIST_MAX_MARKER_LEN)
|
||||
.collect();
|
||||
|
||||
for ordered_config in language.ordered_list() {
|
||||
let regex = match Regex::new(&ordered_config.pattern) {
|
||||
Ok(r) => r,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if let Some(captures) = regex.captures(&candidate) {
|
||||
let full_match = captures.get(0)?;
|
||||
let marker_len = full_match.len();
|
||||
let end_of_prefix = num_of_whitespaces + marker_len;
|
||||
let cursor_is_after_prefix = end_of_prefix <= start_point.column as usize;
|
||||
|
||||
let has_content_after_marker = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(end_of_prefix)
|
||||
.any(|c| !c.is_whitespace());
|
||||
|
||||
if has_content_after_marker && cursor_is_after_prefix {
|
||||
let number: u32 = captures.get(1)?.as_str().parse().ok()?;
|
||||
let continuation = ordered_config
|
||||
.format
|
||||
.replace("{1}", &(number + 1).to_string());
|
||||
return Some(continuation.into());
|
||||
}
|
||||
|
||||
if start_point.column as usize == end_of_prefix {
|
||||
let continuation = ordered_config.format.replace("{1}", "1");
|
||||
if num_of_whitespaces == 0 {
|
||||
*newline_config = NewlineConfig::ClearCurrentLine;
|
||||
} else {
|
||||
*newline_config = NewlineConfig::UnindentCurrentLine {
|
||||
continuation: continuation.into(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
#[derive(Debug, Default)]
|
||||
struct NewlineFormatting {
|
||||
insert_extra_newline: bool,
|
||||
indent_on_newline: IndentSize,
|
||||
indent_on_extra_newline: IndentSize,
|
||||
}
|
||||
|
||||
fn is_list_prefix_row(
|
||||
row: MultiBufferRow,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
language: &LanguageScope,
|
||||
) -> bool {
|
||||
let Some((snapshot, range)) = buffer.buffer_line_for_row(row) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let num_of_whitespaces = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count();
|
||||
|
||||
let task_list_prefixes: Vec<_> = language
|
||||
.task_list()
|
||||
.into_iter()
|
||||
.flat_map(|config| {
|
||||
config
|
||||
.prefixes
|
||||
.iter()
|
||||
.map(|p| p.as_ref())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
let unordered_list_markers: Vec<_> = language
|
||||
.unordered_list()
|
||||
.iter()
|
||||
.map(|marker| marker.as_ref())
|
||||
.collect();
|
||||
let all_prefixes: Vec<_> = task_list_prefixes
|
||||
.into_iter()
|
||||
.chain(unordered_list_markers)
|
||||
.collect();
|
||||
if let Some(max_prefix_len) = all_prefixes.iter().map(|p| p.len()).max() {
|
||||
let candidate: String = snapshot
|
||||
.chars_for_range(range.clone())
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_prefix_len)
|
||||
.collect();
|
||||
if all_prefixes
|
||||
.iter()
|
||||
.any(|prefix| candidate.starts_with(*prefix))
|
||||
{
|
||||
return true;
|
||||
impl NewlineFormatting {
|
||||
fn new(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
language: &LanguageScope,
|
||||
) -> Self {
|
||||
Self {
|
||||
insert_extra_newline: Self::insert_extra_newline_brackets(
|
||||
buffer,
|
||||
range.clone(),
|
||||
language,
|
||||
) || Self::insert_extra_newline_tree_sitter(buffer, range),
|
||||
indent_on_newline: IndentSize::spaces(0),
|
||||
indent_on_extra_newline: IndentSize::spaces(0),
|
||||
}
|
||||
}
|
||||
|
||||
let ordered_list_candidate: String = snapshot
|
||||
.chars_for_range(range)
|
||||
.skip(num_of_whitespaces)
|
||||
.take(ORDERED_LIST_MAX_MARKER_LEN)
|
||||
.collect();
|
||||
for ordered_config in language.ordered_list() {
|
||||
let regex = match Regex::new(&ordered_config.pattern) {
|
||||
Ok(r) => r,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let Some(captures) = regex.captures(&ordered_list_candidate) {
|
||||
return captures.get(0).is_some();
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum NewlineConfig {
|
||||
/// Insert newline with optional additional indent and optional extra blank line
|
||||
Newline {
|
||||
additional_indent: IndentSize,
|
||||
extra_line_additional_indent: Option<IndentSize>,
|
||||
prevent_auto_indent: bool,
|
||||
},
|
||||
/// Clear the current line
|
||||
ClearCurrentLine,
|
||||
/// Unindent the current line and add continuation
|
||||
UnindentCurrentLine { continuation: Arc<str> },
|
||||
}
|
||||
|
||||
impl NewlineConfig {
|
||||
fn has_extra_line(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Newline {
|
||||
extra_line_additional_indent: Some(_),
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn insert_extra_newline_brackets(
|
||||
buffer: &MultiBufferSnapshot,
|
||||
range: Range<MultiBufferOffset>,
|
||||
@@ -25294,70 +25069,6 @@ impl EditorSnapshot {
|
||||
let digit_count = self.widest_line_number().ilog10() + 1;
|
||||
column_pixels(style, digit_count as usize, window)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let point = DisplayPoint::new(line, 0).to_point(self);
|
||||
self.relative_line_delta_to_point(base, point)
|
||||
}
|
||||
|
||||
/// Returns the line delta from `base` to `point` in the multibuffer, ignoring wrapped lines.
|
||||
///
|
||||
/// This is positive if `base` is before `point`.
|
||||
pub fn relative_line_delta_to_point(&self, base: DisplayRow, point: Point) -> 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
|
||||
}
|
||||
|
||||
/// Returns the unsigned relative line number to display for each row in `rows`.
|
||||
///
|
||||
/// Wrapped rows are excluded from the hashmap if `count_relative_lines` is `false`.
|
||||
pub fn calculate_relative_line_numbers(
|
||||
&self,
|
||||
rows: &Range<DisplayRow>,
|
||||
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)
|
||||
.take(rows.len())
|
||||
.enumerate()
|
||||
.map(|(i, row_info)| (DisplayRow(rows.start.0 + i as u32), row_info));
|
||||
display_row_infos
|
||||
.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))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn column_pixels(style: &EditorStyle, column: usize, window: &Window) -> Pixels {
|
||||
|
||||
@@ -215,8 +215,7 @@ impl Settings for EditorSettings {
|
||||
},
|
||||
scrollbar: Scrollbar {
|
||||
show: scrollbar.show.map(Into::into).unwrap(),
|
||||
git_diff: scrollbar.git_diff.unwrap()
|
||||
&& content.git.unwrap().enabled.unwrap().is_git_diff_enabled(),
|
||||
git_diff: scrollbar.git_diff.unwrap(),
|
||||
selected_text: scrollbar.selected_text.unwrap(),
|
||||
selected_symbol: scrollbar.selected_symbol.unwrap(),
|
||||
search_results: scrollbar.search_results.unwrap(),
|
||||
|
||||
@@ -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,
|
||||
IndentGuide, MultiBufferFilterMode, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
@@ -70,6 +69,7 @@ use util::{
|
||||
use workspace::{
|
||||
CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
|
||||
OpenOptions, ViewId,
|
||||
invalid_item_view::InvalidItemView,
|
||||
item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
|
||||
register_project_item,
|
||||
};
|
||||
@@ -20881,36 +20881,6 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_up(&MoveUp, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
|
||||
});
|
||||
cx.assert_state_with_diff(
|
||||
indoc! { "
|
||||
ˇone
|
||||
- two
|
||||
three
|
||||
five
|
||||
"}
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
|
||||
});
|
||||
cx.assert_state_with_diff(
|
||||
indoc! { "
|
||||
one
|
||||
- two
|
||||
ˇthree
|
||||
- four
|
||||
five
|
||||
"}
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
cx.set_state(indoc! { "
|
||||
one
|
||||
ˇTWO
|
||||
@@ -20950,66 +20920,6 @@ async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_toggling_adjacent_diff_hunks_2(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
let diff_base = r#"
|
||||
lineA
|
||||
lineB
|
||||
lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
cx.set_state(
|
||||
&r#"
|
||||
ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.set_head_text(&diff_base);
|
||||
executor.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
- lineA
|
||||
+ ˇlineA1
|
||||
lineB
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.move_right(&MoveRight, window, cx);
|
||||
editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
|
||||
});
|
||||
executor.run_until_parked();
|
||||
cx.assert_state_with_diff(
|
||||
r#"
|
||||
- lineA
|
||||
+ lineA1
|
||||
lˇineB
|
||||
- lineC
|
||||
lineD
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edits_around_expanded_deletion_hunks(
|
||||
executor: BackgroundExecutor,
|
||||
@@ -27757,10 +27667,11 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
// The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
|
||||
// Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
|
||||
// With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
|
||||
assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
|
||||
|
||||
assert_eq!(
|
||||
handle.to_any_view().entity_type(),
|
||||
TypeId::of::<InvalidItemView>()
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -28022,7 +27933,7 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
"
|
||||
});
|
||||
|
||||
// Case 2: Test adding new line after nested list continues the list with unchecked task
|
||||
// Case 2: Test adding new line after nested list preserves indent of previous line
|
||||
cx.set_state(&indoc! {"
|
||||
- [ ] Item 1
|
||||
- [ ] Item 1.a
|
||||
@@ -28039,12 +27950,20 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
- [ ] ˇ"
|
||||
ˇ"
|
||||
});
|
||||
|
||||
// Case 3: Test adding content to continued list item
|
||||
// Case 3: Test adding a new nested list item preserves indent
|
||||
cx.set_state(&indoc! {"
|
||||
- [ ] Item 1
|
||||
- [ ] Item 1.a
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("Item 2.c", window, cx);
|
||||
editor.handle_input("-", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
@@ -28053,10 +27972,22 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
- [ ] Item 2.cˇ"
|
||||
-ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(" [x] Item 2.c", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] Item 1
|
||||
- [ ] Item 1.a
|
||||
- [x] Item 2
|
||||
- [x] Item 2.a
|
||||
- [x] Item 2.b
|
||||
- [x] Item 2.cˇ"
|
||||
});
|
||||
|
||||
// Case 4: Test adding new line after nested ordered list continues with next number
|
||||
// Case 4: Test adding new line after nested ordered list preserves indent of previous line
|
||||
cx.set_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
@@ -28073,12 +28004,44 @@ async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
3. ˇ"
|
||||
ˇ"
|
||||
});
|
||||
|
||||
// Case 5: Adding content to continued ordered list item
|
||||
// Case 5: Adding new ordered list item preserves indent
|
||||
cx.set_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("Item 2.c", window, cx);
|
||||
editor.handle_input("3", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
3ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. Item 1
|
||||
1. Item 1.a
|
||||
2. Item 2
|
||||
1. Item 2.a
|
||||
2. Item 2.b
|
||||
3.ˇ"
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input(" Item 2.c", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(indoc! {"
|
||||
@@ -28634,130 +28597,6 @@ async fn test_sticky_scroll(cx: &mut TestAppContext) {
|
||||
assert_eq!(sticky_headers(10.0), vec![]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_relative_line_numbers(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
|
||||
let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
|
||||
let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
|
||||
|
||||
let multibuffer = cx.new(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(ReadWrite);
|
||||
multibuffer.push_excerpts(
|
||||
buffer_1.clone(),
|
||||
[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
|
||||
cx,
|
||||
);
|
||||
multibuffer.push_excerpts(
|
||||
buffer_2.clone(),
|
||||
[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
|
||||
cx,
|
||||
);
|
||||
multibuffer.push_excerpts(
|
||||
buffer_3.clone(),
|
||||
[ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
|
||||
cx,
|
||||
);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
// wrapped contents of multibuffer:
|
||||
// aaa
|
||||
// aaa
|
||||
// aaa
|
||||
// a
|
||||
// bbb
|
||||
//
|
||||
// ccc
|
||||
// ccc
|
||||
// ccc
|
||||
// c
|
||||
// ddd
|
||||
//
|
||||
// eee
|
||||
// fff
|
||||
// fff
|
||||
// fff
|
||||
// f
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
|
||||
|
||||
// includes trailing newlines.
|
||||
let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
|
||||
let expected_wrapped_line_numbers = [
|
||||
2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
|
||||
];
|
||||
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_ranges([
|
||||
Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
|
||||
]);
|
||||
});
|
||||
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
|
||||
// these are all 0-indexed
|
||||
let base_display_row = DisplayRow(11);
|
||||
let base_row = 3;
|
||||
let wrapped_base_row = 7;
|
||||
|
||||
// test not counting wrapped lines
|
||||
let expected_relative_numbers = expected_line_numbers
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
|
||||
.collect_vec();
|
||||
let actual_relative_numbers = snapshot
|
||||
.calculate_relative_line_numbers(
|
||||
&(DisplayRow(0)..DisplayRow(24)),
|
||||
base_display_row,
|
||||
false,
|
||||
)
|
||||
.into_iter()
|
||||
.sorted()
|
||||
.collect_vec();
|
||||
assert_eq!(expected_relative_numbers, actual_relative_numbers);
|
||||
// check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
|
||||
for (display_row, relative_number) in expected_relative_numbers {
|
||||
assert_eq!(
|
||||
relative_number,
|
||||
snapshot
|
||||
.relative_line_delta(display_row, base_display_row)
|
||||
.unsigned_abs() as u32,
|
||||
);
|
||||
}
|
||||
|
||||
// test counting wrapped lines
|
||||
let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
|
||||
.collect_vec();
|
||||
let actual_relative_numbers = snapshot
|
||||
.calculate_relative_line_numbers(
|
||||
&(DisplayRow(0)..DisplayRow(24)),
|
||||
base_display_row,
|
||||
true,
|
||||
)
|
||||
.into_iter()
|
||||
.sorted()
|
||||
.collect_vec();
|
||||
assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
|
||||
// check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
|
||||
for (display_row, relative_number) in expected_wrapped_relative_numbers {
|
||||
assert_eq!(
|
||||
relative_number,
|
||||
snapshot
|
||||
.relative_wrapped_line_delta(display_row, base_display_row)
|
||||
.unsigned_abs() as u32,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -29570,524 +29409,6 @@ async fn test_find_references_single_case(cx: &mut TestAppContext) {
|
||||
cx.assert_editor_state(after);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] taskˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 2: Works with checked task items 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
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] ta
|
||||
- [ ] ˇsk
|
||||
"});
|
||||
|
||||
// Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
- [ ]$$
|
||||
ˇ
|
||||
"}
|
||||
.replace("$", " ")
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
// Case 5: Adding newline with content adds marker preserving indentation
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] indentedˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] indented
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 6: Adding newline with cursor right after prefix, unindents
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
|
||||
// Case 7: Adding newline with cursor right after prefix, removes marker
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] task
|
||||
- [ ] sub task
|
||||
ˇ
|
||||
"});
|
||||
|
||||
// Case 8: Cursor before or inside prefix does not add marker
|
||||
cx.set_state(indoc! {"
|
||||
ˇ- [ ] task
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ- [ ] task
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
- [ˇ ] task
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [
|
||||
ˇ
|
||||
] task
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
|
||||
cx.set_state(indoc! {"
|
||||
- itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- ˇ
|
||||
"});
|
||||
|
||||
// Case 2: Works with different markers
|
||||
cx.set_state(indoc! {"
|
||||
* starred itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
* starred item
|
||||
* ˇ
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
+ plus itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
+ plus item
|
||||
+ ˇ
|
||||
"});
|
||||
|
||||
// Case 3: Cursor position doesn't matter - content after marker is what counts
|
||||
cx.set_state(indoc! {"
|
||||
- itˇem
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- it
|
||||
- ˇem
|
||||
"});
|
||||
|
||||
// Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
|
||||
cx.set_state(indoc! {"
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
- $
|
||||
ˇ
|
||||
"}
|
||||
.replace("$", " ")
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
// Case 5: Adding newline with content adds marker preserving indentation
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
- indentedˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- indented
|
||||
- ˇ
|
||||
"});
|
||||
|
||||
// Case 6: Adding newline with cursor right after marker, unindents
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
|
||||
// Case 7: Adding newline with cursor right after marker, removes marker
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- item
|
||||
- sub item
|
||||
ˇ
|
||||
"});
|
||||
|
||||
// Case 8: Cursor before or inside prefix does not add marker
|
||||
cx.set_state(indoc! {"
|
||||
ˇ- item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ- item
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
-ˇ item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
-
|
||||
ˇitem
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
|
||||
cx.set_state(indoc! {"
|
||||
1. first itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. first item
|
||||
2. ˇ
|
||||
"});
|
||||
|
||||
// Case 2: Works with larger numbers
|
||||
cx.set_state(indoc! {"
|
||||
10. tenth itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
10. tenth item
|
||||
11. ˇ
|
||||
"});
|
||||
|
||||
// Case 3: Cursor position doesn't matter - content after marker is what counts
|
||||
cx.set_state(indoc! {"
|
||||
1. itˇem
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. it
|
||||
2. ˇem
|
||||
"});
|
||||
|
||||
// Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
|
||||
cx.set_state(indoc! {"
|
||||
1. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(
|
||||
indoc! {"
|
||||
1. $
|
||||
ˇ
|
||||
"}
|
||||
.replace("$", " ")
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
// Case 5: Adding newline with content adds marker preserving indentation
|
||||
cx.set_state(indoc! {"
|
||||
1. item
|
||||
2. indentedˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. indented
|
||||
3. ˇ
|
||||
"});
|
||||
|
||||
// Case 6: Adding newline with cursor right after marker, unindents
|
||||
cx.set_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
3. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
1. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
|
||||
// Case 7: Adding newline with cursor right after marker, removes marker
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
1. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. item
|
||||
2. sub item
|
||||
ˇ
|
||||
"});
|
||||
|
||||
// Case 8: Cursor before or inside prefix does not add marker
|
||||
cx.set_state(indoc! {"
|
||||
ˇ1. item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
|
||||
ˇ1. item
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
1ˇ. item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1
|
||||
ˇ. item
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
|
||||
cx.set_state(indoc! {"
|
||||
1. first item
|
||||
1. sub first item
|
||||
2. sub second item
|
||||
3. ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
1. first item
|
||||
1. sub first item
|
||||
2. sub second item
|
||||
1. ˇ
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab_list_indent(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = Some(2.try_into().unwrap());
|
||||
});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
// Case 1: Unordered list - cursor after prefix, adds indent before prefix
|
||||
cx.set_state(indoc! {"
|
||||
- ˇitem
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- ˇitem
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 2: Task list - cursor after prefix
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] ˇtask
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- [ ] ˇtask
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 3: Ordered list - cursor after prefix
|
||||
cx.set_state(indoc! {"
|
||||
1. ˇfirst
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$1. ˇfirst
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 4: With existing indentation - adds more indent
|
||||
let initial = indoc! {"
|
||||
$$- ˇitem
|
||||
"};
|
||||
cx.set_state(initial.replace("$", " ").as_str());
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$$$- ˇitem
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 5: Empty list item
|
||||
cx.set_state(indoc! {"
|
||||
- ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- ˇ
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 6: Cursor at end of line with content
|
||||
cx.set_state(indoc! {"
|
||||
- itemˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
$$- itemˇ
|
||||
"};
|
||||
cx.assert_editor_state(expected.replace("$", " ").as_str());
|
||||
|
||||
// Case 7: Cursor at start of list item, indents it
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
ˇ - sub item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
- item
|
||||
ˇ - sub item
|
||||
"};
|
||||
cx.assert_editor_state(expected);
|
||||
|
||||
// Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
|
||||
cx.update_editor(|_, _, cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
cx.set_state(indoc! {"
|
||||
- item
|
||||
ˇ - sub item
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
let expected = indoc! {"
|
||||
- item
|
||||
ˇ- sub item
|
||||
"};
|
||||
cx.assert_editor_state(expected);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_local_worktree_trust(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -37,7 +37,11 @@ use crate::{
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use file_icons::FileIcons;
|
||||
use git::{Oid, blame::BlameEntry, commit::ParsedCommitMessage, status::FileStatus};
|
||||
use git::{
|
||||
Oid,
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
status::FileStatus,
|
||||
};
|
||||
use gpui::{
|
||||
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
|
||||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||
@@ -46,9 +50,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, TextAlign, 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, 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};
|
||||
@@ -66,7 +70,7 @@ use project::{
|
||||
};
|
||||
use settings::{
|
||||
GitGutterSetting, GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring,
|
||||
RelativeLineNumbers, Settings,
|
||||
Settings,
|
||||
};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use std::{
|
||||
@@ -194,6 +198,8 @@ pub struct EditorElement {
|
||||
style: EditorStyle,
|
||||
}
|
||||
|
||||
type DisplayRowDelta = u32;
|
||||
|
||||
impl EditorElement {
|
||||
pub(crate) const SCROLLBAR_WIDTH: Pixels = px(15.);
|
||||
|
||||
@@ -588,6 +594,8 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::show_signature_help);
|
||||
register_action(editor, window, Editor::signature_help_prev);
|
||||
register_action(editor, window, Editor::signature_help_next);
|
||||
register_action(editor, window, Editor::next_edit_prediction);
|
||||
register_action(editor, window, Editor::previous_edit_prediction);
|
||||
register_action(editor, window, Editor::show_edit_prediction);
|
||||
register_action(editor, window, Editor::context_menu_first);
|
||||
register_action(editor, window, Editor::context_menu_prev);
|
||||
@@ -1695,13 +1703,9 @@ 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)
|
||||
+ 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;
|
||||
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;
|
||||
if block_width == Pixels::ZERO {
|
||||
block_width = em_advance;
|
||||
}
|
||||
@@ -3227,6 +3231,64 @@ impl EditorElement {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn calculate_relative_line_numbers(
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
rows: &Range<DisplayRow>,
|
||||
relative_to: Option<DisplayRow>,
|
||||
count_wrapped_lines: bool,
|
||||
) -> HashMap<DisplayRow, DisplayRowDelta> {
|
||||
let mut relative_rows: HashMap<DisplayRow, DisplayRowDelta> = Default::default();
|
||||
let Some(relative_to) = relative_to else {
|
||||
return relative_rows;
|
||||
};
|
||||
|
||||
let start = rows.start.min(relative_to);
|
||||
let end = rows.end.max(relative_to);
|
||||
|
||||
let buffer_rows = snapshot
|
||||
.row_infos(start)
|
||||
.take(1 + end.minus(start) as usize)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let head_idx = relative_to.minus(start);
|
||||
let mut delta = 1;
|
||||
let mut i = head_idx + 1;
|
||||
let should_count_line = |row_info: &RowInfo| {
|
||||
if count_wrapped_lines {
|
||||
row_info.buffer_row.is_some() || row_info.wrapped_buffer_row.is_some()
|
||||
} else {
|
||||
row_info.buffer_row.is_some()
|
||||
}
|
||||
};
|
||||
while i < buffer_rows.len() as u32 {
|
||||
if should_count_line(&buffer_rows[i as usize]) {
|
||||
if rows.contains(&DisplayRow(i + start.0)) {
|
||||
relative_rows.insert(DisplayRow(i + start.0), delta);
|
||||
}
|
||||
delta += 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
delta = 1;
|
||||
i = head_idx.min(buffer_rows.len().saturating_sub(1) as u32);
|
||||
while i > 0 && buffer_rows[i as usize].buffer_row.is_none() && !count_wrapped_lines {
|
||||
i -= 1;
|
||||
}
|
||||
|
||||
while i > 0 {
|
||||
i -= 1;
|
||||
if should_count_line(&buffer_rows[i as usize]) {
|
||||
if rows.contains(&DisplayRow(i + start.0)) {
|
||||
relative_rows.insert(DisplayRow(i + start.0), delta);
|
||||
}
|
||||
delta += 1;
|
||||
}
|
||||
}
|
||||
|
||||
relative_rows
|
||||
}
|
||||
|
||||
fn layout_line_numbers(
|
||||
&self,
|
||||
gutter_hitbox: Option<&Hitbox>,
|
||||
@@ -3236,7 +3298,7 @@ impl EditorElement {
|
||||
rows: Range<DisplayRow>,
|
||||
buffer_rows: &[RowInfo],
|
||||
active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
|
||||
relative_line_base: Option<DisplayRow>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -3248,16 +3310,32 @@ impl EditorElement {
|
||||
return Arc::default();
|
||||
}
|
||||
|
||||
let relative = self.editor.read(cx).relative_line_numbers(cx);
|
||||
let (newest_selection_head, relative) = self.editor.update(cx, |editor, cx| {
|
||||
let newest_selection_head = newest_selection_head.unwrap_or_else(|| {
|
||||
let newest = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx));
|
||||
SelectionLayout::new(
|
||||
newest,
|
||||
editor.selections.line_mode(),
|
||||
editor.cursor_offset_on_selection,
|
||||
editor.cursor_shape,
|
||||
&snapshot.display_snapshot,
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.head
|
||||
});
|
||||
let relative = editor.relative_line_numbers(cx);
|
||||
(newest_selection_head, relative)
|
||||
});
|
||||
|
||||
let relative_line_numbers_enabled = relative.enabled();
|
||||
let relative_rows = if relative_line_numbers_enabled && let Some(base) = relative_line_base
|
||||
{
|
||||
snapshot.calculate_relative_line_numbers(&rows, base, relative.wrapped())
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let relative_to = relative_line_numbers_enabled.then(|| newest_selection_head.row());
|
||||
|
||||
let relative_rows =
|
||||
self.calculate_relative_line_numbers(snapshot, &rows, relative_to, relative.wrapped());
|
||||
let mut line_number = String::new();
|
||||
let segments = buffer_rows.iter().enumerate().flat_map(|(ix, row_info)| {
|
||||
let display_row = DisplayRow(rows.start.0 + ix as u32);
|
||||
@@ -4580,8 +4658,6 @@ impl EditorElement {
|
||||
gutter_hitbox: &Hitbox,
|
||||
text_hitbox: &Hitbox,
|
||||
style: &EditorStyle,
|
||||
relative_line_numbers: RelativeLineNumbers,
|
||||
relative_to: Option<DisplayRow>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<StickyHeaders> {
|
||||
@@ -4611,21 +4687,9 @@ 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 number = relative_number
|
||||
.filter(|&delta| delta != 0)
|
||||
.map(|delta| delta.unsigned_abs() as u32)
|
||||
.unwrap_or(start_point.row + 1);
|
||||
let number = (start_point.row + 1).to_string();
|
||||
let color = cx.theme().colors().editor_line_number;
|
||||
self.shape_line_number(SharedString::from(number.to_string()), color, window)
|
||||
self.shape_line_number(SharedString::from(number), color, window)
|
||||
});
|
||||
|
||||
lines.push(StickyHeaderLine::new(
|
||||
@@ -5359,12 +5423,6 @@ impl EditorElement {
|
||||
.max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
|
||||
);
|
||||
|
||||
// Don't show hover popovers when context menu is open to avoid overlap
|
||||
let has_context_menu = self.editor.read(cx).mouse_context_menu.is_some();
|
||||
if has_context_menu {
|
||||
return;
|
||||
}
|
||||
|
||||
let hover_popovers = self.editor.update(cx, |editor, cx| {
|
||||
editor.hover_state.render(
|
||||
snapshot,
|
||||
@@ -6164,25 +6222,10 @@ 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,
|
||||
TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.log_err()
|
||||
line.paint(hitbox.origin, line_height, window, cx).log_err()
|
||||
} else {
|
||||
shaped_line
|
||||
.paint(
|
||||
hitbox.origin,
|
||||
line_height,
|
||||
TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.paint(hitbox.origin, line_height, window, cx)
|
||||
.log_err()
|
||||
}) else {
|
||||
continue;
|
||||
@@ -7271,27 +7314,23 @@ 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)
|
||||
+ alignment_offset,
|
||||
line_layout.x_for_index(range.start.column() as usize),
|
||||
) - layout.position_map.scroll_pixel_position.x,
|
||||
)
|
||||
} else {
|
||||
layout.content_origin.x + alignment_offset
|
||||
layout.content_origin.x
|
||||
- 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)
|
||||
+ alignment_offset,
|
||||
line_layout.x_for_index(range.end.column() as usize),
|
||||
) - layout.position_map.scroll_pixel_position.x,
|
||||
)
|
||||
} else {
|
||||
@@ -7299,7 +7338,6 @@ impl EditorElement {
|
||||
ScrollPixelOffset::from(
|
||||
layout.content_origin.x
|
||||
+ line_layout.width
|
||||
+ alignment_offset
|
||||
+ line_end_overshoot,
|
||||
) - layout.position_map.scroll_pixel_position.x,
|
||||
)
|
||||
@@ -8540,15 +8578,8 @@ impl LineWithInvisibles {
|
||||
for fragment in &self.fragments {
|
||||
match fragment {
|
||||
LineFragment::Text(line) => {
|
||||
line.paint(
|
||||
fragment_origin,
|
||||
line_height,
|
||||
layout.text_align,
|
||||
Some(layout.content_width),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
line.paint(fragment_origin, line_height, window, cx)
|
||||
.log_err();
|
||||
fragment_origin.x += line.width;
|
||||
}
|
||||
LineFragment::Element { size, .. } => {
|
||||
@@ -8590,15 +8621,8 @@ impl LineWithInvisibles {
|
||||
for fragment in &self.fragments {
|
||||
match fragment {
|
||||
LineFragment::Text(line) => {
|
||||
line.paint_background(
|
||||
fragment_origin,
|
||||
line_height,
|
||||
layout.text_align,
|
||||
Some(layout.content_width),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
line.paint_background(fragment_origin, line_height, window, cx)
|
||||
.log_err();
|
||||
fragment_origin.x += line.width;
|
||||
}
|
||||
LineFragment::Element { size, .. } => {
|
||||
@@ -8647,7 +8671,7 @@ impl LineWithInvisibles {
|
||||
[token_offset, token_end_offset],
|
||||
Box::new(move |window: &mut Window, cx: &mut App| {
|
||||
invisible_symbol
|
||||
.paint(origin, line_height, TextAlign::Left, None, window, cx)
|
||||
.paint(origin, line_height, window, cx)
|
||||
.log_err();
|
||||
}),
|
||||
)
|
||||
@@ -8808,15 +8832,6 @@ 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)]
|
||||
@@ -9421,28 +9436,6 @@ impl Element for EditorElement {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
// 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 newest = editor
|
||||
.selections
|
||||
.newest::<Point>(&editor.display_snapshot(cx));
|
||||
Some(SelectionLayout::new(
|
||||
newest,
|
||||
editor.selections.line_mode(),
|
||||
editor.cursor_offset_on_selection,
|
||||
editor.cursor_shape,
|
||||
&snapshot.display_snapshot,
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
)
|
||||
.head.row())
|
||||
});
|
||||
|
||||
let mut breakpoint_rows = self.editor.update(cx, |editor, cx| {
|
||||
editor.active_breakpoints(start_row..end_row, window, cx)
|
||||
});
|
||||
@@ -9460,7 +9453,7 @@ impl Element for EditorElement {
|
||||
start_row..end_row,
|
||||
&row_infos,
|
||||
&active_rows,
|
||||
relative_row_base,
|
||||
newest_selection_head,
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -9780,7 +9773,6 @@ impl Element for EditorElement {
|
||||
&& is_singleton
|
||||
&& EditorSettings::get_global(cx).sticky_scroll.enabled
|
||||
{
|
||||
let relative = self.editor.read(cx).relative_line_numbers(cx);
|
||||
self.layout_sticky_headers(
|
||||
&snapshot,
|
||||
editor_width,
|
||||
@@ -9792,8 +9784,6 @@ impl Element for EditorElement {
|
||||
&gutter_hitbox,
|
||||
&text_hitbox,
|
||||
&style,
|
||||
relative,
|
||||
relative_row_base,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -10219,8 +10209,6 @@ 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
|
||||
@@ -10274,8 +10262,6 @@ impl Element for EditorElement {
|
||||
sticky_buffer_header,
|
||||
sticky_headers,
|
||||
expand_toggles,
|
||||
text_align: self.style.text.text_align,
|
||||
content_width: text_hitbox.size.width,
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -10456,8 +10442,6 @@ 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 {
|
||||
@@ -10625,9 +10609,7 @@ impl StickyHeaderLine {
|
||||
gutter_origin.x + gutter_width - gutter_right_padding - line_number.width,
|
||||
gutter_origin.y,
|
||||
);
|
||||
line_number
|
||||
.paint(origin, line_height, TextAlign::Left, None, window, cx)
|
||||
.log_err();
|
||||
line_number.paint(origin, line_height, window, cx).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11066,8 +11048,6 @@ 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)>,
|
||||
@@ -11133,12 +11113,10 @@ impl PositionMap {
|
||||
.line_layouts
|
||||
.get(row as usize - scroll_position.y as usize)
|
||||
{
|
||||
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) {
|
||||
if let Some(ix) = line.index_for_x(x) {
|
||||
(ix as u32, px(0.))
|
||||
} else {
|
||||
(line.len as u32, px(0.).max(x_relative_to_text - line.width))
|
||||
(line.len as u32, px(0.).max(x - line.width))
|
||||
}
|
||||
} else {
|
||||
(0, x)
|
||||
@@ -11327,14 +11305,7 @@ impl CursorLayout {
|
||||
|
||||
if let Some(block_text) = &self.block_text {
|
||||
block_text
|
||||
.paint(
|
||||
self.origin + origin,
|
||||
self.line_height,
|
||||
TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.paint(self.origin + origin, self.line_height, window, cx)
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
@@ -11660,7 +11631,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_layout_line_numbers(cx: &mut TestAppContext) {
|
||||
fn test_shape_line_numbers(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
|
||||
@@ -11700,7 +11671,7 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
&BTreeMap::default(),
|
||||
Some(DisplayRow(0)),
|
||||
Some(DisplayPoint::new(DisplayRow(0), 0)),
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -11712,9 +11683,10 @@ mod tests {
|
||||
let relative_rows = window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
snapshot.calculate_relative_line_numbers(
|
||||
element.calculate_relative_line_numbers(
|
||||
&snapshot,
|
||||
&(DisplayRow(0)..DisplayRow(6)),
|
||||
DisplayRow(3),
|
||||
Some(DisplayRow(3)),
|
||||
false,
|
||||
)
|
||||
})
|
||||
@@ -11730,9 +11702,10 @@ mod tests {
|
||||
let relative_rows = window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
snapshot.calculate_relative_line_numbers(
|
||||
element.calculate_relative_line_numbers(
|
||||
&snapshot,
|
||||
&(DisplayRow(3)..DisplayRow(6)),
|
||||
DisplayRow(1),
|
||||
Some(DisplayRow(1)),
|
||||
false,
|
||||
)
|
||||
})
|
||||
@@ -11746,9 +11719,10 @@ mod tests {
|
||||
let relative_rows = window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
snapshot.calculate_relative_line_numbers(
|
||||
element.calculate_relative_line_numbers(
|
||||
&snapshot,
|
||||
&(DisplayRow(0)..DisplayRow(3)),
|
||||
DisplayRow(6),
|
||||
Some(DisplayRow(6)),
|
||||
false,
|
||||
)
|
||||
})
|
||||
@@ -11785,7 +11759,7 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
&BTreeMap::default(),
|
||||
Some(DisplayRow(0)),
|
||||
Some(DisplayPoint::new(DisplayRow(0), 0)),
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -11800,7 +11774,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_layout_line_numbers_wrapping(cx: &mut TestAppContext) {
|
||||
fn test_shape_line_numbers_wrapping(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
|
||||
@@ -11845,7 +11819,7 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
&BTreeMap::default(),
|
||||
Some(DisplayRow(0)),
|
||||
Some(DisplayPoint::new(DisplayRow(0), 0)),
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -11857,9 +11831,10 @@ mod tests {
|
||||
let relative_rows = window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
snapshot.calculate_relative_line_numbers(
|
||||
element.calculate_relative_line_numbers(
|
||||
&snapshot,
|
||||
&(DisplayRow(0)..DisplayRow(6)),
|
||||
DisplayRow(3),
|
||||
Some(DisplayRow(3)),
|
||||
true,
|
||||
)
|
||||
})
|
||||
@@ -11896,7 +11871,7 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
&BTreeMap::from_iter([(DisplayRow(0), LineHighlightSpec::default())]),
|
||||
Some(DisplayRow(0)),
|
||||
Some(DisplayPoint::new(DisplayRow(0), 0)),
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -11911,9 +11886,10 @@ mod tests {
|
||||
let relative_rows = window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
snapshot.calculate_relative_line_numbers(
|
||||
element.calculate_relative_line_numbers(
|
||||
&snapshot,
|
||||
&(DisplayRow(0)..DisplayRow(6)),
|
||||
DisplayRow(3),
|
||||
Some(DisplayRow(3)),
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -3,9 +3,9 @@ use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
|
||||
use git::{
|
||||
GitHostingProviderRegistry, Oid,
|
||||
blame::{Blame, BlameEntry},
|
||||
commit::ParsedCommitMessage,
|
||||
GitHostingProviderRegistry, GitRemote, Oid,
|
||||
blame::{Blame, BlameEntry, ParsedCommitMessage},
|
||||
parse_git_remote_url,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task,
|
||||
@@ -525,7 +525,12 @@ impl GitBlame {
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
|
||||
.and_then(|(repo, _)| repo.read(cx).default_remote_url());
|
||||
.and_then(|(repo, _)| {
|
||||
repo.read(cx)
|
||||
.remote_upstream_url
|
||||
.clone()
|
||||
.or(repo.read(cx).remote_origin_url.clone())
|
||||
});
|
||||
let blame_buffer = project
|
||||
.update(cx, |project, cx| project.blame_buffer(&buffer, None, cx));
|
||||
Ok(async move {
|
||||
@@ -549,19 +554,13 @@ impl GitBlame {
|
||||
entries,
|
||||
snapshot.max_point().row,
|
||||
);
|
||||
let commit_details = messages
|
||||
.into_iter()
|
||||
.map(|(oid, message)| {
|
||||
let parsed_commit_message =
|
||||
ParsedCommitMessage::parse(
|
||||
oid.to_string(),
|
||||
message,
|
||||
remote_url.as_deref(),
|
||||
Some(provider_registry.clone()),
|
||||
);
|
||||
(oid, parsed_commit_message)
|
||||
})
|
||||
.collect();
|
||||
let commit_details = parse_commit_messages(
|
||||
messages,
|
||||
remote_url,
|
||||
provider_registry.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
res.push((
|
||||
id,
|
||||
snapshot,
|
||||
@@ -681,6 +680,55 @@ fn build_blame_entry_sum_tree(entries: Vec<BlameEntry>, max_row: u32) -> SumTree
|
||||
entries
|
||||
}
|
||||
|
||||
async fn parse_commit_messages(
|
||||
messages: impl IntoIterator<Item = (Oid, String)>,
|
||||
remote_url: Option<String>,
|
||||
provider_registry: Arc<GitHostingProviderRegistry>,
|
||||
) -> HashMap<Oid, ParsedCommitMessage> {
|
||||
let mut commit_details = HashMap::default();
|
||||
|
||||
let parsed_remote_url = remote_url
|
||||
.as_deref()
|
||||
.and_then(|remote_url| parse_git_remote_url(provider_registry, remote_url));
|
||||
|
||||
for (oid, message) in messages {
|
||||
let permalink = if let Some((provider, git_remote)) = parsed_remote_url.as_ref() {
|
||||
Some(provider.build_commit_permalink(
|
||||
git_remote,
|
||||
git::BuildCommitPermalinkParams {
|
||||
sha: oid.to_string().as_str(),
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let remote = parsed_remote_url
|
||||
.as_ref()
|
||||
.map(|(provider, remote)| GitRemote {
|
||||
host: provider.clone(),
|
||||
owner: remote.owner.clone().into(),
|
||||
repo: remote.repo.clone().into(),
|
||||
});
|
||||
|
||||
let pull_request = parsed_remote_url
|
||||
.as_ref()
|
||||
.and_then(|(provider, remote)| provider.extract_pull_request(remote, &message));
|
||||
|
||||
commit_details.insert(
|
||||
oid,
|
||||
ParsedCommitMessage {
|
||||
message: message.into(),
|
||||
permalink,
|
||||
remote,
|
||||
pull_request,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
commit_details
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -17,8 +17,8 @@ use gpui::{
|
||||
ParentElement, Pixels, SharedString, Styled, Task, WeakEntity, Window, point,
|
||||
};
|
||||
use language::{
|
||||
Bias, Buffer, BufferRow, CharKind, CharScopeContext, LocalFile, Point, SelectionGoal,
|
||||
proto::serialize_anchor as serialize_text_anchor,
|
||||
Bias, Buffer, BufferRow, CharKind, CharScopeContext, DiskState, 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().is_deleted());
|
||||
.is_some_and(|file| file.disk_state() == DiskState::Deleted);
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
};
|
||||
use gpui::{Bounds, Context, Pixels, Window};
|
||||
use language::Point;
|
||||
use multi_buffer::{Anchor, ToPoint};
|
||||
use multi_buffer::Anchor;
|
||||
use std::cmp;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
@@ -186,19 +186,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
let style = self.style(cx).clone();
|
||||
let sticky_headers = self.sticky_headers(&style, cx).unwrap_or_default();
|
||||
let visible_sticky_headers = sticky_headers
|
||||
.iter()
|
||||
.filter(|h| {
|
||||
let buffer_snapshot = display_map.buffer_snapshot();
|
||||
let buffer_range =
|
||||
h.range.start.to_point(buffer_snapshot)..h.range.end.to_point(buffer_snapshot);
|
||||
|
||||
buffer_range.contains(&Point::new(target_top as u32, 0))
|
||||
})
|
||||
.count();
|
||||
|
||||
let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
|
||||
0.
|
||||
} else {
|
||||
@@ -231,7 +218,7 @@ impl Editor {
|
||||
let was_autoscrolled = match strategy {
|
||||
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
|
||||
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
|
||||
let target_top = (target_top - margin - visible_sticky_headers as f64).max(0.0);
|
||||
let target_top = (target_top - margin).max(0.0);
|
||||
let target_bottom = target_bottom + margin;
|
||||
let start_row = scroll_position.y;
|
||||
let end_row = start_row + visible_lines;
|
||||
|
||||
@@ -205,49 +205,6 @@ impl EditorLspTestContext {
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
"#})),
|
||||
text_objects: Some(Cow::from(indoc! {r#"
|
||||
(function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(method_definition
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
; Arrow function in variable declaration - capture the full declaration
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
]) @function.around
|
||||
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function)))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function)))
|
||||
]) @function.around
|
||||
|
||||
; Catch-all for arrow functions in other contexts (callbacks, etc.)
|
||||
((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
|
||||
"#})),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Could not parse queries");
|
||||
@@ -319,49 +276,6 @@ impl EditorLspTestContext {
|
||||
(jsx_opening_element) @start
|
||||
(jsx_closing_element)? @end) @indent
|
||||
"#})),
|
||||
text_objects: Some(Cow::from(indoc! {r#"
|
||||
(function_declaration
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
(method_definition
|
||||
body: (_
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}")) @function.around
|
||||
|
||||
; Arrow function in variable declaration - capture the full declaration
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function
|
||||
body: (statement_block
|
||||
"{"
|
||||
(_)* @function.inside
|
||||
"}"))))
|
||||
]) @function.around
|
||||
|
||||
([
|
||||
(lexical_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function)))
|
||||
(variable_declaration
|
||||
(variable_declarator
|
||||
value: (arrow_function)))
|
||||
]) @function.around
|
||||
|
||||
; Catch-all for arrow functions in other contexts (callbacks, etc.)
|
||||
((arrow_function) @function.around (#not-has-parent? @function.around variable_declarator))
|
||||
"#})),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Could not parse queries");
|
||||
|
||||
@@ -1 +1 @@
|
||||
../../LICENSE-GPL
|
||||
LICENSE-GPL
|
||||
@@ -19,9 +19,6 @@ impl Global for GlobalExtensionHostProxy {}
|
||||
///
|
||||
/// This object implements each of the individual proxy types so that their
|
||||
/// methods can be called directly on it.
|
||||
/// Registration function for language model providers.
|
||||
pub type LanguageModelProviderRegistration = Box<dyn FnOnce(&mut App) + Send>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExtensionHostProxy {
|
||||
theme_proxy: RwLock<Option<Arc<dyn ExtensionThemeProxy>>>,
|
||||
@@ -32,7 +29,6 @@ pub struct ExtensionHostProxy {
|
||||
slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
|
||||
context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
|
||||
debug_adapter_provider_proxy: RwLock<Option<Arc<dyn ExtensionDebugAdapterProviderProxy>>>,
|
||||
language_model_provider_proxy: RwLock<Option<Arc<dyn ExtensionLanguageModelProviderProxy>>>,
|
||||
}
|
||||
|
||||
impl ExtensionHostProxy {
|
||||
@@ -58,7 +54,6 @@ impl ExtensionHostProxy {
|
||||
slash_command_proxy: RwLock::default(),
|
||||
context_server_proxy: RwLock::default(),
|
||||
debug_adapter_provider_proxy: RwLock::default(),
|
||||
language_model_provider_proxy: RwLock::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,15 +90,6 @@ impl ExtensionHostProxy {
|
||||
.write()
|
||||
.replace(Arc::new(proxy));
|
||||
}
|
||||
|
||||
pub fn register_language_model_provider_proxy(
|
||||
&self,
|
||||
proxy: impl ExtensionLanguageModelProviderProxy,
|
||||
) {
|
||||
self.language_model_provider_proxy
|
||||
.write()
|
||||
.replace(Arc::new(proxy));
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionThemeProxy: Send + Sync + 'static {
|
||||
@@ -460,37 +446,3 @@ impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy {
|
||||
proxy.unregister_debug_locator(locator_name)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExtensionLanguageModelProviderProxy: Send + Sync + 'static {
|
||||
fn register_language_model_provider(
|
||||
&self,
|
||||
provider_id: Arc<str>,
|
||||
register_fn: LanguageModelProviderRegistration,
|
||||
cx: &mut App,
|
||||
);
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +93,6 @@ pub struct ExtensionManifest {
|
||||
pub debug_adapters: BTreeMap<Arc<str>, DebugAdapterManifestEntry>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub debug_locators: BTreeMap<Arc<str>, DebugLocatorManifestEntry>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub language_model_providers: BTreeMap<Arc<str>, LanguageModelProviderManifestEntry>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
@@ -290,16 +288,6 @@ pub struct DebugAdapterManifestEntry {
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct DebugLocatorManifestEntry {}
|
||||
|
||||
/// Manifest entry for a language model provider.
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct LanguageModelProviderManifestEntry {
|
||||
/// Display name for the provider.
|
||||
pub name: String,
|
||||
/// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg").
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtensionManifest {
|
||||
pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
|
||||
let extension_name = extension_dir
|
||||
@@ -370,7 +358,6 @@ fn manifest_from_old_manifest(
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +391,6 @@ mod tests {
|
||||
capabilities: vec![],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -331,6 +331,7 @@ static mut EXTENSION: Option<Box<dyn Extension>> = None;
|
||||
pub static ZED_API_VERSION: [u8; 6] = *include_bytes!(concat!(env!("OUT_DIR"), "/version_bytes"));
|
||||
|
||||
mod wit {
|
||||
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-extension"],
|
||||
path: "./wit/since_v0.8.0",
|
||||
@@ -523,12 +524,6 @@ impl wit::Guest for Component {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct LanguageServerId(String);
|
||||
|
||||
impl LanguageServerId {
|
||||
pub fn new(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for LanguageServerId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
@@ -545,12 +540,6 @@ impl fmt::Display for LanguageServerId {
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct ContextServerId(String);
|
||||
|
||||
impl ContextServerId {
|
||||
pub fn new(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ContextServerId {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
|
||||
@@ -148,7 +148,6 @@ fn manifest() -> ExtensionManifest {
|
||||
)],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ mod tests {
|
||||
capabilities: vec![],
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -197,7 +196,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
@@ -378,7 +376,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
capabilities: Vec::new(),
|
||||
debug_adapters: Default::default(),
|
||||
debug_locators: Default::default(),
|
||||
language_model_providers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
|
||||
@@ -434,18 +434,7 @@ impl RealFs {
|
||||
for component in path.components() {
|
||||
match component {
|
||||
std::path::Component::Prefix(_) => {
|
||||
let component = component.as_os_str();
|
||||
let canonicalized = if component
|
||||
.to_str()
|
||||
.map(|e| e.ends_with("\\"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
std::fs::canonicalize(component)
|
||||
} else {
|
||||
let mut component = component.to_os_string();
|
||||
component.push("\\");
|
||||
std::fs::canonicalize(component)
|
||||
}?;
|
||||
let canonicalized = std::fs::canonicalize(component)?;
|
||||
|
||||
let mut strip = PathBuf::new();
|
||||
for component in canonicalized.components() {
|
||||
@@ -3405,26 +3394,6 @@ mod tests {
|
||||
assert_eq!(content, "Hello");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn test_realfs_canonicalize(executor: BackgroundExecutor) {
|
||||
use util::paths::SanitizedPath;
|
||||
|
||||
let fs = RealFs {
|
||||
bundled_git_binary_path: None,
|
||||
executor,
|
||||
next_job_id: Arc::new(AtomicUsize::new(0)),
|
||||
job_event_subscribers: Arc::new(Mutex::new(Vec::new())),
|
||||
};
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let file = temp_dir.path().join("test (1).txt");
|
||||
let file = SanitizedPath::new(&file);
|
||||
std::fs::write(&file, "test").unwrap();
|
||||
|
||||
let canonicalized = fs.canonicalize(file.as_path()).await;
|
||||
assert!(canonicalized.is_ok());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rename(executor: BackgroundExecutor) {
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::Oid;
|
||||
use crate::commit::get_messages;
|
||||
use crate::repository::RepoPath;
|
||||
use crate::{GitRemote, Oid};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::AsyncWriteExt;
|
||||
use gpui::SharedString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Stdio;
|
||||
use std::{ops::Range, path::Path};
|
||||
@@ -20,6 +21,14 @@ pub struct Blame {
|
||||
pub messages: HashMap<Oid, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ParsedCommitMessage {
|
||||
pub message: SharedString,
|
||||
pub permalink: Option<url::Url>,
|
||||
pub pull_request: Option<crate::hosting_provider::PullRequest>,
|
||||
pub remote: Option<GitRemote>,
|
||||
}
|
||||
|
||||
impl Blame {
|
||||
pub async fn for_path(
|
||||
git_binary: &Path,
|
||||
|
||||
@@ -1,52 +1,7 @@
|
||||
use crate::{
|
||||
BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, Oid, parse_git_remote_url,
|
||||
status::StatusCode,
|
||||
};
|
||||
use crate::{Oid, status::StatusCode};
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::SharedString;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ParsedCommitMessage {
|
||||
pub message: SharedString,
|
||||
pub permalink: Option<url::Url>,
|
||||
pub pull_request: Option<crate::hosting_provider::PullRequest>,
|
||||
pub remote: Option<GitRemote>,
|
||||
}
|
||||
|
||||
impl ParsedCommitMessage {
|
||||
pub fn parse(
|
||||
sha: String,
|
||||
message: String,
|
||||
remote_url: Option<&str>,
|
||||
provider_registry: Option<Arc<GitHostingProviderRegistry>>,
|
||||
) -> Self {
|
||||
if let Some((hosting_provider, remote)) = provider_registry
|
||||
.and_then(|reg| remote_url.and_then(|url| parse_git_remote_url(reg, url)))
|
||||
{
|
||||
let pull_request = hosting_provider.extract_pull_request(&remote, &message);
|
||||
Self {
|
||||
message: message.into(),
|
||||
permalink: Some(
|
||||
hosting_provider
|
||||
.build_commit_permalink(&remote, BuildCommitPermalinkParams { sha: &sha }),
|
||||
),
|
||||
pull_request,
|
||||
remote: Some(GitRemote {
|
||||
host: hosting_provider,
|
||||
owner: remote.owner.into(),
|
||||
repo: remote.repo.into(),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
message: message.into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
use std::path::Path;
|
||||
|
||||
pub async fn get_messages(working_directory: &Path, shas: &[Oid]) -> Result<HashMap<Oid, String>> {
|
||||
if shas.is_empty() {
|
||||
|
||||
@@ -3,7 +3,10 @@ use crate::{
|
||||
commit_view::CommitView,
|
||||
};
|
||||
use editor::{BlameRenderer, Editor, hover_markdown_style};
|
||||
use git::{blame::BlameEntry, commit::ParsedCommitMessage, repository::CommitSummary};
|
||||
use git::{
|
||||
blame::{BlameEntry, ParsedCommitMessage},
|
||||
repository::CommitSummary,
|
||||
};
|
||||
use gpui::{
|
||||
ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle,
|
||||
TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
use gpui::{App, Context, WeakEntity, Window};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use std::sync::Arc;
|
||||
use ui::{Color, IconName, SharedString};
|
||||
use util::ResultExt;
|
||||
use workspace::{self, Workspace};
|
||||
|
||||
pub fn clone_and_open(
|
||||
repo_url: SharedString,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
on_success: Arc<
|
||||
dyn Fn(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + Sync + 'static,
|
||||
>,
|
||||
) {
|
||||
let destination_prompt = cx.prompt_for_paths(gpui::PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
prompt: Some("Select as Repository Destination".into()),
|
||||
});
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let mut paths = destination_prompt.await.ok()?.ok()??;
|
||||
let mut destination_dir = paths.pop()?;
|
||||
|
||||
let repo_name = repo_url
|
||||
.split('/')
|
||||
.next_back()
|
||||
.map(|name| name.strip_suffix(".git").unwrap_or(name))
|
||||
.unwrap_or("repository")
|
||||
.to_owned();
|
||||
|
||||
let clone_task = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let destination_dir = destination_dir.clone();
|
||||
let repo_url = repo_url.clone();
|
||||
cx.spawn(async move |_workspace, _cx| {
|
||||
fs.git_clone(&repo_url, destination_dir.as_path()).await
|
||||
})
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
if let Err(error) = clone_task.await {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let toast = StatusToast::new(error.to_string(), cx, |this, _| {
|
||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||
.dismiss_button(true)
|
||||
});
|
||||
workspace.toggle_status_toast(toast, cx);
|
||||
})
|
||||
.log_err();
|
||||
return None;
|
||||
}
|
||||
|
||||
let has_worktrees = workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
workspace.project().read(cx).worktrees(cx).next().is_some()
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let prompt_answer = if has_worktrees {
|
||||
cx.update(|window, cx| {
|
||||
window.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
&format!("Git Clone: {}", repo_name),
|
||||
None,
|
||||
&["Add repo to project", "Open repo in new project"],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()?
|
||||
.await
|
||||
.ok()?
|
||||
} else {
|
||||
// Don't ask if project is empty
|
||||
0
|
||||
};
|
||||
|
||||
destination_dir.push(&repo_name);
|
||||
|
||||
match prompt_answer {
|
||||
0 => {
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let create_task = workspace.project().update(cx, |project, cx| {
|
||||
project.create_worktree(destination_dir.as_path(), true, cx)
|
||||
});
|
||||
|
||||
let workspace_weak = cx.weak_entity();
|
||||
let on_success = on_success.clone();
|
||||
cx.spawn_in(window, async move |_window, cx| {
|
||||
if create_task.await.log_err().is_some() {
|
||||
workspace_weak
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
(on_success)(workspace, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.ok()?;
|
||||
}
|
||||
1 => {
|
||||
workspace
|
||||
.update(cx, move |workspace, cx| {
|
||||
let app_state = workspace.app_state().clone();
|
||||
let destination_path = destination_dir.clone();
|
||||
let on_success = on_success.clone();
|
||||
|
||||
workspace::open_new(
|
||||
Default::default(),
|
||||
app_state,
|
||||
cx,
|
||||
move |workspace, window, cx| {
|
||||
cx.activate(true);
|
||||
|
||||
let create_task =
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.create_worktree(
|
||||
destination_path.as_path(),
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let workspace_weak = cx.weak_entity();
|
||||
cx.spawn_in(window, async move |_window, cx| {
|
||||
if create_task.await.log_err().is_some() {
|
||||
workspace_weak
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
(on_success)(workspace, window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use editor::hover_markdown_style;
|
||||
use futures::Future;
|
||||
use git::blame::BlameEntry;
|
||||
use git::repository::CommitSummary;
|
||||
use git::{GitRemote, commit::ParsedCommitMessage};
|
||||
use git::{GitRemote, blame::ParsedCommitMessage};
|
||||
use gpui::{
|
||||
App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
|
||||
StatefulInteractiveElement, WeakEntity, prelude::*,
|
||||
|
||||
@@ -69,7 +69,7 @@ struct GitBlob {
|
||||
path: RepoPath,
|
||||
worktree_id: WorktreeId,
|
||||
is_deleted: bool,
|
||||
display_name: String,
|
||||
display_name: Arc<str>,
|
||||
}
|
||||
|
||||
const COMMIT_MESSAGE_SORT_PREFIX: u64 = 0;
|
||||
@@ -243,8 +243,9 @@ impl CommitView {
|
||||
.path
|
||||
.file_name()
|
||||
.map(|name| name.to_string())
|
||||
.unwrap_or_else(|| file.path.display(PathStyle::local()).to_string());
|
||||
let display_name = format!("{short_sha} - {file_name}");
|
||||
.unwrap_or_else(|| file.path.display(PathStyle::Posix).to_string());
|
||||
let display_name: Arc<str> =
|
||||
Arc::from(format!("{short_sha} - {file_name}").into_boxed_str());
|
||||
|
||||
let file = Arc::new(GitBlob {
|
||||
path: file.path.clone(),
|
||||
@@ -660,13 +661,15 @@ impl language::File for GitBlob {
|
||||
}
|
||||
|
||||
fn disk_state(&self) -> DiskState {
|
||||
DiskState::Historic {
|
||||
was_deleted: self.is_deleted,
|
||||
if self.is_deleted {
|
||||
DiskState::Deleted
|
||||
} else {
|
||||
DiskState::New
|
||||
}
|
||||
}
|
||||
|
||||
fn path_style(&self, _: &App) -> PathStyle {
|
||||
PathStyle::local()
|
||||
PathStyle::Posix
|
||||
}
|
||||
|
||||
fn path(&self) -> &Arc<RelPath> {
|
||||
@@ -694,6 +697,45 @@ impl language::File for GitBlob {
|
||||
}
|
||||
}
|
||||
|
||||
// No longer needed since metadata buffer is not created
|
||||
// impl language::File for CommitMetadataFile {
|
||||
// fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
// None
|
||||
// }
|
||||
//
|
||||
// fn disk_state(&self) -> DiskState {
|
||||
// DiskState::New
|
||||
// }
|
||||
//
|
||||
// fn path_style(&self, _: &App) -> PathStyle {
|
||||
// PathStyle::Posix
|
||||
// }
|
||||
//
|
||||
// fn path(&self) -> &Arc<RelPath> {
|
||||
// &self.title
|
||||
// }
|
||||
//
|
||||
// fn full_path(&self, _: &App) -> PathBuf {
|
||||
// self.title.as_std_path().to_path_buf()
|
||||
// }
|
||||
//
|
||||
// fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
|
||||
// self.title.file_name().unwrap_or("commit")
|
||||
// }
|
||||
//
|
||||
// fn worktree_id(&self, _: &App) -> WorktreeId {
|
||||
// self.worktree_id
|
||||
// }
|
||||
//
|
||||
// fn to_proto(&self, _cx: &App) -> language::proto::File {
|
||||
// unimplemented!()
|
||||
// }
|
||||
//
|
||||
// fn is_private(&self) -> bool {
|
||||
// false
|
||||
// }
|
||||
// }
|
||||
|
||||
async fn build_buffer(
|
||||
mut text: String,
|
||||
blob: Arc<dyn File>,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,6 @@ use ui::{
|
||||
};
|
||||
|
||||
mod blame_ui;
|
||||
pub mod clone;
|
||||
|
||||
use git::{
|
||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use anyhow::Context as _;
|
||||
use collections::HashSet;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
|
||||
use git::repository::Worktree as GitWorktree;
|
||||
@@ -10,11 +9,7 @@ use gpui::{
|
||||
actions, rems,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use project::{
|
||||
DirectoryLister,
|
||||
git_store::Repository,
|
||||
trusted_worktrees::{PathTrust, RemoteHostLocation, TrustedWorktrees},
|
||||
};
|
||||
use project::{DirectoryLister, git_store::Repository};
|
||||
use recent_projects::{RemoteConnectionModal, connect};
|
||||
use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
@@ -224,6 +219,7 @@ impl WorktreeListDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
let workspace = self.workspace.clone();
|
||||
let Some(repo) = self.repo.clone() else {
|
||||
return;
|
||||
};
|
||||
@@ -251,7 +247,6 @@ impl WorktreeListDelegate {
|
||||
|
||||
let branch = worktree_branch.to_string();
|
||||
let window_handle = window.window_handle();
|
||||
let workspace = self.workspace.clone();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
let Some(paths) = worktree_path.await? else {
|
||||
return anyhow::Ok(());
|
||||
@@ -262,32 +257,8 @@ impl WorktreeListDelegate {
|
||||
repo.create_worktree(branch.clone(), path.clone(), commit)
|
||||
})?
|
||||
.await??;
|
||||
let new_worktree_path = path.join(branch);
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
|
||||
let repo_path = &repo.read(cx).snapshot().work_directory_abs_path;
|
||||
let project = workspace.project();
|
||||
if let Some((parent_worktree, _)) =
|
||||
project.read(cx).find_worktree(repo_path, cx)
|
||||
{
|
||||
trusted_worktrees.update(cx, |trusted_worktrees, cx| {
|
||||
if trusted_worktrees.can_trust(parent_worktree.read(cx).id(), cx) {
|
||||
trusted_worktrees.trust(
|
||||
HashSet::from_iter([PathTrust::AbsPath(
|
||||
new_worktree_path.clone(),
|
||||
)]),
|
||||
project
|
||||
.read(cx)
|
||||
.remote_connection_options(cx)
|
||||
.map(RemoteHostLocation::from),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})?;
|
||||
let final_path = path.join(branch);
|
||||
|
||||
let (connection_options, app_state, is_local) =
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
@@ -303,7 +274,7 @@ impl WorktreeListDelegate {
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
replace_current_window,
|
||||
vec![new_worktree_path],
|
||||
vec![final_path],
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -312,7 +283,7 @@ impl WorktreeListDelegate {
|
||||
} else if let Some(connection_options) = connection_options {
|
||||
open_remote_worktree(
|
||||
connection_options,
|
||||
vec![new_worktree_path],
|
||||
vec![final_path],
|
||||
app_state,
|
||||
window_handle,
|
||||
replace_current_window,
|
||||
|
||||
@@ -512,8 +512,6 @@ pub enum Model {
|
||||
Gemini25Pro,
|
||||
#[serde(rename = "gemini-3-pro-preview")]
|
||||
Gemini3Pro,
|
||||
#[serde(rename = "gemini-3-flash-preview")]
|
||||
Gemini3Flash,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
@@ -536,7 +534,6 @@ impl Model {
|
||||
Self::Gemini25Flash => "gemini-2.5-flash",
|
||||
Self::Gemini25Pro => "gemini-2.5-pro",
|
||||
Self::Gemini3Pro => "gemini-3-pro-preview",
|
||||
Self::Gemini3Flash => "gemini-3-flash-preview",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
@@ -546,7 +543,6 @@ impl Model {
|
||||
Self::Gemini25Flash => "gemini-2.5-flash",
|
||||
Self::Gemini25Pro => "gemini-2.5-pro",
|
||||
Self::Gemini3Pro => "gemini-3-pro-preview",
|
||||
Self::Gemini3Flash => "gemini-3-flash-preview",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
@@ -557,7 +553,6 @@ impl Model {
|
||||
Self::Gemini25Flash => "Gemini 2.5 Flash",
|
||||
Self::Gemini25Pro => "Gemini 2.5 Pro",
|
||||
Self::Gemini3Pro => "Gemini 3 Pro",
|
||||
Self::Gemini3Flash => "Gemini 3 Flash",
|
||||
Self::Custom {
|
||||
name, display_name, ..
|
||||
} => display_name.as_ref().unwrap_or(name),
|
||||
@@ -566,22 +561,20 @@ impl Model {
|
||||
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
match self {
|
||||
Self::Gemini25FlashLite
|
||||
| Self::Gemini25Flash
|
||||
| Self::Gemini25Pro
|
||||
| Self::Gemini3Pro
|
||||
| Self::Gemini3Flash => 1_048_576,
|
||||
Self::Gemini25FlashLite => 1_048_576,
|
||||
Self::Gemini25Flash => 1_048_576,
|
||||
Self::Gemini25Pro => 1_048_576,
|
||||
Self::Gemini3Pro => 1_048_576,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||
match self {
|
||||
Model::Gemini25FlashLite
|
||||
| Model::Gemini25Flash
|
||||
| Model::Gemini25Pro
|
||||
| Model::Gemini3Pro
|
||||
| Model::Gemini3Flash => Some(65_536),
|
||||
Model::Gemini25FlashLite => Some(65_536),
|
||||
Model::Gemini25Flash => Some(65_536),
|
||||
Model::Gemini25Pro => Some(65_536),
|
||||
Model::Gemini3Pro => Some(65_536),
|
||||
Model::Custom { .. } => None,
|
||||
}
|
||||
}
|
||||
@@ -606,7 +599,6 @@ impl Model {
|
||||
budget_tokens: None,
|
||||
}
|
||||
}
|
||||
Self::Gemini3Flash => GoogleModelMode::Default,
|
||||
Self::Custom { mode, .. } => *mode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,14 +198,14 @@ wayland-backend = { version = "0.3.3", features = [
|
||||
"client_system",
|
||||
"dlopen",
|
||||
], optional = true }
|
||||
wayland-client = { version = "0.31.11", optional = true }
|
||||
wayland-cursor = { version = "0.31.11", optional = true }
|
||||
wayland-protocols = { version = "0.32.9", features = [
|
||||
wayland-client = { version = "0.31.2", optional = true }
|
||||
wayland-cursor = { version = "0.31.1", optional = true }
|
||||
wayland-protocols = { version = "0.31.2", features = [
|
||||
"client",
|
||||
"staging",
|
||||
"unstable",
|
||||
], optional = true }
|
||||
wayland-protocols-plasma = { version = "0.3.9", features = [
|
||||
wayland-protocols-plasma = { version = "0.2.0", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.9", features = [
|
||||
|
||||
@@ -546,15 +546,8 @@ impl Element for TextElement {
|
||||
window.paint_quad(selection)
|
||||
}
|
||||
let line = prepaint.line.take().unwrap();
|
||||
line.paint(
|
||||
bounds.origin,
|
||||
window.line_height(),
|
||||
gpui::TextAlign::Left,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
line.paint(bounds.origin, window.line_height(), window, cx)
|
||||
.unwrap();
|
||||
|
||||
if focus_handle.is_focused(window)
|
||||
&& let Some(cursor) = prepaint.cursor.take()
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
use gpui::{
|
||||
App, Application, Context, Corner, Div, Hsla, Stateful, Window, WindowOptions, anchored,
|
||||
deferred, div, prelude::*, px,
|
||||
};
|
||||
|
||||
/// An example show use deferred to create a floating layers.
|
||||
struct HelloWorld {
|
||||
open: bool,
|
||||
secondary_open: bool,
|
||||
}
|
||||
|
||||
fn button(id: &'static str) -> Stateful<Div> {
|
||||
div()
|
||||
.id(id)
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.px_3()
|
||||
.py_1()
|
||||
}
|
||||
|
||||
fn popover() -> Div {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.shadow_lg()
|
||||
.p_3()
|
||||
.rounded_md()
|
||||
.bg(gpui::white())
|
||||
.text_color(gpui::black())
|
||||
.border_1()
|
||||
.text_sm()
|
||||
.border_color(gpui::black().opacity(0.1))
|
||||
}
|
||||
|
||||
fn line(color: Hsla) -> Div {
|
||||
div().w(px(480.)).h_2().bg(color.opacity(0.25))
|
||||
}
|
||||
|
||||
impl HelloWorld {
|
||||
fn render_secondary_popover(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
button("secondary-btn")
|
||||
.mt_2()
|
||||
.child("Child Popover")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.secondary_open = true;
|
||||
cx.notify();
|
||||
}))
|
||||
.when(self.secondary_open, |this| {
|
||||
this.child(
|
||||
// GPUI can't support deferred here yet,
|
||||
// it was inside another deferred element.
|
||||
anchored()
|
||||
.anchor(Corner::TopLeft)
|
||||
.snap_to_window_with_margin(px(8.))
|
||||
.child(
|
||||
popover()
|
||||
.child("This is second level Popover")
|
||||
.bg(gpui::white())
|
||||
.border_color(gpui::blue())
|
||||
.on_mouse_down_out(cx.listener(|this, _, _, cx| {
|
||||
this.secondary_open = false;
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for HelloWorld {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.size_full()
|
||||
.bg(gpui::white())
|
||||
.text_color(gpui::black())
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_4()
|
||||
.child(
|
||||
button("popover0").child("Opened Popover").child(
|
||||
deferred(
|
||||
anchored()
|
||||
.anchor(Corner::TopLeft)
|
||||
.snap_to_window_with_margin(px(8.))
|
||||
.child(popover().w_96().gap_3().child(
|
||||
"This is a default opened Popover, \
|
||||
we can use deferred to render it \
|
||||
in a floating layer.",
|
||||
)),
|
||||
)
|
||||
.priority(0),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
button("popover1")
|
||||
.child("Open Popover")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.open = true;
|
||||
cx.notify();
|
||||
}))
|
||||
.when(self.open, |this| {
|
||||
this.child(
|
||||
deferred(
|
||||
anchored()
|
||||
.anchor(Corner::TopLeft)
|
||||
.snap_to_window_with_margin(px(8.))
|
||||
.child(
|
||||
popover()
|
||||
.w_96()
|
||||
.gap_3()
|
||||
.child(
|
||||
"This is first level Popover, \
|
||||
we can use deferred to render it \
|
||||
in a floating layer.\n\
|
||||
Click outside to close.",
|
||||
)
|
||||
.when(!self.secondary_open, |this| {
|
||||
this.on_mouse_down_out(cx.listener(
|
||||
|this, _, _, cx| {
|
||||
this.open = false;
|
||||
cx.notify();
|
||||
},
|
||||
))
|
||||
})
|
||||
// Here we need render popover after the content
|
||||
// to ensure it will be on top layer.
|
||||
.child(
|
||||
self.render_secondary_popover(window, cx),
|
||||
),
|
||||
),
|
||||
)
|
||||
.priority(1),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
"Here is an example text rendered, \
|
||||
to ensure the Popover will float above this contents.",
|
||||
)
|
||||
.children([
|
||||
line(gpui::red()),
|
||||
line(gpui::yellow()),
|
||||
line(gpui::blue()),
|
||||
line(gpui::green()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
Application::new().run(|cx: &mut App| {
|
||||
cx.open_window(WindowOptions::default(), |_, cx| {
|
||||
cx.new(|_| HelloWorld {
|
||||
open: false,
|
||||
secondary_open: false,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
cx.activate(true);
|
||||
});
|
||||
}
|
||||
@@ -130,50 +130,6 @@ impl Render for Example {
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("group-1")
|
||||
.tab_index(6)
|
||||
.tab_group()
|
||||
.tab_stop(false)
|
||||
.child(
|
||||
button("group-1-button-1")
|
||||
.tab_index(1)
|
||||
.child("Tab index [6, 1]"),
|
||||
)
|
||||
.child(
|
||||
button("group-1-button-2")
|
||||
.tab_index(2)
|
||||
.child("Tab index [6, 2]"),
|
||||
)
|
||||
.child(
|
||||
button("group-1-button-3")
|
||||
.tab_index(3)
|
||||
.child("Tab index [6, 3]"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("group-2")
|
||||
.tab_index(7)
|
||||
.tab_group()
|
||||
.tab_stop(false)
|
||||
.child(
|
||||
button("group-2-button-1")
|
||||
.tab_index(1)
|
||||
.child("Tab index [7, 1]"),
|
||||
)
|
||||
.child(
|
||||
button("group-2-button-2")
|
||||
.tab_index(2)
|
||||
.child("Tab index [7, 2]"),
|
||||
)
|
||||
.child(
|
||||
button("group-2-button-3")
|
||||
.tab_index(3)
|
||||
.child("Tab index [7, 3]"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user