Compare commits
1 Commits
actually-r
...
git-integr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aa901e70e |
@@ -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:
|
||||
|
||||
4
.github/workflows/autofix_pr.yml
vendored
4
.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
|
||||
|
||||
@@ -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
|
||||
3
.github/workflows/extension_tests.yml
vendored
3
.github/workflows/extension_tests.yml
vendored
@@ -61,7 +61,8 @@ jobs:
|
||||
uses: namespacelabs/nscloud-cache-action@v1
|
||||
with:
|
||||
cache: rust
|
||||
- name: steps::cargo_fmt
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: extension_tests::run_clippy
|
||||
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -26,7 +26,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -71,9 +72,15 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -87,6 +94,8 @@ jobs:
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_windows:
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||
@@ -105,7 +114,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
|
||||
6
.github/workflows/release_nightly.yml
vendored
6
.github/workflows/release_nightly.yml
vendored
@@ -20,7 +20,8 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
fetch-depth: 0
|
||||
- name: steps::cargo_fmt
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/clippy
|
||||
@@ -44,7 +45,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
|
||||
50
.github/workflows/run_tests.yml
vendored
50
.github/workflows/run_tests.yml
vendored
@@ -74,12 +74,19 @@ jobs:
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
with:
|
||||
version: '9'
|
||||
- name: steps::prettier
|
||||
- id: prettier
|
||||
name: steps::prettier
|
||||
run: ./script/prettier
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_fmt
|
||||
- id: cargo_fmt
|
||||
name: steps::cargo_fmt
|
||||
run: cargo fmt --all -- --check
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_style_failure
|
||||
name: steps::record_style_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: ./script/check-todos
|
||||
run: ./script/check-todos
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -90,6 +97,8 @@ jobs:
|
||||
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
|
||||
with:
|
||||
config: ./typos.toml
|
||||
outputs:
|
||||
style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_windows:
|
||||
needs:
|
||||
@@ -110,7 +119,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy.ps1
|
||||
shell: pwsh
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -157,9 +167,15 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- id: record_clippy_failure
|
||||
name: steps::record_clippy_failure
|
||||
if: always()
|
||||
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -173,6 +189,8 @@ jobs:
|
||||
run: |
|
||||
rm -rf ./../.cargo
|
||||
shell: bash -euxo pipefail {0}
|
||||
outputs:
|
||||
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
|
||||
timeout-minutes: 60
|
||||
run_tests_mac:
|
||||
needs:
|
||||
@@ -193,7 +211,8 @@ jobs:
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: steps::clippy
|
||||
- id: clippy
|
||||
name: steps::clippy
|
||||
run: ./script/clippy
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::clear_target_dir_if_large
|
||||
@@ -353,9 +372,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:
|
||||
@@ -576,6 +592,24 @@ jobs:
|
||||
|
||||
exit $EXIT_CODE
|
||||
shell: bash -euxo pipefail {0}
|
||||
call_autofix:
|
||||
needs:
|
||||
- check_style
|
||||
- run_tests_linux
|
||||
if: always() && (needs.check_style.outputs.style_failed == 'true' || needs.run_tests_linux.outputs.clippy_failed == 'true') && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
|
||||
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: run_tests::call_autofix::dispatch_autofix
|
||||
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=${{ needs.run_tests_linux.outputs.clippy_failed == 'true' }}
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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
|
||||
|
||||
45
Cargo.lock
generated
45
Cargo.lock
generated
@@ -3525,33 +3525,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 +5021,8 @@ name = "docs_preprocessor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command_palette",
|
||||
"gpui",
|
||||
"mdbook",
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -5056,6 +5031,7 @@ dependencies = [
|
||||
"task",
|
||||
"theme",
|
||||
"util",
|
||||
"zed",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
@@ -8956,8 +8932,6 @@ dependencies = [
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
"editor",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
@@ -12597,7 +12571,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"notifications",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rayon",
|
||||
@@ -20292,16 +20265,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 +20633,6 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette",
|
||||
"component",
|
||||
"component_preview",
|
||||
"copilot",
|
||||
"crashes",
|
||||
"dap",
|
||||
@@ -20776,6 +20738,7 @@ dependencies = [
|
||||
"tree-sitter-md",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"ui_prompt",
|
||||
"url",
|
||||
"urlencoding",
|
||||
|
||||
@@ -39,7 +39,6 @@ members = [
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/component",
|
||||
"crates/component_preview",
|
||||
"crates/context_server",
|
||||
"crates/copilot",
|
||||
"crates/crashes",
|
||||
@@ -199,7 +198,6 @@ members = [
|
||||
"crates/web_search_providers",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
"crates/worktree_benchmarks",
|
||||
"crates/x_ai",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
@@ -276,7 +274,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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -1323,15 +1319,17 @@
|
||||
// Globs to match files that will be considered "hidden". These files can be hidden from the
|
||||
// project panel by toggling the "hide_hidden" setting.
|
||||
"hidden_files": ["**/.*"],
|
||||
// Git gutter behavior configuration.
|
||||
// Git integration settings.
|
||||
"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.
|
||||
// Master switch to disable all git integration features.
|
||||
// When true, all git features are disabled regardless of other settings.
|
||||
// When false (default), individual features are controlled by their respective settings.
|
||||
"disable_git": false,
|
||||
// Whether to enable git status tracking.
|
||||
// Whether to show git status indicators (modified, added, deleted) in the
|
||||
// project panel, outline panel, and tabs.
|
||||
"enable_status": true,
|
||||
// Whether to enable git diff display.
|
||||
// Whether to show git diff information, including gutter diff indicators
|
||||
// and scrollbar diff markers.
|
||||
"enable_diff": true,
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
|
||||
@@ -11,7 +11,6 @@ use language::language_settings::FormatOnSave;
|
||||
pub use mention::*;
|
||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::to_string_pretty;
|
||||
use settings::Settings as _;
|
||||
use task::{Shell, ShellBuilder};
|
||||
pub use terminal::*;
|
||||
@@ -1993,42 +1992,37 @@ impl AcpThread {
|
||||
fn update_last_checkpoint(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
|
||||
let Some((_, message)) = self.last_user_message() else {
|
||||
let old_checkpoint = if let Some((_, message)) = self.last_user_message() {
|
||||
if let Some(checkpoint) = message.checkpoint.as_ref() {
|
||||
checkpoint.git_checkpoint.clone()
|
||||
} else {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
} else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let Some(user_message_id) = message.id.clone() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let Some(checkpoint) = message.checkpoint.as_ref() else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let old_checkpoint = checkpoint.git_checkpoint.clone();
|
||||
|
||||
let new_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx));
|
||||
cx.spawn(async move |this, cx| {
|
||||
let Some(new_checkpoint) = new_checkpoint
|
||||
let new_checkpoint = new_checkpoint
|
||||
.await
|
||||
.context("failed to get new checkpoint")
|
||||
.log_err()
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let equal = git_store
|
||||
.update(cx, |git, cx| {
|
||||
git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or(true);
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some((ix, message)) = this.user_message_mut(&user_message_id) {
|
||||
if let Some(checkpoint) = message.checkpoint.as_mut() {
|
||||
checkpoint.show = !equal;
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
.log_err();
|
||||
if let Some(new_checkpoint) = new_checkpoint {
|
||||
let equal = git_store
|
||||
.update(cx, |git, cx| {
|
||||
git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or(true);
|
||||
this.update(cx, |this, cx| {
|
||||
let (ix, message) = this.last_user_message().context("no user message")?;
|
||||
let checkpoint = message.checkpoint.as_mut().context("no checkpoint")?;
|
||||
checkpoint.show = !equal;
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -2428,10 +2422,8 @@ fn markdown_for_raw_output(
|
||||
)
|
||||
})),
|
||||
value => Some(cx.new(|cx| {
|
||||
let pretty_json = to_string_pretty(value).unwrap_or_else(|_| value.to_string());
|
||||
|
||||
Markdown::new(
|
||||
format!("```json\n{}\n```", pretty_json).into(),
|
||||
format!("```json\n{}\n```", value).into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
@@ -4074,66 +4066,4 @@ mod tests {
|
||||
"Should have exactly 2 terminals (the completed ones from before checkpoint)"
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that update_last_checkpoint correctly updates the original message's checkpoint
|
||||
/// even when a new user message is added while the async checkpoint comparison is in progress.
|
||||
///
|
||||
/// This is a regression test for a bug where update_last_checkpoint would fail with
|
||||
/// "no checkpoint" if a new user message (without a checkpoint) was added between when
|
||||
/// update_last_checkpoint started and when its async closure ran.
|
||||
#[gpui::test]
|
||||
async fn test_update_last_checkpoint_with_new_message_added(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/test"), json!({".git": {}, "file.txt": "content"}))
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [Path::new(path!("/test"))], cx).await;
|
||||
|
||||
let handler_done = Arc::new(AtomicBool::new(false));
|
||||
let handler_done_clone = handler_done.clone();
|
||||
let connection = Rc::new(FakeAgentConnection::new().on_user_message(
|
||||
move |_, _thread, _cx| {
|
||||
handler_done_clone.store(true, SeqCst);
|
||||
async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) }.boxed_local()
|
||||
},
|
||||
));
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let send_future = thread.update(cx, |thread, cx| thread.send_raw("First message", cx));
|
||||
let send_task = cx.background_executor.spawn(send_future);
|
||||
|
||||
// Tick until handler completes, then a few more to let update_last_checkpoint start
|
||||
while !handler_done.load(SeqCst) {
|
||||
cx.executor().tick();
|
||||
}
|
||||
for _ in 0..5 {
|
||||
cx.executor().tick();
|
||||
}
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_entry(
|
||||
AgentThreadEntry::UserMessage(UserMessage {
|
||||
id: Some(UserMessageId::new()),
|
||||
content: ContentBlock::Empty,
|
||||
chunks: vec!["Injected message (no checkpoint)".into()],
|
||||
checkpoint: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
let result = send_task.await;
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"send should succeed even when new message added during update_last_checkpoint: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<_>>();
|
||||
@@ -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),
|
||||
}]
|
||||
)])
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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| {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2489,11 +2489,9 @@ impl AcpThreadView {
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(input_output_header("Raw Input:".into()))
|
||||
.children(tool_call.raw_input_markdown.clone().map(|input| {
|
||||
div().id(("tool-call-raw-input-markdown", entry_ix)).child(
|
||||
self.render_markdown(
|
||||
input,
|
||||
default_markdown_style(false, false, window, cx),
|
||||
),
|
||||
self.render_markdown(
|
||||
input,
|
||||
default_markdown_style(false, false, window, cx),
|
||||
)
|
||||
}))
|
||||
.child(input_output_header("Output:".into())),
|
||||
@@ -2501,17 +2499,15 @@ impl AcpThreadView {
|
||||
})
|
||||
.children(tool_call.content.iter().enumerate().map(
|
||||
|(content_ix, content)| {
|
||||
div().id(("tool-call-output", entry_ix)).child(
|
||||
self.render_tool_call_content(
|
||||
entry_ix,
|
||||
content,
|
||||
content_ix,
|
||||
tool_call,
|
||||
use_card_layout,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
)
|
||||
div().child(self.render_tool_call_content(
|
||||
entry_ix,
|
||||
content,
|
||||
content_ix,
|
||||
tool_call,
|
||||
use_card_layout,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
},
|
||||
))
|
||||
.into_any(),
|
||||
@@ -2722,7 +2718,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);
|
||||
}))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
@@ -2232,10 +2231,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 +2244,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 +2291,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)
|
||||
|
||||
@@ -27,7 +27,7 @@ impl RenderOnce for HoldForDefault {
|
||||
PlatformStyle::platform(),
|
||||
None,
|
||||
Some(TextSize::Default.rems(cx).into()),
|
||||
false,
|
||||
true,
|
||||
)))
|
||||
.child(div().map(|this| {
|
||||
if self.is_default {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,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);
|
||||
});
|
||||
}
|
||||
@@ -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): ");
|
||||
|
||||
@@ -893,7 +893,7 @@ impl CompletionsMenu {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(text.trim().to_string())
|
||||
Label::new(text.clone())
|
||||
.ml_4()
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
@@ -1615,12 +1615,8 @@ impl CodeActionsMenu {
|
||||
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,
|
||||
);
|
||||
let is_truncated =
|
||||
line_wrapper.should_truncate_line(&label, CODE_ACTION_MENU_MAX_WIDTH, "…");
|
||||
|
||||
if is_truncated.is_none() {
|
||||
return None;
|
||||
|
||||
@@ -163,7 +163,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 +4787,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()
|
||||
@@ -10452,22 +10387,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 +20394,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 +23355,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 +23407,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 +23424,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 +23440,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 +25009,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 {
|
||||
|
||||
@@ -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};
|
||||
@@ -28022,7 +28021,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 +28038,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 +28060,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 +28092,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 +28685,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 +29497,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, |_| {});
|
||||
|
||||
@@ -46,9 +46,9 @@ use gpui::{
|
||||
KeybindingKeystroke, Length, Modifiers, ModifiersChangedEvent, MouseButton, MouseClickEvent,
|
||||
MouseDownEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent, PaintQuad, ParentElement,
|
||||
Pixels, PressureStage, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString,
|
||||
Size, StatefulInteractiveElement, Style, Styled, 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};
|
||||
@@ -64,10 +64,7 @@ use project::{
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
||||
project_settings::ProjectSettings,
|
||||
};
|
||||
use settings::{
|
||||
GitGutterSetting, GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring,
|
||||
RelativeLineNumbers, Settings,
|
||||
};
|
||||
use settings::{GitHunkStyleSetting, IndentGuideBackgroundColoring, IndentGuideColoring, Settings};
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
@@ -194,6 +191,8 @@ pub struct EditorElement {
|
||||
style: EditorStyle,
|
||||
}
|
||||
|
||||
type DisplayRowDelta = u32;
|
||||
|
||||
impl EditorElement {
|
||||
pub(crate) const SCROLLBAR_WIDTH: Pixels = px(15.);
|
||||
|
||||
@@ -1695,13 +1694,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;
|
||||
}
|
||||
@@ -2206,8 +2201,8 @@ impl EditorElement {
|
||||
.display_diff_hunks_for_rows(display_rows, folded_buffers)
|
||||
.map(|hunk| (hunk, None))
|
||||
.collect::<Vec<_>>();
|
||||
let git_gutter_setting = ProjectSettings::get_global(cx).git.git_gutter;
|
||||
if let GitGutterSetting::TrackedFiles = git_gutter_setting {
|
||||
let git_settings = &ProjectSettings::get_global(cx).git;
|
||||
if git_settings.is_gutter_enabled() {
|
||||
for (hunk, hitbox) in &mut display_hunks {
|
||||
if matches!(hunk, DisplayDiffHunk::Unfolded { .. }) {
|
||||
let hunk_bounds =
|
||||
@@ -3227,6 +3222,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 +3289,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 +3301,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 +4649,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 +4678,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 +5414,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 +6213,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;
|
||||
@@ -6432,12 +6466,7 @@ impl EditorElement {
|
||||
.position_map
|
||||
.snapshot
|
||||
.show_git_diff_gutter
|
||||
.unwrap_or_else(|| {
|
||||
matches!(
|
||||
ProjectSettings::get_global(cx).git.git_gutter,
|
||||
GitGutterSetting::TrackedFiles
|
||||
)
|
||||
});
|
||||
.unwrap_or_else(|| ProjectSettings::get_global(cx).git.is_gutter_enabled());
|
||||
if show_git_gutter {
|
||||
Self::paint_gutter_diff_hunks(layout, window, cx)
|
||||
}
|
||||
@@ -7271,27 +7300,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 +7324,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 +8564,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 +8607,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 +8657,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 +8818,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 +9422,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 +9439,7 @@ impl Element for EditorElement {
|
||||
start_row..end_row,
|
||||
&row_infos,
|
||||
&active_rows,
|
||||
relative_row_base,
|
||||
newest_selection_head,
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -9780,7 +9759,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 +9770,6 @@ impl Element for EditorElement {
|
||||
&gutter_hitbox,
|
||||
&text_hitbox,
|
||||
&style,
|
||||
relative,
|
||||
relative_row_base,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -10219,8 +10195,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 +10248,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 +10428,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 +10595,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 +11034,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 +11099,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 +11291,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 +11617,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 +11657,7 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
&BTreeMap::default(),
|
||||
Some(DisplayRow(0)),
|
||||
Some(DisplayPoint::new(DisplayRow(0), 0)),
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -11712,9 +11669,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 +11688,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 +11705,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 +11745,7 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
&BTreeMap::default(),
|
||||
Some(DisplayRow(0)),
|
||||
Some(DisplayPoint::new(DisplayRow(0), 0)),
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -11800,7 +11760,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 +11805,7 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
&BTreeMap::default(),
|
||||
Some(DisplayRow(0)),
|
||||
Some(DisplayPoint::new(DisplayRow(0), 0)),
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -11857,9 +11817,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 +11857,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 +11872,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,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
@@ -2849,15 +2849,93 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
pub(crate) fn git_clone(&mut self, repo: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let path = cx.prompt_for_paths(gpui::PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
prompt: Some("Select as Repository Destination".into()),
|
||||
});
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
crate::clone::clone_and_open(
|
||||
repo.into(),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
Arc::new(|_workspace: &mut workspace::Workspace, _window, _cx| {}),
|
||||
);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let mut paths = path.await.ok()?.ok()??;
|
||||
let mut path = paths.pop()?;
|
||||
let repo_name = repo.split("/").last()?.strip_suffix(".git")?.to_owned();
|
||||
|
||||
let fs = this.read_with(cx, |this, _| this.fs.clone()).ok()?;
|
||||
|
||||
let prompt_answer = match fs.git_clone(&repo, path.as_path()).await {
|
||||
Ok(_) => cx.update(|window, cx| {
|
||||
window.prompt(
|
||||
PromptLevel::Info,
|
||||
&format!("Git Clone: {}", repo_name),
|
||||
None,
|
||||
&["Add repo to project", "Open repo in new project"],
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
Err(e) => {
|
||||
this.update(cx, |this: &mut GitPanel, cx| {
|
||||
let toast = StatusToast::new(e.to_string(), cx, |this, _| {
|
||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||
.dismiss_button(true)
|
||||
});
|
||||
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_status_toast(toast, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
.ok()?;
|
||||
|
||||
path.push(repo_name);
|
||||
match prompt_answer.await.ok()? {
|
||||
0 => {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |project, cx| {
|
||||
project.create_worktree(path.as_path(), true, cx)
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
1 => {
|
||||
workspace
|
||||
.update(cx, move |workspace, cx| {
|
||||
workspace::open_new(
|
||||
Default::default(),
|
||||
workspace.app_state().clone(),
|
||||
cx,
|
||||
move |workspace, _, cx| {
|
||||
cx.activate(true);
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |project, cx| {
|
||||
project.create_worktree(&path, true, cx)
|
||||
})
|
||||
.detach();
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub(crate) fn git_init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -5203,7 +5281,7 @@ impl GitPanel {
|
||||
|
||||
this.child(
|
||||
self.entry_label(path_name, path_color)
|
||||
.truncate_start()
|
||||
.truncate()
|
||||
.when(strikethrough, Label::strikethrough),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -10,7 +10,6 @@ use ui::{
|
||||
};
|
||||
|
||||
mod blame_ui;
|
||||
pub mod clone;
|
||||
|
||||
use git::{
|
||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||
|
||||
@@ -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]"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -580,7 +580,6 @@ impl GpuiMode {
|
||||
/// You need a reference to an `App` to access the state of a [Entity].
|
||||
pub struct App {
|
||||
pub(crate) this: Weak<AppCell>,
|
||||
pub(crate) liveness: std::sync::Arc<()>,
|
||||
pub(crate) platform: Rc<dyn Platform>,
|
||||
pub(crate) mode: GpuiMode,
|
||||
text_system: Arc<TextSystem>,
|
||||
@@ -659,7 +658,6 @@ impl App {
|
||||
let app = Rc::new_cyclic(|this| AppCell {
|
||||
app: RefCell::new(App {
|
||||
this: this.clone(),
|
||||
liveness: std::sync::Arc::new(()),
|
||||
platform: platform.clone(),
|
||||
text_system,
|
||||
mode: GpuiMode::Production,
|
||||
@@ -1079,9 +1077,11 @@ impl App {
|
||||
self.platform.window_appearance()
|
||||
}
|
||||
|
||||
/// Reads data from the platform clipboard.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
/// Writes data to the primary selection buffer.
|
||||
/// Only available on Linux.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub fn write_to_primary(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_primary(item)
|
||||
}
|
||||
|
||||
/// Writes data to the platform clipboard.
|
||||
@@ -1096,31 +1096,9 @@ impl App {
|
||||
self.platform.read_from_primary()
|
||||
}
|
||||
|
||||
/// Writes data to the primary selection buffer.
|
||||
/// Only available on Linux.
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
pub fn write_to_primary(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_primary(item)
|
||||
}
|
||||
|
||||
/// Reads data from macOS's "Find" pasteboard.
|
||||
///
|
||||
/// Used to share the current search string between apps.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_find_pasteboard()
|
||||
}
|
||||
|
||||
/// Writes data to macOS's "Find" pasteboard.
|
||||
///
|
||||
/// Used to share the current search string between apps.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/appkit/nspasteboard/name-swift.struct/find
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn write_to_find_pasteboard(&self, item: ClipboardItem) {
|
||||
self.platform.write_to_find_pasteboard(item)
|
||||
/// Reads data from the platform clipboard.
|
||||
pub fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.platform.read_from_clipboard()
|
||||
}
|
||||
|
||||
/// Writes credentials to the platform keychain.
|
||||
@@ -1478,7 +1456,6 @@ impl App {
|
||||
pub fn to_async(&self) -> AsyncApp {
|
||||
AsyncApp {
|
||||
app: self.this.clone(),
|
||||
liveness_token: std::sync::Arc::downgrade(&self.liveness),
|
||||
background_executor: self.background_executor.clone(),
|
||||
foreground_executor: self.foreground_executor.clone(),
|
||||
}
|
||||
@@ -2215,6 +2192,8 @@ impl App {
|
||||
}
|
||||
|
||||
impl AppContext for App {
|
||||
type Result<T> = T;
|
||||
|
||||
/// Builds an entity that is owned by the application.
|
||||
///
|
||||
/// The given function will be invoked with a [`Context`] and must return an object representing the entity. An
|
||||
@@ -2236,7 +2215,7 @@ impl AppContext for App {
|
||||
})
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<Reservation<T>> {
|
||||
Reservation(self.entities.reserve())
|
||||
}
|
||||
|
||||
@@ -2244,7 +2223,7 @@ impl AppContext for App {
|
||||
&mut self,
|
||||
reservation: Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
) -> Self::Result<Entity<T>> {
|
||||
self.update(|cx| {
|
||||
let slot = reservation.0;
|
||||
let entity = build_entity(&mut Context::new_context(cx, slot.downgrade()));
|
||||
@@ -2277,7 +2256,11 @@ impl AppContext for App {
|
||||
GpuiBorrow::new(handle.clone(), self)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
read: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
@@ -2322,7 +2305,7 @@ impl AppContext for App {
|
||||
self.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, App, AppCell, AppContext, BackgroundExecutor, BorrowAppContext,
|
||||
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, GpuiBorrow, PromptButton,
|
||||
PromptLevel, Render, Reservation, Result, Subscription, Task, VisualContext, Window,
|
||||
WindowHandle,
|
||||
Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptButton, PromptLevel, Render,
|
||||
Reservation, Result, Subscription, Task, VisualContext, Window, WindowHandle,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use futures::channel::oneshot;
|
||||
use std::{future::Future, rc::Weak};
|
||||
@@ -13,73 +12,72 @@ use super::{Context, WeakEntity};
|
||||
|
||||
/// An async-friendly version of [App] with a static lifetime so it can be held across `await` points in async code.
|
||||
/// You're provided with an instance when calling [App::spawn], and you can also create one with [App::to_async].
|
||||
///
|
||||
/// Internally, this holds a weak reference to an `App`. Methods will panic if the app has been dropped,
|
||||
/// but this should not happen in practice when using foreground tasks spawned via `cx.spawn()`,
|
||||
/// as the executor checks if the app is alive before running each task.
|
||||
/// Internally, this holds a weak reference to an `App`, so its methods are fallible to protect against cases where the [App] is dropped.
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncApp {
|
||||
pub(crate) app: Weak<AppCell>,
|
||||
pub(crate) liveness_token: std::sync::Weak<()>,
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
}
|
||||
|
||||
impl AsyncApp {
|
||||
fn app(&self) -> std::rc::Rc<AppCell> {
|
||||
self.app
|
||||
.upgrade()
|
||||
.expect("app was released before async operation completed")
|
||||
}
|
||||
}
|
||||
|
||||
impl AppContext for AsyncApp {
|
||||
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
|
||||
let app = self.app();
|
||||
type Result<T> = Result<T>;
|
||||
|
||||
fn new<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut app = app.borrow_mut();
|
||||
app.new(build_entity)
|
||||
Ok(app.new(build_entity))
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
|
||||
let app = self.app();
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Result<Reservation<T>> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut app = app.borrow_mut();
|
||||
app.reserve_entity()
|
||||
Ok(app.reserve_entity())
|
||||
}
|
||||
|
||||
fn insert_entity<T: 'static>(
|
||||
&mut self,
|
||||
reservation: Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
let app = self.app();
|
||||
) -> Result<Entity<T>> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut app = app.borrow_mut();
|
||||
app.insert_entity(reservation, build_entity)
|
||||
Ok(app.insert_entity(reservation, build_entity))
|
||||
}
|
||||
|
||||
fn update_entity<T: 'static, R>(
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R {
|
||||
let app = self.app();
|
||||
) -> Self::Result<R> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut app = app.borrow_mut();
|
||||
app.update_entity(handle, update)
|
||||
Ok(app.update_entity(handle, update))
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _handle: &Entity<T>) -> GpuiBorrow<'a, T>
|
||||
fn as_mut<'a, T>(&'a mut self, _handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
panic!("Cannot as_mut with an async context. Try calling update() first")
|
||||
Err(anyhow!(
|
||||
"Cannot as_mut with an async context. Try calling update() first"
|
||||
))
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, callback: impl FnOnce(&T, &App) -> R) -> R
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
callback: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
let app = self.app();
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let lock = app.borrow();
|
||||
lock.read_entity(handle, callback)
|
||||
Ok(lock.read_entity(handle, callback))
|
||||
}
|
||||
|
||||
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||
@@ -111,22 +109,23 @@ impl AppContext for AsyncApp {
|
||||
self.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
let app = self.app();
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut lock = app.borrow_mut();
|
||||
lock.update(|this| this.read_global(callback))
|
||||
Ok(lock.update(|this| this.read_global(callback)))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncApp {
|
||||
/// Schedules all windows in the application to be redrawn.
|
||||
pub fn refresh(&self) {
|
||||
let app = self.app();
|
||||
pub fn refresh(&self) -> Result<()> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut lock = app.borrow_mut();
|
||||
lock.refresh_windows();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get an executor which can be used to spawn futures in the background.
|
||||
@@ -140,10 +139,10 @@ impl AsyncApp {
|
||||
}
|
||||
|
||||
/// Invoke the given function in the context of the app, then flush any effects produced during its invocation.
|
||||
pub fn update<R>(&self, f: impl FnOnce(&mut App) -> R) -> R {
|
||||
let app = self.app();
|
||||
pub fn update<R>(&self, f: impl FnOnce(&mut App) -> R) -> Result<R> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut lock = app.borrow_mut();
|
||||
lock.update(f)
|
||||
Ok(lock.update(f))
|
||||
}
|
||||
|
||||
/// Arrange for the given callback to be invoked whenever the given entity emits an event of a given type.
|
||||
@@ -151,15 +150,16 @@ impl AsyncApp {
|
||||
pub fn subscribe<T, Event>(
|
||||
&mut self,
|
||||
entity: &Entity<T>,
|
||||
on_event: impl FnMut(Entity<T>, &Event, &mut App) + 'static,
|
||||
) -> Subscription
|
||||
mut on_event: impl FnMut(Entity<T>, &Event, &mut App) + 'static,
|
||||
) -> Result<Subscription>
|
||||
where
|
||||
T: 'static + EventEmitter<Event>,
|
||||
Event: 'static,
|
||||
{
|
||||
let app = self.app();
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut lock = app.borrow_mut();
|
||||
lock.subscribe(entity, on_event)
|
||||
let subscription = lock.subscribe(entity, on_event);
|
||||
Ok(subscription)
|
||||
}
|
||||
|
||||
/// Open a window with the given options based on the root view returned by the given function.
|
||||
@@ -171,7 +171,7 @@ impl AsyncApp {
|
||||
where
|
||||
V: 'static + Render,
|
||||
{
|
||||
let app = self.app();
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut lock = app.borrow_mut();
|
||||
lock.open_window(options, build_root_view)
|
||||
}
|
||||
@@ -185,54 +185,65 @@ impl AsyncApp {
|
||||
{
|
||||
let mut cx = self.clone();
|
||||
self.foreground_executor
|
||||
.spawn_context(self.liveness_token.clone(), async move { f(&mut cx).await })
|
||||
.spawn(async move { f(&mut cx).await })
|
||||
}
|
||||
|
||||
/// Determine whether global state of the specified type has been assigned.
|
||||
pub fn has_global<G: Global>(&self) -> bool {
|
||||
let app = self.app();
|
||||
/// Returns an error if the `App` has been dropped.
|
||||
pub fn has_global<G: Global>(&self) -> Result<bool> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let app = app.borrow_mut();
|
||||
app.has_global::<G>()
|
||||
Ok(app.has_global::<G>())
|
||||
}
|
||||
|
||||
/// Reads the global state of the specified type, passing it to the given callback.
|
||||
///
|
||||
/// Panics if no global state of the specified type has been assigned.
|
||||
pub fn read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> R {
|
||||
let app = self.app();
|
||||
/// Returns an error if the `App` has been dropped.
|
||||
pub fn read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> Result<R> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let app = app.borrow_mut();
|
||||
read(app.global(), &app)
|
||||
Ok(read(app.global(), &app))
|
||||
}
|
||||
|
||||
/// Reads the global state of the specified type, passing it to the given callback.
|
||||
///
|
||||
/// Similar to [`AsyncApp::read_global`], but returns an error instead of panicking
|
||||
/// if no state of the specified type has been assigned.
|
||||
///
|
||||
/// Returns an error if no state of the specified type has been assigned the `App` has been dropped.
|
||||
pub fn try_read_global<G: Global, R>(&self, read: impl FnOnce(&G, &App) -> R) -> Option<R> {
|
||||
let app = self.app();
|
||||
let app = self.app.upgrade()?;
|
||||
let app = app.borrow_mut();
|
||||
Some(read(app.try_global()?, &app))
|
||||
}
|
||||
|
||||
/// Reads the global state of the specified type, passing it to the given callback.
|
||||
/// A default value is assigned if a global of this type has not yet been assigned.
|
||||
pub fn read_default_global<G: Global + Default, R>(
|
||||
///
|
||||
/// # Errors
|
||||
/// If the app has ben dropped this returns an error.
|
||||
pub fn try_read_default_global<G: Global + Default, R>(
|
||||
&self,
|
||||
read: impl FnOnce(&G, &App) -> R,
|
||||
) -> R {
|
||||
let app = self.app();
|
||||
) -> Result<R> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut app = app.borrow_mut();
|
||||
app.update(|cx| {
|
||||
cx.default_global::<G>();
|
||||
});
|
||||
read(app.global(), &app)
|
||||
Ok(read(app.try_global().context("app was released")?, &app))
|
||||
}
|
||||
|
||||
/// A convenience method for [`App::update_global`](BorrowAppContext::update_global)
|
||||
/// for updating the global state of the specified type.
|
||||
pub fn update_global<G: Global, R>(&self, update: impl FnOnce(&mut G, &mut App) -> R) -> R {
|
||||
let app = self.app();
|
||||
pub fn update_global<G: Global, R>(
|
||||
&self,
|
||||
update: impl FnOnce(&mut G, &mut App) -> R,
|
||||
) -> Result<R> {
|
||||
let app = self.app.upgrade().context("app was released")?;
|
||||
let mut app = app.borrow_mut();
|
||||
app.update(|cx| cx.update_global(update))
|
||||
Ok(app.update(|cx| cx.update_global(update)))
|
||||
}
|
||||
|
||||
/// Run something using this entity and cx, when the returned struct is dropped
|
||||
@@ -323,10 +334,7 @@ impl AsyncWindowContext {
|
||||
{
|
||||
let mut cx = self.clone();
|
||||
self.foreground_executor
|
||||
.spawn_context(
|
||||
self.app.liveness_token.clone(),
|
||||
async move { f(&mut cx).await },
|
||||
)
|
||||
.spawn(async move { f(&mut cx).await })
|
||||
}
|
||||
|
||||
/// Present a platform dialog.
|
||||
@@ -351,41 +359,54 @@ impl AsyncWindowContext {
|
||||
}
|
||||
|
||||
impl AppContext for AsyncWindowContext {
|
||||
fn new<T>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T>
|
||||
type Result<T> = Result<T>;
|
||||
|
||||
fn new<T>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Result<Entity<T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.app.new(build_entity)
|
||||
self.app
|
||||
.update_window(self.window, |_, _, cx| cx.new(build_entity))
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T> {
|
||||
self.app.reserve_entity()
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Result<Reservation<T>> {
|
||||
self.app
|
||||
.update_window(self.window, |_, _, cx| cx.reserve_entity())
|
||||
}
|
||||
|
||||
fn insert_entity<T: 'static>(
|
||||
&mut self,
|
||||
reservation: Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
self.app.insert_entity(reservation, build_entity)
|
||||
) -> Self::Result<Entity<T>> {
|
||||
self.app.update_window(self.window, |_, _, cx| {
|
||||
cx.insert_entity(reservation, build_entity)
|
||||
})
|
||||
}
|
||||
|
||||
fn update_entity<T: 'static, R>(
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R {
|
||||
self.app.update_entity(handle, update)
|
||||
) -> Result<R> {
|
||||
self.app
|
||||
.update_window(self.window, |_, _, cx| cx.update_entity(handle, update))
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> GpuiBorrow<'a, T>
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
panic!("Cannot use as_mut() from an async context, call `update`")
|
||||
Err(anyhow!(
|
||||
"Cannot use as_mut() from an async context, call `update`"
|
||||
))
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
read: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
@@ -417,7 +438,7 @@ impl AppContext for AsyncWindowContext {
|
||||
self.app.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Result<R>
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
@@ -433,7 +454,7 @@ impl VisualContext for AsyncWindowContext {
|
||||
fn new_window_entity<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,
|
||||
) -> Result<Entity<T>> {
|
||||
) -> Self::Result<Entity<T>> {
|
||||
self.app.update_window(self.window, |_, window, cx| {
|
||||
cx.new(|cx| build_entity(window, cx))
|
||||
})
|
||||
@@ -443,7 +464,7 @@ impl VisualContext for AsyncWindowContext {
|
||||
&mut self,
|
||||
view: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
|
||||
) -> Result<R> {
|
||||
) -> Self::Result<R> {
|
||||
self.app.update_window(self.window, |_, window, cx| {
|
||||
view.update(cx, |entity, cx| update(entity, window, cx))
|
||||
})
|
||||
@@ -452,7 +473,7 @@ impl VisualContext for AsyncWindowContext {
|
||||
fn replace_root_view<V>(
|
||||
&mut self,
|
||||
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||
) -> Result<Entity<V>>
|
||||
) -> Self::Result<Entity<V>>
|
||||
where
|
||||
V: 'static + Render,
|
||||
{
|
||||
@@ -461,7 +482,7 @@ impl VisualContext for AsyncWindowContext {
|
||||
})
|
||||
}
|
||||
|
||||
fn focus<V>(&mut self, view: &Entity<V>) -> Result<()>
|
||||
fn focus<V>(&mut self, view: &Entity<V>) -> Self::Result<()>
|
||||
where
|
||||
V: Focusable,
|
||||
{
|
||||
|
||||
@@ -753,6 +753,8 @@ impl<T> Context<'_, T> {
|
||||
}
|
||||
|
||||
impl<T> AppContext for Context<'_, T> {
|
||||
type Result<U> = U;
|
||||
|
||||
#[inline]
|
||||
fn new<U: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<U>) -> U) -> Entity<U> {
|
||||
self.app.new(build_entity)
|
||||
@@ -768,7 +770,7 @@ impl<T> AppContext for Context<'_, T> {
|
||||
&mut self,
|
||||
reservation: Reservation<U>,
|
||||
build_entity: impl FnOnce(&mut Context<U>) -> U,
|
||||
) -> Entity<U> {
|
||||
) -> Self::Result<Entity<U>> {
|
||||
self.app.insert_entity(reservation, build_entity)
|
||||
}
|
||||
|
||||
@@ -782,7 +784,7 @@ impl<T> AppContext for Context<'_, T> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_mut<'a, E>(&'a mut self, handle: &Entity<E>) -> super::GpuiBorrow<'a, E>
|
||||
fn as_mut<'a, E>(&'a mut self, handle: &Entity<E>) -> Self::Result<super::GpuiBorrow<'a, E>>
|
||||
where
|
||||
E: 'static,
|
||||
{
|
||||
@@ -790,7 +792,11 @@ impl<T> AppContext for Context<'_, T> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_entity<U, R>(&self, handle: &Entity<U>, read: impl FnOnce(&U, &App) -> R) -> R
|
||||
fn read_entity<U, R>(
|
||||
&self,
|
||||
handle: &Entity<U>,
|
||||
read: impl FnOnce(&U, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
where
|
||||
U: 'static,
|
||||
{
|
||||
@@ -826,7 +832,7 @@ impl<T> AppContext for Context<'_, T> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
|
||||
@@ -431,7 +431,11 @@ impl<T: 'static> Entity<T> {
|
||||
|
||||
/// Read the entity referenced by this handle with the given function.
|
||||
#[inline]
|
||||
pub fn read_with<R, C: AppContext>(&self, cx: &C, f: impl FnOnce(&T, &App) -> R) -> R {
|
||||
pub fn read_with<R, C: AppContext>(
|
||||
&self,
|
||||
cx: &C,
|
||||
f: impl FnOnce(&T, &App) -> R,
|
||||
) -> C::Result<R> {
|
||||
cx.read_entity(self, f)
|
||||
}
|
||||
|
||||
@@ -441,18 +445,18 @@ impl<T: 'static> Entity<T> {
|
||||
&self,
|
||||
cx: &mut C,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R {
|
||||
) -> C::Result<R> {
|
||||
cx.update_entity(self, update)
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function.
|
||||
#[inline]
|
||||
pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> GpuiBorrow<'a, T> {
|
||||
pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> C::Result<GpuiBorrow<'a, T>> {
|
||||
cx.as_mut(self)
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function.
|
||||
pub fn write<C: AppContext>(&self, cx: &mut C, value: T) {
|
||||
pub fn write<C: AppContext>(&self, cx: &mut C, value: T) -> C::Result<()> {
|
||||
self.update(cx, |entity, cx| {
|
||||
*entity = value;
|
||||
cx.notify();
|
||||
@@ -461,13 +465,13 @@ impl<T: 'static> Entity<T> {
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function if
|
||||
/// the referenced entity still exists, within a visual context that has a window.
|
||||
/// Returns an error if the window has been closed.
|
||||
/// Returns an error if the entity has been released.
|
||||
#[inline]
|
||||
pub fn update_in<R, C: VisualContext>(
|
||||
&self,
|
||||
cx: &mut C,
|
||||
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
|
||||
) -> Result<R> {
|
||||
) -> C::Result<R> {
|
||||
cx.update_window_entity(self, update)
|
||||
}
|
||||
}
|
||||
@@ -745,9 +749,13 @@ impl<T: 'static> WeakEntity<T> {
|
||||
) -> Result<R>
|
||||
where
|
||||
C: AppContext,
|
||||
Result<C::Result<R>>: crate::Flatten<R>,
|
||||
{
|
||||
let entity = self.upgrade().context("entity released")?;
|
||||
Ok(cx.update_entity(&entity, update))
|
||||
crate::Flatten::flatten(
|
||||
self.upgrade()
|
||||
.context("entity released")
|
||||
.map(|this| cx.update_entity(&this, update)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Updates the entity referenced by this handle with the given function if
|
||||
@@ -760,13 +768,14 @@ impl<T: 'static> WeakEntity<T> {
|
||||
) -> Result<R>
|
||||
where
|
||||
C: VisualContext,
|
||||
Result<C::Result<R>>: crate::Flatten<R>,
|
||||
{
|
||||
let window = cx.window_handle();
|
||||
let entity = self.upgrade().context("entity released")?;
|
||||
let this = self.upgrade().context("entity released")?;
|
||||
|
||||
window.update(cx, |_, window, cx| {
|
||||
entity.update(cx, |entity, cx| update(entity, window, cx))
|
||||
})
|
||||
crate::Flatten::flatten(window.update(cx, |_, window, cx| {
|
||||
this.update(cx, |entity, cx| update(entity, window, cx))
|
||||
}))
|
||||
}
|
||||
|
||||
/// Reads the entity referenced by this handle with the given function if
|
||||
@@ -775,9 +784,13 @@ impl<T: 'static> WeakEntity<T> {
|
||||
pub fn read_with<C, R>(&self, cx: &C, read: impl FnOnce(&T, &App) -> R) -> Result<R>
|
||||
where
|
||||
C: AppContext,
|
||||
Result<C::Result<R>>: crate::Flatten<R>,
|
||||
{
|
||||
let entity = self.upgrade().context("entity released")?;
|
||||
Ok(cx.read_entity(&entity, read))
|
||||
crate::Flatten::flatten(
|
||||
self.upgrade()
|
||||
.context("entity released")
|
||||
.map(|this| cx.read_entity(&this, read)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a new weak entity that can never be upgraded.
|
||||
|
||||
@@ -33,12 +33,17 @@ pub struct TestAppContext {
|
||||
}
|
||||
|
||||
impl AppContext for TestAppContext {
|
||||
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
|
||||
type Result<T> = T;
|
||||
|
||||
fn new<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.new(build_entity)
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> crate::Reservation<T> {
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<crate::Reservation<T>> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.reserve_entity()
|
||||
}
|
||||
@@ -47,7 +52,7 @@ impl AppContext for TestAppContext {
|
||||
&mut self,
|
||||
reservation: crate::Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
) -> Self::Result<Entity<T>> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.insert_entity(reservation, build_entity)
|
||||
}
|
||||
@@ -56,19 +61,23 @@ impl AppContext for TestAppContext {
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R {
|
||||
) -> Self::Result<R> {
|
||||
let mut app = self.app.borrow_mut();
|
||||
app.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> super::GpuiBorrow<'a, T>
|
||||
fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
panic!("Cannot use as_mut with a test app context. Try calling update() first")
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
read: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
@@ -103,7 +112,7 @@ impl AppContext for TestAppContext {
|
||||
self.background_executor.spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
@@ -396,7 +405,6 @@ impl TestAppContext {
|
||||
pub fn to_async(&self) -> AsyncApp {
|
||||
AsyncApp {
|
||||
app: Rc::downgrade(&self.app),
|
||||
liveness_token: std::sync::Arc::downgrade(&self.app.borrow().liveness),
|
||||
background_executor: self.background_executor.clone(),
|
||||
foreground_executor: self.foreground_executor.clone(),
|
||||
}
|
||||
@@ -908,11 +916,16 @@ impl VisualTestContext {
|
||||
}
|
||||
|
||||
impl AppContext for VisualTestContext {
|
||||
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
|
||||
type Result<T> = <TestAppContext as AppContext>::Result<T>;
|
||||
|
||||
fn new<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>> {
|
||||
self.cx.new(build_entity)
|
||||
}
|
||||
|
||||
fn reserve_entity<T: 'static>(&mut self) -> crate::Reservation<T> {
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<crate::Reservation<T>> {
|
||||
self.cx.reserve_entity()
|
||||
}
|
||||
|
||||
@@ -920,7 +933,7 @@ impl AppContext for VisualTestContext {
|
||||
&mut self,
|
||||
reservation: crate::Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T> {
|
||||
) -> Self::Result<Entity<T>> {
|
||||
self.cx.insert_entity(reservation, build_entity)
|
||||
}
|
||||
|
||||
@@ -928,21 +941,25 @@ impl AppContext for VisualTestContext {
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R
|
||||
) -> Self::Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.cx.update_entity(handle, update)
|
||||
}
|
||||
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> super::GpuiBorrow<'a, T>
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.cx.as_mut(handle)
|
||||
}
|
||||
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
read: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
@@ -974,7 +991,7 @@ impl AppContext for VisualTestContext {
|
||||
self.cx.background_spawn(future)
|
||||
}
|
||||
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
where
|
||||
G: Global,
|
||||
{
|
||||
@@ -991,38 +1008,46 @@ impl VisualContext for VisualTestContext {
|
||||
fn new_window_entity<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,
|
||||
) -> Result<Entity<T>> {
|
||||
self.window.update(&mut self.cx, |_, window, cx| {
|
||||
cx.new(|cx| build_entity(window, cx))
|
||||
})
|
||||
) -> Self::Result<Entity<T>> {
|
||||
self.window
|
||||
.update(&mut self.cx, |_, window, cx| {
|
||||
cx.new(|cx| build_entity(window, cx))
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn update_window_entity<V: 'static, R>(
|
||||
&mut self,
|
||||
view: &Entity<V>,
|
||||
update: impl FnOnce(&mut V, &mut Window, &mut Context<V>) -> R,
|
||||
) -> Result<R> {
|
||||
self.window.update(&mut self.cx, |_, window, cx| {
|
||||
view.update(cx, |v, cx| update(v, window, cx))
|
||||
})
|
||||
) -> Self::Result<R> {
|
||||
self.window
|
||||
.update(&mut self.cx, |_, window, cx| {
|
||||
view.update(cx, |v, cx| update(v, window, cx))
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn replace_root_view<V>(
|
||||
&mut self,
|
||||
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||
) -> Result<Entity<V>>
|
||||
) -> Self::Result<Entity<V>>
|
||||
where
|
||||
V: 'static + Render,
|
||||
{
|
||||
self.window.update(&mut self.cx, |_, window, cx| {
|
||||
window.replace_root(cx, build_view)
|
||||
})
|
||||
self.window
|
||||
.update(&mut self.cx, |_, window, cx| {
|
||||
window.replace_root(cx, build_view)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn focus<V: crate::Focusable>(&mut self, view: &Entity<V>) -> Result<()> {
|
||||
self.window.update(&mut self.cx, |_, window, cx| {
|
||||
view.read(cx).focus_handle(cx).focus(window, cx)
|
||||
})
|
||||
fn focus<V: crate::Focusable>(&mut self, view: &Entity<V>) -> Self::Result<()> {
|
||||
self.window
|
||||
.update(&mut self.cx, |_, window, cx| {
|
||||
view.read(cx).focus_handle(cx).focus(window, cx)
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1730,11 +1730,6 @@ impl Interactivity {
|
||||
let clicked_state = clicked_state.borrow();
|
||||
self.active = Some(clicked_state.element);
|
||||
}
|
||||
if self.hover_style.is_some() || self.group_hover_style.is_some() {
|
||||
element_state
|
||||
.hover_state
|
||||
.get_or_insert_with(Default::default);
|
||||
}
|
||||
if let Some(active_tooltip) = element_state.active_tooltip.as_ref() {
|
||||
if self.tooltip_builder.is_some() {
|
||||
self.tooltip_id = set_tooltip_on_window(active_tooltip, window);
|
||||
@@ -2155,46 +2150,14 @@ impl Interactivity {
|
||||
{
|
||||
let hitbox = hitbox.clone();
|
||||
let was_hovered = hitbox.is_hovered(window);
|
||||
let hover_state = self.hover_style.as_ref().and_then(|_| {
|
||||
element_state
|
||||
.as_ref()
|
||||
.and_then(|state| state.hover_state.as_ref())
|
||||
.cloned()
|
||||
});
|
||||
let current_view = window.current_view();
|
||||
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| {
|
||||
let hovered = hitbox.is_hovered(window);
|
||||
if phase == DispatchPhase::Capture && hovered != was_hovered {
|
||||
if let Some(hover_state) = &hover_state {
|
||||
hover_state.borrow_mut().element = hovered;
|
||||
}
|
||||
cx.notify(current_view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(group_hover) = self.group_hover_style.as_ref() {
|
||||
if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) {
|
||||
let hover_state = element_state
|
||||
.as_ref()
|
||||
.and_then(|element| element.hover_state.as_ref())
|
||||
.cloned();
|
||||
|
||||
let was_group_hovered = group_hitbox_id.is_hovered(window);
|
||||
let current_view = window.current_view();
|
||||
|
||||
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| {
|
||||
let group_hovered = group_hitbox_id.is_hovered(window);
|
||||
if phase == DispatchPhase::Capture && group_hovered != was_group_hovered {
|
||||
if let Some(hover_state) = &hover_state {
|
||||
hover_state.borrow_mut().group = group_hovered;
|
||||
}
|
||||
cx.notify(current_view);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let drag_cursor_style = self.base_style.as_ref().mouse_cursor;
|
||||
|
||||
let mut drag_listener = mem::take(&mut self.drag_listener);
|
||||
@@ -2383,8 +2346,8 @@ impl Interactivity {
|
||||
&& hitbox.is_hovered(window);
|
||||
let mut was_hovered = was_hovered.borrow_mut();
|
||||
|
||||
if is_hovered != was_hovered.element {
|
||||
was_hovered.element = is_hovered;
|
||||
if is_hovered != *was_hovered {
|
||||
*was_hovered = is_hovered;
|
||||
drop(was_hovered);
|
||||
|
||||
hover_listener(&is_hovered, window, cx);
|
||||
@@ -2617,46 +2580,22 @@ impl Interactivity {
|
||||
}
|
||||
}
|
||||
|
||||
if !cx.has_active_drag() {
|
||||
if let Some(group_hover) = self.group_hover_style.as_ref() {
|
||||
let is_group_hovered =
|
||||
if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) {
|
||||
group_hitbox_id.is_hovered(window)
|
||||
} else if let Some(element_state) = element_state.as_ref() {
|
||||
element_state
|
||||
.hover_state
|
||||
.as_ref()
|
||||
.map(|state| state.borrow().group)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if is_group_hovered {
|
||||
if let Some(hitbox) = hitbox {
|
||||
if !cx.has_active_drag() {
|
||||
if let Some(group_hover) = self.group_hover_style.as_ref()
|
||||
&& let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx)
|
||||
&& group_hitbox_id.is_hovered(window)
|
||||
{
|
||||
style.refine(&group_hover.style);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(hover_style) = self.hover_style.as_ref() {
|
||||
let is_hovered = if let Some(hitbox) = hitbox {
|
||||
hitbox.is_hovered(window)
|
||||
} else if let Some(element_state) = element_state.as_ref() {
|
||||
element_state
|
||||
.hover_state
|
||||
.as_ref()
|
||||
.map(|state| state.borrow().element)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if is_hovered {
|
||||
if let Some(hover_style) = self.hover_style.as_ref()
|
||||
&& hitbox.is_hovered(window)
|
||||
{
|
||||
style.refine(hover_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(hitbox) = hitbox {
|
||||
if let Some(drag) = cx.active_drag.take() {
|
||||
let mut can_drop = true;
|
||||
if let Some(can_drop_predicate) = &self.can_drop_predicate {
|
||||
@@ -2715,7 +2654,7 @@ impl Interactivity {
|
||||
pub struct InteractiveElementState {
|
||||
pub(crate) focus_handle: Option<FocusHandle>,
|
||||
pub(crate) clicked_state: Option<Rc<RefCell<ElementClickedState>>>,
|
||||
pub(crate) hover_state: Option<Rc<RefCell<ElementHoverState>>>,
|
||||
pub(crate) hover_state: Option<Rc<RefCell<bool>>>,
|
||||
pub(crate) pending_mouse_down: Option<Rc<RefCell<Option<MouseDownEvent>>>>,
|
||||
pub(crate) scroll_offset: Option<Rc<RefCell<Point<Pixels>>>>,
|
||||
pub(crate) active_tooltip: Option<Rc<RefCell<Option<ActiveTooltip>>>>,
|
||||
@@ -2737,16 +2676,6 @@ impl ElementClickedState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether or not the element or a group that contains it is hovered.
|
||||
#[derive(Copy, Clone, Default, Eq, PartialEq)]
|
||||
pub struct ElementHoverState {
|
||||
/// True if this element's group is hovered, false otherwise
|
||||
pub group: bool,
|
||||
|
||||
/// True if this element is hovered, false otherwise
|
||||
pub element: bool,
|
||||
}
|
||||
|
||||
pub(crate) enum ActiveTooltip {
|
||||
/// Currently delaying before showing the tooltip.
|
||||
WaitingForShow { _task: Task<()> },
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::{
|
||||
ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
|
||||
HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow,
|
||||
TextRun, TextStyle, TooltipId, TruncateFrom, WhiteSpace, Window, WrappedLine,
|
||||
WrappedLineLayout, register_tooltip_mouse_handlers, set_tooltip_on_window,
|
||||
TextRun, TextStyle, TooltipId, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
|
||||
register_tooltip_mouse_handlers, set_tooltip_on_window,
|
||||
};
|
||||
use anyhow::Context as _;
|
||||
use itertools::Itertools;
|
||||
@@ -354,7 +354,7 @@ impl TextLayout {
|
||||
None
|
||||
};
|
||||
|
||||
let (truncate_width, truncation_affix, truncate_from) =
|
||||
let (truncate_width, truncation_suffix) =
|
||||
if let Some(text_overflow) = text_style.text_overflow.clone() {
|
||||
let width = known_dimensions.width.or(match available_space.width {
|
||||
crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
|
||||
@@ -365,24 +365,17 @@ impl TextLayout {
|
||||
});
|
||||
|
||||
match text_overflow {
|
||||
TextOverflow::Truncate(s) => (width, s, TruncateFrom::End),
|
||||
TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start),
|
||||
TextOverflow::Truncate(s) => (width, s),
|
||||
}
|
||||
} else {
|
||||
(None, "".into(), TruncateFrom::End)
|
||||
(None, "".into())
|
||||
};
|
||||
|
||||
// Only use cached layout if:
|
||||
// 1. We have a cached size
|
||||
// 2. wrap_width matches (or both are None)
|
||||
// 3. truncate_width is None (if truncate_width is Some, we need to re-layout
|
||||
// because the previous layout may have been computed without truncation)
|
||||
if let Some(text_layout) = element_state.0.borrow().as_ref()
|
||||
&& let Some(size) = text_layout.size
|
||||
&& text_layout.size.is_some()
|
||||
&& (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
|
||||
&& truncate_width.is_none()
|
||||
{
|
||||
return size;
|
||||
return text_layout.size.unwrap();
|
||||
}
|
||||
|
||||
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
|
||||
@@ -390,9 +383,8 @@ impl TextLayout {
|
||||
line_wrapper.truncate_line(
|
||||
text.clone(),
|
||||
truncate_width,
|
||||
&truncation_affix,
|
||||
&truncation_suffix,
|
||||
&runs,
|
||||
truncate_from,
|
||||
)
|
||||
} else {
|
||||
(text.clone(), Cow::Borrowed(&*runs))
|
||||
|
||||
@@ -125,30 +125,6 @@ impl<T> Task<T> {
|
||||
Task(TaskState::Spawned(task)) => task.detach(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this task into a fallible task that returns `Option<T>`.
|
||||
///
|
||||
/// Unlike the standard `Task<T>`, a [`FallibleTask`] will return `None`
|
||||
/// if the app was dropped while the task is executing.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Background task that gracefully handles app shutdown:
|
||||
/// cx.background_spawn(async move {
|
||||
/// let result = foreground_task.fallible().await;
|
||||
/// if let Some(value) = result {
|
||||
/// // Process the value
|
||||
/// }
|
||||
/// // If None, app was shut down - just exit gracefully
|
||||
/// }).detach();
|
||||
/// ```
|
||||
pub fn fallible(self) -> FallibleTask<T> {
|
||||
FallibleTask(match self.0 {
|
||||
TaskState::Ready(val) => FallibleTaskState::Ready(val),
|
||||
TaskState::Spawned(task) => FallibleTaskState::Spawned(task.fallible()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, T> Task<Result<T, E>>
|
||||
@@ -178,55 +154,6 @@ impl<T> Future for Task<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A task that returns `Option<T>` instead of panicking when cancelled.
|
||||
#[must_use]
|
||||
pub struct FallibleTask<T>(FallibleTaskState<T>);
|
||||
|
||||
enum FallibleTaskState<T> {
|
||||
/// A task that is ready to return a value
|
||||
Ready(Option<T>),
|
||||
|
||||
/// A task that is currently running (wraps async_task::FallibleTask).
|
||||
Spawned(async_task::FallibleTask<T, RunnableMeta>),
|
||||
}
|
||||
|
||||
impl<T> FallibleTask<T> {
|
||||
/// Creates a new fallible task that will resolve with the value.
|
||||
pub fn ready(val: T) -> Self {
|
||||
FallibleTask(FallibleTaskState::Ready(Some(val)))
|
||||
}
|
||||
|
||||
/// Detaching a task runs it to completion in the background.
|
||||
pub fn detach(self) {
|
||||
match self.0 {
|
||||
FallibleTaskState::Ready(_) => {}
|
||||
FallibleTaskState::Spawned(task) => task.detach(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for FallibleTask<T> {
|
||||
type Output = Option<T>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
match unsafe { self.get_unchecked_mut() } {
|
||||
FallibleTask(FallibleTaskState::Ready(val)) => Poll::Ready(val.take()),
|
||||
FallibleTask(FallibleTaskState::Spawned(task)) => Pin::new(task).poll(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for FallibleTask<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.0 {
|
||||
FallibleTaskState::Ready(_) => f.debug_tuple("FallibleTask::Ready").finish(),
|
||||
FallibleTaskState::Spawned(task) => {
|
||||
f.debug_tuple("FallibleTask::Spawned").field(task).finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A task label is an opaque identifier that you can use to
|
||||
/// refer to a task in tests.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
@@ -325,10 +252,7 @@ impl BackgroundExecutor {
|
||||
|
||||
let (runnable, task) = unsafe {
|
||||
async_task::Builder::new()
|
||||
.metadata(RunnableMeta {
|
||||
location,
|
||||
app: None,
|
||||
})
|
||||
.metadata(RunnableMeta { location })
|
||||
.spawn_unchecked(
|
||||
move |_| async {
|
||||
let _notify_guard = NotifyOnDrop(pair);
|
||||
@@ -406,10 +330,7 @@ impl BackgroundExecutor {
|
||||
);
|
||||
|
||||
async_task::Builder::new()
|
||||
.metadata(RunnableMeta {
|
||||
location,
|
||||
app: None,
|
||||
})
|
||||
.metadata(RunnableMeta { location })
|
||||
.spawn(
|
||||
move |_| future,
|
||||
move |runnable| {
|
||||
@@ -419,10 +340,7 @@ impl BackgroundExecutor {
|
||||
} else {
|
||||
let location = core::panic::Location::caller();
|
||||
async_task::Builder::new()
|
||||
.metadata(RunnableMeta {
|
||||
location,
|
||||
app: None,
|
||||
})
|
||||
.metadata(RunnableMeta { location })
|
||||
.spawn(
|
||||
move |_| future,
|
||||
move |runnable| {
|
||||
@@ -648,10 +566,7 @@ impl BackgroundExecutor {
|
||||
}
|
||||
let location = core::panic::Location::caller();
|
||||
let (runnable, task) = async_task::Builder::new()
|
||||
.metadata(RunnableMeta {
|
||||
location,
|
||||
app: None,
|
||||
})
|
||||
.metadata(RunnableMeta { location })
|
||||
.spawn(move |_| async move {}, {
|
||||
let dispatcher = self.dispatcher.clone();
|
||||
move |runnable| dispatcher.dispatch_after(duration, RunnableVariant::Meta(runnable))
|
||||
@@ -766,7 +681,7 @@ impl ForegroundExecutor {
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
self.inner_spawn(None, Priority::default(), future)
|
||||
self.spawn_with_priority(Priority::default(), future)
|
||||
}
|
||||
|
||||
/// Enqueues the given Task to run on the main thread at some point in the future.
|
||||
@@ -776,31 +691,6 @@ impl ForegroundExecutor {
|
||||
priority: Priority,
|
||||
future: impl Future<Output = R> + 'static,
|
||||
) -> Task<R>
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
self.inner_spawn(None, priority, future)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn spawn_context<R>(
|
||||
&self,
|
||||
app: std::sync::Weak<()>,
|
||||
future: impl Future<Output = R> + 'static,
|
||||
) -> Task<R>
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
self.inner_spawn(Some(app), Priority::default(), future)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn inner_spawn<R>(
|
||||
&self,
|
||||
app: Option<std::sync::Weak<()>>,
|
||||
priority: Priority,
|
||||
future: impl Future<Output = R> + 'static,
|
||||
) -> Task<R>
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
@@ -812,7 +702,6 @@ impl ForegroundExecutor {
|
||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||
future: AnyLocalFuture<R>,
|
||||
location: &'static core::panic::Location<'static>,
|
||||
app: Option<std::sync::Weak<()>>,
|
||||
priority: Priority,
|
||||
) -> Task<R> {
|
||||
let (runnable, task) = spawn_local_with_source_location(
|
||||
@@ -820,12 +709,12 @@ impl ForegroundExecutor {
|
||||
move |runnable| {
|
||||
dispatcher.dispatch_on_main_thread(RunnableVariant::Meta(runnable), priority)
|
||||
},
|
||||
RunnableMeta { location, app },
|
||||
RunnableMeta { location },
|
||||
);
|
||||
runnable.schedule();
|
||||
Task(TaskState::Spawned(task))
|
||||
}
|
||||
inner::<R>(dispatcher, Box::pin(future), location, app, priority)
|
||||
inner::<R>(dispatcher, Box::pin(future), location, priority)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -958,259 +847,3 @@ impl Drop for Scope<'_> {
|
||||
self.executor.block(self.rx.next());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{App, TestDispatcher, TestPlatform};
|
||||
use rand::SeedableRng;
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[test]
|
||||
fn sanity_test_tasks_run() {
|
||||
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
|
||||
let platform = TestPlatform::new(background_executor, foreground_executor.clone());
|
||||
let asset_source = Arc::new(());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
|
||||
let app = App::new_app(platform, asset_source, http_client);
|
||||
let liveness_token = std::sync::Arc::downgrade(&app.borrow().liveness);
|
||||
|
||||
let task_ran = Rc::new(RefCell::new(false));
|
||||
|
||||
foreground_executor
|
||||
.spawn_context(liveness_token, {
|
||||
let task_ran = Rc::clone(&task_ran);
|
||||
async move {
|
||||
*task_ran.borrow_mut() = true;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Run dispatcher while app is still alive
|
||||
dispatcher.run_until_parked();
|
||||
|
||||
// Task should have run
|
||||
assert!(
|
||||
*task_ran.borrow(),
|
||||
"Task should run normally when app is alive"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_cancelled_when_app_dropped() {
|
||||
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
|
||||
let platform = TestPlatform::new(background_executor, foreground_executor.clone());
|
||||
let asset_source = Arc::new(());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
|
||||
let app = App::new_app(platform, asset_source, http_client);
|
||||
let liveness_token = std::sync::Arc::downgrade(&app.borrow().liveness);
|
||||
let app_weak = Rc::downgrade(&app);
|
||||
|
||||
let task_ran = Rc::new(RefCell::new(false));
|
||||
let task_ran_clone = Rc::clone(&task_ran);
|
||||
|
||||
foreground_executor
|
||||
.spawn_context(liveness_token, async move {
|
||||
*task_ran_clone.borrow_mut() = true;
|
||||
})
|
||||
.detach();
|
||||
|
||||
drop(app);
|
||||
|
||||
assert!(app_weak.upgrade().is_none(), "App should have been dropped");
|
||||
|
||||
dispatcher.run_until_parked();
|
||||
|
||||
// The task should have been cancelled, not run
|
||||
assert!(
|
||||
!*task_ran.borrow(),
|
||||
"Task should have been cancelled when app was dropped, but it ran!"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_tasks_both_cancel() {
|
||||
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
|
||||
let platform = TestPlatform::new(background_executor, foreground_executor.clone());
|
||||
let asset_source = Arc::new(());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
|
||||
let app = App::new_app(platform, asset_source, http_client);
|
||||
let liveness_token = std::sync::Arc::downgrade(&app.borrow().liveness);
|
||||
let app_weak = Rc::downgrade(&app);
|
||||
|
||||
let outer_completed = Rc::new(RefCell::new(false));
|
||||
let inner_completed = Rc::new(RefCell::new(false));
|
||||
let reached_await = Rc::new(RefCell::new(false));
|
||||
|
||||
let outer_flag = Rc::clone(&outer_completed);
|
||||
let inner_flag = Rc::clone(&inner_completed);
|
||||
let await_flag = Rc::clone(&reached_await);
|
||||
|
||||
// Channel to block the inner task until we're ready
|
||||
let (tx, rx) = futures::channel::oneshot::channel::<()>();
|
||||
|
||||
// We need clones of executor and liveness_token for the inner spawn
|
||||
let inner_executor = foreground_executor.clone();
|
||||
let inner_liveness_token = liveness_token.clone();
|
||||
|
||||
foreground_executor
|
||||
.spawn_context(liveness_token, async move {
|
||||
let inner_task = inner_executor.spawn_context(inner_liveness_token, {
|
||||
let inner_flag = Rc::clone(&inner_flag);
|
||||
async move {
|
||||
rx.await.ok();
|
||||
*inner_flag.borrow_mut() = true;
|
||||
}
|
||||
});
|
||||
|
||||
*await_flag.borrow_mut() = true;
|
||||
|
||||
inner_task.await;
|
||||
|
||||
*outer_flag.borrow_mut() = true;
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Run dispatcher until outer task reaches the await point
|
||||
// The inner task will be blocked on the channel
|
||||
dispatcher.run_until_parked();
|
||||
|
||||
// Verify we actually reached the await point before dropping the app
|
||||
assert!(
|
||||
*reached_await.borrow(),
|
||||
"Outer task should have reached the await point"
|
||||
);
|
||||
|
||||
// Neither task should have completed yet
|
||||
assert!(
|
||||
!*outer_completed.borrow(),
|
||||
"Outer task should not have completed yet"
|
||||
);
|
||||
assert!(
|
||||
!*inner_completed.borrow(),
|
||||
"Inner task should not have completed yet"
|
||||
);
|
||||
|
||||
// Drop the channel sender and app while outer is awaiting inner
|
||||
drop(tx);
|
||||
drop(app);
|
||||
assert!(app_weak.upgrade().is_none(), "App should have been dropped");
|
||||
|
||||
// Run dispatcher - both tasks should be cancelled
|
||||
dispatcher.run_until_parked();
|
||||
|
||||
// Neither task should have completed (both were cancelled)
|
||||
assert!(
|
||||
!*outer_completed.borrow(),
|
||||
"Outer task should have been cancelled, not completed"
|
||||
);
|
||||
assert!(
|
||||
!*inner_completed.borrow(),
|
||||
"Inner task should have been cancelled, not completed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_task_without_app_tracking_still_runs() {
|
||||
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
|
||||
let platform = TestPlatform::new(background_executor, foreground_executor.clone());
|
||||
let asset_source = Arc::new(());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
|
||||
let app = App::new_app(platform, asset_source, http_client);
|
||||
let app_weak = Rc::downgrade(&app);
|
||||
|
||||
let task_ran = Rc::new(RefCell::new(false));
|
||||
let task_ran_clone = Rc::clone(&task_ran);
|
||||
|
||||
let _task = foreground_executor.spawn(async move {
|
||||
*task_ran_clone.borrow_mut() = true;
|
||||
});
|
||||
|
||||
drop(app);
|
||||
|
||||
assert!(app_weak.upgrade().is_none(), "App should have been dropped");
|
||||
|
||||
dispatcher.run_until_parked();
|
||||
|
||||
assert!(
|
||||
*task_ran.borrow(),
|
||||
"Task without app tracking should still run after app is dropped"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_polling_cancelled_task_panics() {
|
||||
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
|
||||
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
|
||||
let asset_source = Arc::new(());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
|
||||
let app = App::new_app(platform, asset_source, http_client);
|
||||
let liveness_token = std::sync::Arc::downgrade(&app.borrow().liveness);
|
||||
let app_weak = Rc::downgrade(&app);
|
||||
|
||||
let task = foreground_executor.spawn_context(liveness_token, async move { 42 });
|
||||
|
||||
drop(app);
|
||||
|
||||
assert!(app_weak.upgrade().is_none(), "App should have been dropped");
|
||||
|
||||
dispatcher.run_until_parked();
|
||||
|
||||
background_executor.block(task);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_polling_cancelled_task_returns_none_with_fallible() {
|
||||
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
|
||||
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
|
||||
let asset_source = Arc::new(());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
|
||||
let app = App::new_app(platform, asset_source, http_client);
|
||||
let liveness_token = std::sync::Arc::downgrade(&app.borrow().liveness);
|
||||
let app_weak = Rc::downgrade(&app);
|
||||
|
||||
let task = foreground_executor
|
||||
.spawn_context(liveness_token, async move { 42 })
|
||||
.fallible();
|
||||
|
||||
drop(app);
|
||||
|
||||
assert!(app_weak.upgrade().is_none(), "App should have been dropped");
|
||||
|
||||
dispatcher.run_until_parked();
|
||||
|
||||
let result = background_executor.block(task);
|
||||
assert_eq!(result, None, "Cancelled task should return None");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,16 +118,23 @@ pub use window::*;
|
||||
/// The context trait, allows the different contexts in GPUI to be used
|
||||
/// interchangeably for certain operations.
|
||||
pub trait AppContext {
|
||||
/// The result type for this context, used for async contexts that
|
||||
/// can't hold a direct reference to the application context.
|
||||
type Result<T>;
|
||||
|
||||
/// Create a new entity in the app context.
|
||||
#[expect(
|
||||
clippy::wrong_self_convention,
|
||||
reason = "`App::new` is an ubiquitous function for creating entities"
|
||||
)]
|
||||
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T>;
|
||||
fn new<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Self::Result<Entity<T>>;
|
||||
|
||||
/// Reserve a slot for a entity to be inserted later.
|
||||
/// The returned [Reservation] allows you to obtain the [EntityId] for the future entity.
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Reservation<T>;
|
||||
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<Reservation<T>>;
|
||||
|
||||
/// Insert a new entity in the app context based on a [Reservation] previously obtained from [`reserve_entity`].
|
||||
///
|
||||
@@ -136,24 +143,28 @@ pub trait AppContext {
|
||||
&mut self,
|
||||
reservation: Reservation<T>,
|
||||
build_entity: impl FnOnce(&mut Context<T>) -> T,
|
||||
) -> Entity<T>;
|
||||
) -> Self::Result<Entity<T>>;
|
||||
|
||||
/// Update a entity in the app context.
|
||||
fn update_entity<T, R>(
|
||||
&mut self,
|
||||
handle: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Context<T>) -> R,
|
||||
) -> R
|
||||
) -> Self::Result<R>
|
||||
where
|
||||
T: 'static;
|
||||
|
||||
/// Update a entity in the app context.
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> GpuiBorrow<'a, T>
|
||||
fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<GpuiBorrow<'a, T>>
|
||||
where
|
||||
T: 'static;
|
||||
|
||||
/// Read a entity from the app context.
|
||||
fn read_entity<T, R>(&self, handle: &Entity<T>, read: impl FnOnce(&T, &App) -> R) -> R
|
||||
fn read_entity<T, R>(
|
||||
&self,
|
||||
handle: &Entity<T>,
|
||||
read: impl FnOnce(&T, &App) -> R,
|
||||
) -> Self::Result<R>
|
||||
where
|
||||
T: 'static;
|
||||
|
||||
@@ -177,7 +188,7 @@ pub trait AppContext {
|
||||
R: Send + 'static;
|
||||
|
||||
/// Read a global from this app context
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> R
|
||||
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &App) -> R) -> Self::Result<R>
|
||||
where
|
||||
G: Global;
|
||||
}
|
||||
@@ -204,24 +215,24 @@ pub trait VisualContext: AppContext {
|
||||
&mut self,
|
||||
entity: &Entity<T>,
|
||||
update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
|
||||
) -> Result<R>;
|
||||
) -> Self::Result<R>;
|
||||
|
||||
/// Create a new entity, with access to `Window`.
|
||||
fn new_window_entity<T: 'static>(
|
||||
&mut self,
|
||||
build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,
|
||||
) -> Result<Entity<T>>;
|
||||
) -> Self::Result<Entity<T>>;
|
||||
|
||||
/// Replace the root view of a window with a new view.
|
||||
fn replace_root_view<V>(
|
||||
&mut self,
|
||||
build_view: impl FnOnce(&mut Window, &mut Context<V>) -> V,
|
||||
) -> Result<Entity<V>>
|
||||
) -> Self::Result<Entity<V>>
|
||||
where
|
||||
V: 'static + Render;
|
||||
|
||||
/// Focus a entity in the window, if it implements the [`Focusable`] trait.
|
||||
fn focus<V>(&mut self, entity: &Entity<V>) -> Result<()>
|
||||
fn focus<V>(&mut self, entity: &Entity<V>) -> Self::Result<()>
|
||||
where
|
||||
V: Focusable;
|
||||
}
|
||||
@@ -273,6 +284,24 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// A flatten equivalent for anyhow `Result`s.
|
||||
pub trait Flatten<T> {
|
||||
/// Convert this type into a simple `Result<T>`.
|
||||
fn flatten(self) -> Result<T>;
|
||||
}
|
||||
|
||||
impl<T> Flatten<T> for Result<Result<T>> {
|
||||
fn flatten(self) -> Result<T> {
|
||||
self?
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Flatten<T> for Result<T> {
|
||||
fn flatten(self) -> Result<T> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the GPU GPUI is running on.
|
||||
#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)]
|
||||
pub struct GpuSpecs {
|
||||
|
||||
@@ -262,18 +262,12 @@ pub(crate) trait Platform: 'static {
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
fn should_auto_hide_scrollbars(&self) -> bool;
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn write_to_primary(&self, item: ClipboardItem);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read_from_find_pasteboard(&self) -> Option<ClipboardItem>;
|
||||
#[cfg(target_os = "macos")]
|
||||
fn write_to_find_pasteboard(&self, item: ClipboardItem);
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||
@@ -575,30 +569,10 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
/// This type is public so that our test macro can generate and use it, but it should not
|
||||
/// be considered part of our public API.
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug)]
|
||||
pub struct RunnableMeta {
|
||||
/// Location of the runnable
|
||||
pub location: &'static core::panic::Location<'static>,
|
||||
/// Weak reference to check if the app is still alive before running this task
|
||||
pub app: Option<std::sync::Weak<()>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RunnableMeta {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RunnableMeta")
|
||||
.field("location", &self.location)
|
||||
.field("app_alive", &self.is_app_alive())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RunnableMeta {
|
||||
/// Returns true if the app is still alive (or if no app tracking is configured).
|
||||
pub fn is_app_alive(&self) -> bool {
|
||||
match &self.app {
|
||||
Some(weak) => weak.strong_count() > 0,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{Action, App, Platform, SharedString};
|
||||
use util::ResultExt;
|
||||
|
||||
/// A menu of the application, either a main menu or a submenu
|
||||
pub struct Menu {
|
||||
@@ -262,18 +263,14 @@ pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) {
|
||||
platform.on_will_open_app_menu(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move || {
|
||||
if let Some(app) = cx.app.upgrade() {
|
||||
app.borrow_mut().update(|cx| cx.clear_pending_keystrokes());
|
||||
}
|
||||
cx.update(|cx| cx.clear_pending_keystrokes()).ok();
|
||||
}
|
||||
}));
|
||||
|
||||
platform.on_validate_app_menu_command(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
cx.app
|
||||
.upgrade()
|
||||
.map(|app| app.borrow_mut().update(|cx| cx.is_action_available(action)))
|
||||
cx.update(|cx| cx.is_action_available(action))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}));
|
||||
@@ -281,9 +278,7 @@ pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &App) {
|
||||
platform.on_app_menu_action(Box::new({
|
||||
let cx = cx.to_async();
|
||||
move |action| {
|
||||
if let Some(app) = cx.app.upgrade() {
|
||||
app.borrow_mut().update(|cx| cx.dispatch_action(action));
|
||||
}
|
||||
cx.update(|cx| cx.dispatch_action(action)).log_err();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -32,11 +32,7 @@ impl HeadlessClient {
|
||||
.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
match runnable {
|
||||
crate::RunnableVariant::Meta(runnable) => {
|
||||
if runnable.metadata().is_app_alive() {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
crate::RunnableVariant::Meta(runnable) => runnable.run(),
|
||||
crate::RunnableVariant::Compat(runnable) => runnable.run(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -502,14 +502,7 @@ impl WaylandClient {
|
||||
let start = Instant::now();
|
||||
let mut timing = match runnable {
|
||||
RunnableVariant::Meta(runnable) => {
|
||||
let metadata = runnable.metadata();
|
||||
let location = metadata.location;
|
||||
|
||||
if !metadata.is_app_alive() {
|
||||
drop(runnable);
|
||||
return;
|
||||
}
|
||||
|
||||
let location = runnable.metadata().location;
|
||||
let timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
|
||||
@@ -316,14 +316,7 @@ impl X11Client {
|
||||
let start = Instant::now();
|
||||
let mut timing = match runnable {
|
||||
RunnableVariant::Meta(runnable) => {
|
||||
let metadata = runnable.metadata();
|
||||
let location = metadata.location;
|
||||
|
||||
if !metadata.is_app_alive() {
|
||||
drop(runnable);
|
||||
return;
|
||||
}
|
||||
|
||||
let location = runnable.metadata().location;
|
||||
let timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
@@ -951,8 +944,6 @@ impl X11Client {
|
||||
let window = self.get_window(event.event)?;
|
||||
window.set_active(false);
|
||||
let mut state = self.0.borrow_mut();
|
||||
// Set last scroll values to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
|
||||
reset_all_pointer_device_scroll_positions(&mut state.pointer_device_states);
|
||||
state.keyboard_focused_window = None;
|
||||
if let Some(compose_state) = state.compose_state.as_mut() {
|
||||
compose_state.reset();
|
||||
|
||||
@@ -431,7 +431,6 @@ impl X11WindowState {
|
||||
// https://stackoverflow.com/questions/43218127/x11-xlib-xcb-creating-a-window-requires-border-pixel-if-specifying-colormap-wh
|
||||
.border_pixel(visual_set.black_pixel)
|
||||
.colormap(colormap)
|
||||
.override_redirect((params.kind == WindowKind::PopUp) as u32)
|
||||
.event_mask(
|
||||
xproto::EventMask::EXPOSURE
|
||||
| xproto::EventMask::STRUCTURE_NOTIFY
|
||||
|
||||
@@ -5,7 +5,6 @@ mod display;
|
||||
mod display_link;
|
||||
mod events;
|
||||
mod keyboard;
|
||||
mod pasteboard;
|
||||
|
||||
#[cfg(feature = "screen-capture")]
|
||||
mod screen_capture;
|
||||
@@ -22,6 +21,8 @@ use metal_renderer as renderer;
|
||||
#[cfg(feature = "macos-blade")]
|
||||
use crate::platform::blade as renderer;
|
||||
|
||||
mod attributed_string;
|
||||
|
||||
#[cfg(feature = "font-kit")]
|
||||
mod open_type;
|
||||
|
||||
|
||||
129
crates/gpui/src/platform/mac/attributed_string.rs
Normal file
129
crates/gpui/src/platform/mac/attributed_string.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use cocoa::base::id;
|
||||
use cocoa::foundation::NSRange;
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
|
||||
/// The `cocoa` crate does not define NSAttributedString (and related Cocoa classes),
|
||||
/// which are needed for copying rich text (that is, text intermingled with images)
|
||||
/// to the clipboard. This adds access to those APIs.
|
||||
#[allow(non_snake_case)]
|
||||
pub trait NSAttributedString: Sized {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSAttributedString), alloc]
|
||||
}
|
||||
|
||||
unsafe fn init_attributed_string(self, string: id) -> id;
|
||||
unsafe fn appendAttributedString_(self, attr_string: id);
|
||||
unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id;
|
||||
unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id;
|
||||
unsafe fn string(self) -> id;
|
||||
}
|
||||
|
||||
impl NSAttributedString for id {
|
||||
unsafe fn init_attributed_string(self, string: id) -> id {
|
||||
msg_send![self, initWithString: string]
|
||||
}
|
||||
|
||||
unsafe fn appendAttributedString_(self, attr_string: id) {
|
||||
let _: () = msg_send![self, appendAttributedString: attr_string];
|
||||
}
|
||||
|
||||
unsafe fn RTFDFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id {
|
||||
msg_send![self, RTFDFromRange: range documentAttributes: attrs]
|
||||
}
|
||||
|
||||
unsafe fn RTFFromRange_documentAttributes_(self, range: NSRange, attrs: id) -> id {
|
||||
msg_send![self, RTFFromRange: range documentAttributes: attrs]
|
||||
}
|
||||
|
||||
unsafe fn string(self) -> id {
|
||||
msg_send![self, string]
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NSMutableAttributedString: NSAttributedString {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSMutableAttributedString), alloc]
|
||||
}
|
||||
}
|
||||
|
||||
impl NSMutableAttributedString for id {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::platform::mac::ns_string;
|
||||
|
||||
use super::*;
|
||||
use cocoa::appkit::NSImage;
|
||||
use cocoa::base::nil;
|
||||
use cocoa::foundation::NSAutoreleasePool;
|
||||
#[test]
|
||||
#[ignore] // This was SIGSEGV-ing on CI but not locally; need to investigate https://github.com/zed-industries/zed/actions/runs/10362363230/job/28684225486?pr=15782#step:4:1348
|
||||
fn test_nsattributed_string() {
|
||||
// TODO move these to parent module once it's actually ready to be used
|
||||
#[allow(non_snake_case)]
|
||||
pub trait NSTextAttachment: Sized {
|
||||
unsafe fn alloc(_: Self) -> id {
|
||||
msg_send![class!(NSTextAttachment), alloc]
|
||||
}
|
||||
}
|
||||
|
||||
impl NSTextAttachment for id {}
|
||||
|
||||
unsafe {
|
||||
let image: id = {
|
||||
let img: id = msg_send![class!(NSImage), alloc];
|
||||
let img: id = msg_send![img, initWithContentsOfFile: ns_string("test.jpeg")];
|
||||
let img: id = msg_send![img, autorelease];
|
||||
img
|
||||
};
|
||||
let _size = image.size();
|
||||
|
||||
let string = ns_string("Test String");
|
||||
let attr_string = NSMutableAttributedString::alloc(nil)
|
||||
.init_attributed_string(string)
|
||||
.autorelease();
|
||||
let hello_string = ns_string("Hello World");
|
||||
let hello_attr_string = NSAttributedString::alloc(nil)
|
||||
.init_attributed_string(hello_string)
|
||||
.autorelease();
|
||||
attr_string.appendAttributedString_(hello_attr_string);
|
||||
|
||||
let attachment: id = msg_send![NSTextAttachment::alloc(nil), autorelease];
|
||||
let _: () = msg_send![attachment, setImage: image];
|
||||
let image_attr_string =
|
||||
msg_send![class!(NSAttributedString), attributedStringWithAttachment: attachment];
|
||||
attr_string.appendAttributedString_(image_attr_string);
|
||||
|
||||
let another_string = ns_string("Another String");
|
||||
let another_attr_string = NSAttributedString::alloc(nil)
|
||||
.init_attributed_string(another_string)
|
||||
.autorelease();
|
||||
attr_string.appendAttributedString_(another_attr_string);
|
||||
|
||||
let _len: cocoa::foundation::NSUInteger = msg_send![attr_string, length];
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// pasteboard.clearContents();
|
||||
|
||||
let rtfd_data = attr_string.RTFDFromRange_documentAttributes_(
|
||||
NSRange::new(0, msg_send![attr_string, length]),
|
||||
nil,
|
||||
);
|
||||
assert_ne!(rtfd_data, nil);
|
||||
// if rtfd_data != nil {
|
||||
// pasteboard.setData_forType(rtfd_data, NSPasteboardTypeRTFD);
|
||||
// }
|
||||
|
||||
// let rtf_data = attributed_string.RTFFromRange_documentAttributes_(
|
||||
// NSRange::new(0, attributed_string.length()),
|
||||
// nil,
|
||||
// );
|
||||
// if rtf_data != nil {
|
||||
// pasteboard.setData_forType(rtf_data, NSPasteboardTypeRTF);
|
||||
// }
|
||||
|
||||
// let plain_text = attributed_string.string();
|
||||
// pasteboard.setString_forType(plain_text, NSPasteboardTypeString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,13 +251,7 @@ extern "C" fn trampoline(runnable: *mut c_void) {
|
||||
let task =
|
||||
unsafe { Runnable::<RunnableMeta>::from_raw(NonNull::new_unchecked(runnable as *mut ())) };
|
||||
|
||||
let metadata = task.metadata();
|
||||
let location = metadata.location;
|
||||
|
||||
if !metadata.is_app_alive() {
|
||||
drop(task);
|
||||
return;
|
||||
}
|
||||
let location = task.metadata().location;
|
||||
|
||||
let start = Instant::now();
|
||||
let timing = TaskTiming {
|
||||
|
||||
@@ -46,9 +46,9 @@ pub unsafe fn new_renderer(
|
||||
_native_window: *mut c_void,
|
||||
_native_view: *mut c_void,
|
||||
_bounds: crate::Size<f32>,
|
||||
_transparent: bool,
|
||||
transparent: bool,
|
||||
) -> Renderer {
|
||||
MetalRenderer::new(context)
|
||||
MetalRenderer::new(context, transparent)
|
||||
}
|
||||
|
||||
pub(crate) struct InstanceBufferPool {
|
||||
@@ -128,7 +128,7 @@ pub struct PathRasterizationVertex {
|
||||
}
|
||||
|
||||
impl MetalRenderer {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>, transparent: bool) -> Self {
|
||||
// Prefer low‐power integrated GPUs on Intel Mac. On Apple
|
||||
// Silicon, there is only ever one GPU, so this is equivalent to
|
||||
// `metal::Device::system_default()`.
|
||||
@@ -152,8 +152,13 @@ impl MetalRenderer {
|
||||
let layer = metal::MetalLayer::new();
|
||||
layer.set_device(&device);
|
||||
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
|
||||
layer.set_opaque(false);
|
||||
// Support direct-to-display rendering if the window is not transparent
|
||||
// https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos
|
||||
layer.set_opaque(!transparent);
|
||||
layer.set_maximum_drawable_count(3);
|
||||
// We already present at display sync with the display link
|
||||
// This allows to use direct-to-display even in window mode
|
||||
layer.set_display_sync_enabled(false);
|
||||
unsafe {
|
||||
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
|
||||
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
|
||||
@@ -352,8 +357,8 @@ impl MetalRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_transparency(&self, _transparent: bool) {
|
||||
// todo(mac)?
|
||||
pub fn update_transparency(&self, transparent: bool) {
|
||||
self.layer.set_opaque(!transparent);
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
|
||||
@@ -1,344 +0,0 @@
|
||||
use core::slice;
|
||||
use std::ffi::c_void;
|
||||
|
||||
use cocoa::{
|
||||
appkit::{NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString, NSPasteboardTypeTIFF},
|
||||
base::{id, nil},
|
||||
foundation::NSData,
|
||||
};
|
||||
use objc::{msg_send, runtime::Object, sel, sel_impl};
|
||||
use strum::IntoEnumIterator as _;
|
||||
|
||||
use crate::{
|
||||
ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, asset_cache::hash,
|
||||
platform::mac::ns_string,
|
||||
};
|
||||
|
||||
pub struct Pasteboard {
|
||||
inner: id,
|
||||
text_hash_type: id,
|
||||
metadata_type: id,
|
||||
}
|
||||
|
||||
impl Pasteboard {
|
||||
pub fn general() -> Self {
|
||||
unsafe { Self::new(NSPasteboard::generalPasteboard(nil)) }
|
||||
}
|
||||
|
||||
pub fn find() -> Self {
|
||||
unsafe { Self::new(NSPasteboard::pasteboardWithName(nil, NSPasteboardNameFind)) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn unique() -> Self {
|
||||
unsafe { Self::new(NSPasteboard::pasteboardWithUniqueName(nil)) }
|
||||
}
|
||||
|
||||
unsafe fn new(inner: id) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
text_hash_type: unsafe { ns_string("zed-text-hash") },
|
||||
metadata_type: unsafe { ns_string("zed-metadata") },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self) -> Option<ClipboardItem> {
|
||||
// First, see if it's a string.
|
||||
unsafe {
|
||||
let pasteboard_types: id = self.inner.types();
|
||||
let string_type: id = ns_string("public.utf8-plain-text");
|
||||
|
||||
if msg_send![pasteboard_types, containsObject: string_type] {
|
||||
let data = self.inner.dataForType(string_type);
|
||||
if data == nil {
|
||||
return None;
|
||||
} else if data.bytes().is_null() {
|
||||
// https://developer.apple.com/documentation/foundation/nsdata/1410616-bytes?language=objc
|
||||
// "If the length of the NSData object is 0, this property returns nil."
|
||||
return Some(self.read_string(&[]));
|
||||
} else {
|
||||
let bytes =
|
||||
slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize);
|
||||
|
||||
return Some(self.read_string(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't a string, try the various supported image types.
|
||||
for format in ImageFormat::iter() {
|
||||
if let Some(item) = self.read_image(format) {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't a string or a supported image type, give up.
|
||||
None
|
||||
}
|
||||
|
||||
fn read_image(&self, format: ImageFormat) -> Option<ClipboardItem> {
|
||||
let mut ut_type: UTType = format.into();
|
||||
|
||||
unsafe {
|
||||
let types: id = self.inner.types();
|
||||
if msg_send![types, containsObject: ut_type.inner()] {
|
||||
self.data_for_type(ut_type.inner_mut()).map(|bytes| {
|
||||
let bytes = bytes.to_vec();
|
||||
let id = hash(&bytes);
|
||||
|
||||
ClipboardItem {
|
||||
entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_string(&self, text_bytes: &[u8]) -> ClipboardItem {
|
||||
unsafe {
|
||||
let text = String::from_utf8_lossy(text_bytes).to_string();
|
||||
let metadata = self
|
||||
.data_for_type(self.text_hash_type)
|
||||
.and_then(|hash_bytes| {
|
||||
let hash_bytes = hash_bytes.try_into().ok()?;
|
||||
let hash = u64::from_be_bytes(hash_bytes);
|
||||
let metadata = self.data_for_type(self.metadata_type)?;
|
||||
|
||||
if hash == ClipboardString::text_hash(&text) {
|
||||
String::from_utf8(metadata.to_vec()).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
ClipboardItem {
|
||||
entries: vec![ClipboardEntry::String(ClipboardString { text, metadata })],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn data_for_type(&self, kind: id) -> Option<&[u8]> {
|
||||
unsafe {
|
||||
let data = self.inner.dataForType(kind);
|
||||
if data == nil {
|
||||
None
|
||||
} else {
|
||||
Some(slice::from_raw_parts(
|
||||
data.bytes() as *mut u8,
|
||||
data.length() as usize,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&self, item: ClipboardItem) {
|
||||
unsafe {
|
||||
match item.entries.as_slice() {
|
||||
[] => {
|
||||
// Writing an empty list of entries just clears the clipboard.
|
||||
self.inner.clearContents();
|
||||
}
|
||||
[ClipboardEntry::String(string)] => {
|
||||
self.write_plaintext(string);
|
||||
}
|
||||
[ClipboardEntry::Image(image)] => {
|
||||
self.write_image(image);
|
||||
}
|
||||
[ClipboardEntry::ExternalPaths(_)] => {}
|
||||
_ => {
|
||||
// Agus NB: We're currently only writing string entries to the clipboard when we have more than one.
|
||||
//
|
||||
// This was the existing behavior before I refactored the outer clipboard code:
|
||||
// https://github.com/zed-industries/zed/blob/65f7412a0265552b06ce122655369d6cc7381dd6/crates/gpui/src/platform/mac/platform.rs#L1060-L1110
|
||||
//
|
||||
// Note how `any_images` is always `false`. We should fix that, but that's orthogonal to the refactor.
|
||||
|
||||
let mut combined = ClipboardString {
|
||||
text: String::new(),
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
for entry in item.entries {
|
||||
match entry {
|
||||
ClipboardEntry::String(text) => {
|
||||
combined.text.push_str(&text.text());
|
||||
if combined.metadata.is_none() {
|
||||
combined.metadata = text.metadata;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.write_plaintext(&combined);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_plaintext(&self, string: &ClipboardString) {
|
||||
unsafe {
|
||||
self.inner.clearContents();
|
||||
|
||||
let text_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
string.text.as_ptr() as *const c_void,
|
||||
string.text.len() as u64,
|
||||
);
|
||||
self.inner
|
||||
.setData_forType(text_bytes, NSPasteboardTypeString);
|
||||
|
||||
if let Some(metadata) = string.metadata.as_ref() {
|
||||
let hash_bytes = ClipboardString::text_hash(&string.text).to_be_bytes();
|
||||
let hash_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
hash_bytes.as_ptr() as *const c_void,
|
||||
hash_bytes.len() as u64,
|
||||
);
|
||||
self.inner.setData_forType(hash_bytes, self.text_hash_type);
|
||||
|
||||
let metadata_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
metadata.as_ptr() as *const c_void,
|
||||
metadata.len() as u64,
|
||||
);
|
||||
self.inner
|
||||
.setData_forType(metadata_bytes, self.metadata_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn write_image(&self, image: &Image) {
|
||||
unsafe {
|
||||
self.inner.clearContents();
|
||||
|
||||
let bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
image.bytes.as_ptr() as *const c_void,
|
||||
image.bytes.len() as u64,
|
||||
);
|
||||
|
||||
self.inner
|
||||
.setData_forType(bytes, Into::<UTType>::into(image.format).inner_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[link(name = "AppKit", kind = "framework")]
|
||||
unsafe extern "C" {
|
||||
/// [Apple's documentation](https://developer.apple.com/documentation/appkit/nspasteboardnamefind?language=objc)
|
||||
pub static NSPasteboardNameFind: id;
|
||||
}
|
||||
|
||||
impl From<ImageFormat> for UTType {
|
||||
fn from(value: ImageFormat) -> Self {
|
||||
match value {
|
||||
ImageFormat::Png => Self::png(),
|
||||
ImageFormat::Jpeg => Self::jpeg(),
|
||||
ImageFormat::Tiff => Self::tiff(),
|
||||
ImageFormat::Webp => Self::webp(),
|
||||
ImageFormat::Gif => Self::gif(),
|
||||
ImageFormat::Bmp => Self::bmp(),
|
||||
ImageFormat::Svg => Self::svg(),
|
||||
ImageFormat::Ico => Self::ico(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/
|
||||
pub struct UTType(id);
|
||||
|
||||
impl UTType {
|
||||
pub fn png() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png
|
||||
Self(unsafe { NSPasteboardTypePNG }) // This is a rare case where there's a built-in NSPasteboardType
|
||||
}
|
||||
|
||||
pub fn jpeg() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/jpeg
|
||||
Self(unsafe { ns_string("public.jpeg") })
|
||||
}
|
||||
|
||||
pub fn gif() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/gif
|
||||
Self(unsafe { ns_string("com.compuserve.gif") })
|
||||
}
|
||||
|
||||
pub fn webp() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/webp
|
||||
Self(unsafe { ns_string("org.webmproject.webp") })
|
||||
}
|
||||
|
||||
pub fn bmp() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/bmp
|
||||
Self(unsafe { ns_string("com.microsoft.bmp") })
|
||||
}
|
||||
|
||||
pub fn svg() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/svg
|
||||
Self(unsafe { ns_string("public.svg-image") })
|
||||
}
|
||||
|
||||
pub fn ico() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico
|
||||
Self(unsafe { ns_string("com.microsoft.ico") })
|
||||
}
|
||||
|
||||
pub fn tiff() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff
|
||||
Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType
|
||||
}
|
||||
|
||||
fn inner(&self) -> *const Object {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn inner_mut(&self) -> *mut Object {
|
||||
self.0 as *mut _
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use cocoa::{appkit::NSPasteboardTypeString, foundation::NSData};
|
||||
|
||||
use crate::{ClipboardEntry, ClipboardItem, ClipboardString};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_string() {
|
||||
let pasteboard = Pasteboard::unique();
|
||||
assert_eq!(pasteboard.read(), None);
|
||||
|
||||
let item = ClipboardItem::new_string("1".to_string());
|
||||
pasteboard.write(item.clone());
|
||||
assert_eq!(pasteboard.read(), Some(item));
|
||||
|
||||
let item = ClipboardItem {
|
||||
entries: vec![ClipboardEntry::String(
|
||||
ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]),
|
||||
)],
|
||||
};
|
||||
pasteboard.write(item.clone());
|
||||
assert_eq!(pasteboard.read(), Some(item));
|
||||
|
||||
let text_from_other_app = "text from other app";
|
||||
unsafe {
|
||||
let bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
text_from_other_app.as_ptr() as *const c_void,
|
||||
text_from_other_app.len() as u64,
|
||||
);
|
||||
pasteboard
|
||||
.inner
|
||||
.setData_forType(bytes, NSPasteboardTypeString);
|
||||
}
|
||||
assert_eq!(
|
||||
pasteboard.read(),
|
||||
Some(ClipboardItem::new_string(text_from_other_app.to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
use super::{
|
||||
BoolExt, MacKeyboardLayout, MacKeyboardMapper, events::key_to_native, ns_string, renderer,
|
||||
BoolExt, MacKeyboardLayout, MacKeyboardMapper,
|
||||
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
||||
events::key_to_native,
|
||||
ns_string, renderer,
|
||||
};
|
||||
use crate::{
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor,
|
||||
KeyContext, Keymap, MacDispatcher, MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu,
|
||||
PathPromptOptions, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PlatformWindow, Result, SystemMenuType, Task, WindowAppearance,
|
||||
WindowParams, platform::mac::pasteboard::Pasteboard,
|
||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PlatformWindow, Result, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
|
||||
};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
appkit::{
|
||||
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSSavePanel,
|
||||
NSVisualEffectState, NSVisualEffectView, NSWindow,
|
||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
||||
NSPasteboardTypePNG, NSPasteboardTypeRTF, NSPasteboardTypeRTFD, NSPasteboardTypeString,
|
||||
NSPasteboardTypeTIFF, NSSavePanel, NSVisualEffectState, NSVisualEffectView, NSWindow,
|
||||
},
|
||||
base::{BOOL, NO, YES, id, nil, selector},
|
||||
foundation::{
|
||||
NSArray, NSAutoreleasePool, NSBundle, NSInteger, NSProcessInfo, NSString, NSUInteger, NSURL,
|
||||
NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSRange, NSString,
|
||||
NSUInteger, NSURL,
|
||||
},
|
||||
};
|
||||
use core_foundation::{
|
||||
@@ -44,6 +49,7 @@ use ptr::null_mut;
|
||||
use semver::Version;
|
||||
use std::{
|
||||
cell::Cell,
|
||||
convert::TryInto,
|
||||
ffi::{CStr, OsStr, c_void},
|
||||
os::{raw::c_char, unix::ffi::OsStrExt},
|
||||
path::{Path, PathBuf},
|
||||
@@ -52,6 +58,7 @@ use std::{
|
||||
slice, str,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use strum::IntoEnumIterator;
|
||||
use util::{
|
||||
ResultExt,
|
||||
command::{new_smol_command, new_std_command},
|
||||
@@ -157,8 +164,9 @@ pub(crate) struct MacPlatformState {
|
||||
text_system: Arc<dyn PlatformTextSystem>,
|
||||
renderer_context: renderer::Context,
|
||||
headless: bool,
|
||||
general_pasteboard: Pasteboard,
|
||||
find_pasteboard: Pasteboard,
|
||||
pasteboard: id,
|
||||
text_hash_pasteboard_type: id,
|
||||
metadata_pasteboard_type: id,
|
||||
reopen: Option<Box<dyn FnMut()>>,
|
||||
on_keyboard_layout_change: Option<Box<dyn FnMut()>>,
|
||||
quit: Option<Box<dyn FnMut()>>,
|
||||
@@ -198,8 +206,9 @@ impl MacPlatform {
|
||||
background_executor: BackgroundExecutor::new(dispatcher.clone()),
|
||||
foreground_executor: ForegroundExecutor::new(dispatcher),
|
||||
renderer_context: renderer::Context::default(),
|
||||
general_pasteboard: Pasteboard::general(),
|
||||
find_pasteboard: Pasteboard::find(),
|
||||
pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
|
||||
text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
|
||||
metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
|
||||
reopen: None,
|
||||
quit: None,
|
||||
menu_command: None,
|
||||
@@ -215,6 +224,20 @@ impl MacPlatform {
|
||||
}))
|
||||
}
|
||||
|
||||
unsafe fn read_from_pasteboard(&self, pasteboard: *mut Object, kind: id) -> Option<&[u8]> {
|
||||
unsafe {
|
||||
let data = pasteboard.dataForType(kind);
|
||||
if data == nil {
|
||||
None
|
||||
} else {
|
||||
Some(slice::from_raw_parts(
|
||||
data.bytes() as *mut u8,
|
||||
data.length() as usize,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn create_menu_bar(
|
||||
&self,
|
||||
menus: &Vec<Menu>,
|
||||
@@ -687,7 +710,7 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
|
||||
self.background_executor()
|
||||
.spawn(async { done_rx.await.map_err(|e| anyhow!(e))? })
|
||||
.spawn(async { crate::Flatten::flatten(done_rx.await.map_err(|e| anyhow!(e))) })
|
||||
}
|
||||
|
||||
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
|
||||
@@ -1011,24 +1034,119 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
use crate::ClipboardEntry;
|
||||
|
||||
unsafe {
|
||||
// We only want to use NSAttributedString if there are multiple entries to write.
|
||||
if item.entries.len() <= 1 {
|
||||
match item.entries.first() {
|
||||
Some(entry) => match entry {
|
||||
ClipboardEntry::String(string) => {
|
||||
self.write_plaintext_to_clipboard(string);
|
||||
}
|
||||
ClipboardEntry::Image(image) => {
|
||||
self.write_image_to_clipboard(image);
|
||||
}
|
||||
ClipboardEntry::ExternalPaths(_) => {}
|
||||
},
|
||||
None => {
|
||||
// Writing an empty list of entries just clears the clipboard.
|
||||
let state = self.0.lock();
|
||||
state.pasteboard.clearContents();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut any_images = false;
|
||||
let attributed_string = {
|
||||
let mut buf = NSMutableAttributedString::alloc(nil)
|
||||
// TODO can we skip this? Or at least part of it?
|
||||
.init_attributed_string(ns_string(""))
|
||||
.autorelease();
|
||||
|
||||
for entry in item.entries {
|
||||
if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry
|
||||
{
|
||||
let to_append = NSAttributedString::alloc(nil)
|
||||
.init_attributed_string(ns_string(&text))
|
||||
.autorelease();
|
||||
|
||||
buf.appendAttributedString_(to_append);
|
||||
}
|
||||
}
|
||||
|
||||
buf
|
||||
};
|
||||
|
||||
let state = self.0.lock();
|
||||
state.pasteboard.clearContents();
|
||||
|
||||
// Only set rich text clipboard types if we actually have 1+ images to include.
|
||||
if any_images {
|
||||
let rtfd_data = attributed_string.RTFDFromRange_documentAttributes_(
|
||||
NSRange::new(0, msg_send![attributed_string, length]),
|
||||
nil,
|
||||
);
|
||||
if rtfd_data != nil {
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(rtfd_data, NSPasteboardTypeRTFD);
|
||||
}
|
||||
|
||||
let rtf_data = attributed_string.RTFFromRange_documentAttributes_(
|
||||
NSRange::new(0, attributed_string.length()),
|
||||
nil,
|
||||
);
|
||||
if rtf_data != nil {
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(rtf_data, NSPasteboardTypeRTF);
|
||||
}
|
||||
}
|
||||
|
||||
let plain_text = attributed_string.string();
|
||||
state
|
||||
.pasteboard
|
||||
.setString_forType(plain_text, NSPasteboardTypeString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
let state = self.0.lock();
|
||||
state.general_pasteboard.read()
|
||||
}
|
||||
let pasteboard = state.pasteboard;
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
let state = self.0.lock();
|
||||
state.general_pasteboard.write(item);
|
||||
}
|
||||
// First, see if it's a string.
|
||||
unsafe {
|
||||
let types: id = pasteboard.types();
|
||||
let string_type: id = ns_string("public.utf8-plain-text");
|
||||
|
||||
fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
|
||||
let state = self.0.lock();
|
||||
state.find_pasteboard.read()
|
||||
}
|
||||
if msg_send![types, containsObject: string_type] {
|
||||
let data = pasteboard.dataForType(string_type);
|
||||
if data == nil {
|
||||
return None;
|
||||
} else if data.bytes().is_null() {
|
||||
// https://developer.apple.com/documentation/foundation/nsdata/1410616-bytes?language=objc
|
||||
// "If the length of the NSData object is 0, this property returns nil."
|
||||
return Some(self.read_string_from_clipboard(&state, &[]));
|
||||
} else {
|
||||
let bytes =
|
||||
slice::from_raw_parts(data.bytes() as *mut u8, data.length() as usize);
|
||||
|
||||
fn write_to_find_pasteboard(&self, item: ClipboardItem) {
|
||||
let state = self.0.lock();
|
||||
state.find_pasteboard.write(item);
|
||||
return Some(self.read_string_from_clipboard(&state, bytes));
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't a string, try the various supported image types.
|
||||
for format in ImageFormat::iter() {
|
||||
if let Some(item) = try_clipboard_image(pasteboard, format) {
|
||||
return Some(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it wasn't a string or a supported image type, give up.
|
||||
None
|
||||
}
|
||||
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
|
||||
@@ -1137,6 +1255,116 @@ impl Platform for MacPlatform {
|
||||
}
|
||||
}
|
||||
|
||||
impl MacPlatform {
|
||||
unsafe fn read_string_from_clipboard(
|
||||
&self,
|
||||
state: &MacPlatformState,
|
||||
text_bytes: &[u8],
|
||||
) -> ClipboardItem {
|
||||
unsafe {
|
||||
let text = String::from_utf8_lossy(text_bytes).to_string();
|
||||
let metadata = self
|
||||
.read_from_pasteboard(state.pasteboard, state.text_hash_pasteboard_type)
|
||||
.and_then(|hash_bytes| {
|
||||
let hash_bytes = hash_bytes.try_into().ok()?;
|
||||
let hash = u64::from_be_bytes(hash_bytes);
|
||||
let metadata = self
|
||||
.read_from_pasteboard(state.pasteboard, state.metadata_pasteboard_type)?;
|
||||
|
||||
if hash == ClipboardString::text_hash(&text) {
|
||||
String::from_utf8(metadata.to_vec()).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
ClipboardItem {
|
||||
entries: vec![ClipboardEntry::String(ClipboardString { text, metadata })],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn write_plaintext_to_clipboard(&self, string: &ClipboardString) {
|
||||
unsafe {
|
||||
let state = self.0.lock();
|
||||
state.pasteboard.clearContents();
|
||||
|
||||
let text_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
string.text.as_ptr() as *const c_void,
|
||||
string.text.len() as u64,
|
||||
);
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(text_bytes, NSPasteboardTypeString);
|
||||
|
||||
if let Some(metadata) = string.metadata.as_ref() {
|
||||
let hash_bytes = ClipboardString::text_hash(&string.text).to_be_bytes();
|
||||
let hash_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
hash_bytes.as_ptr() as *const c_void,
|
||||
hash_bytes.len() as u64,
|
||||
);
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(hash_bytes, state.text_hash_pasteboard_type);
|
||||
|
||||
let metadata_bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
metadata.as_ptr() as *const c_void,
|
||||
metadata.len() as u64,
|
||||
);
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(metadata_bytes, state.metadata_pasteboard_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn write_image_to_clipboard(&self, image: &Image) {
|
||||
unsafe {
|
||||
let state = self.0.lock();
|
||||
state.pasteboard.clearContents();
|
||||
|
||||
let bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
image.bytes.as_ptr() as *const c_void,
|
||||
image.bytes.len() as u64,
|
||||
);
|
||||
|
||||
state
|
||||
.pasteboard
|
||||
.setData_forType(bytes, Into::<UTType>::into(image.format).inner_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_clipboard_image(pasteboard: id, format: ImageFormat) -> Option<ClipboardItem> {
|
||||
let mut ut_type: UTType = format.into();
|
||||
|
||||
unsafe {
|
||||
let types: id = pasteboard.types();
|
||||
if msg_send![types, containsObject: ut_type.inner()] {
|
||||
let data = pasteboard.dataForType(ut_type.inner_mut());
|
||||
if data == nil {
|
||||
None
|
||||
} else {
|
||||
let bytes = Vec::from(slice::from_raw_parts(
|
||||
data.bytes() as *mut u8,
|
||||
data.length() as usize,
|
||||
));
|
||||
let id = hash(&bytes);
|
||||
|
||||
Some(ClipboardItem {
|
||||
entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn path_from_objc(path: id) -> PathBuf {
|
||||
let len = msg_send![path, lengthOfBytesUsingEncoding: NSUTF8StringEncoding];
|
||||
let bytes = unsafe { path.UTF8String() as *const u8 };
|
||||
@@ -1377,3 +1605,120 @@ mod security {
|
||||
pub const errSecUserCanceled: OSStatus = -128;
|
||||
pub const errSecItemNotFound: OSStatus = -25300;
|
||||
}
|
||||
|
||||
impl From<ImageFormat> for UTType {
|
||||
fn from(value: ImageFormat) -> Self {
|
||||
match value {
|
||||
ImageFormat::Png => Self::png(),
|
||||
ImageFormat::Jpeg => Self::jpeg(),
|
||||
ImageFormat::Tiff => Self::tiff(),
|
||||
ImageFormat::Webp => Self::webp(),
|
||||
ImageFormat::Gif => Self::gif(),
|
||||
ImageFormat::Bmp => Self::bmp(),
|
||||
ImageFormat::Svg => Self::svg(),
|
||||
ImageFormat::Ico => Self::ico(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/
|
||||
struct UTType(id);
|
||||
|
||||
impl UTType {
|
||||
pub fn png() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/png
|
||||
Self(unsafe { NSPasteboardTypePNG }) // This is a rare case where there's a built-in NSPasteboardType
|
||||
}
|
||||
|
||||
pub fn jpeg() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/jpeg
|
||||
Self(unsafe { ns_string("public.jpeg") })
|
||||
}
|
||||
|
||||
pub fn gif() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/gif
|
||||
Self(unsafe { ns_string("com.compuserve.gif") })
|
||||
}
|
||||
|
||||
pub fn webp() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/webp
|
||||
Self(unsafe { ns_string("org.webmproject.webp") })
|
||||
}
|
||||
|
||||
pub fn bmp() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/bmp
|
||||
Self(unsafe { ns_string("com.microsoft.bmp") })
|
||||
}
|
||||
|
||||
pub fn svg() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/svg
|
||||
Self(unsafe { ns_string("public.svg-image") })
|
||||
}
|
||||
|
||||
pub fn ico() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico
|
||||
Self(unsafe { ns_string("com.microsoft.ico") })
|
||||
}
|
||||
|
||||
pub fn tiff() -> Self {
|
||||
// https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff
|
||||
Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType
|
||||
}
|
||||
|
||||
fn inner(&self) -> *const Object {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn inner_mut(&self) -> *mut Object {
|
||||
self.0 as *mut _
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ClipboardItem;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_clipboard() {
|
||||
let platform = build_platform();
|
||||
assert_eq!(platform.read_from_clipboard(), None);
|
||||
|
||||
let item = ClipboardItem::new_string("1".to_string());
|
||||
platform.write_to_clipboard(item.clone());
|
||||
assert_eq!(platform.read_from_clipboard(), Some(item));
|
||||
|
||||
let item = ClipboardItem {
|
||||
entries: vec![ClipboardEntry::String(
|
||||
ClipboardString::new("2".to_string()).with_json_metadata(vec![3, 4]),
|
||||
)],
|
||||
};
|
||||
platform.write_to_clipboard(item.clone());
|
||||
assert_eq!(platform.read_from_clipboard(), Some(item));
|
||||
|
||||
let text_from_other_app = "text from other app";
|
||||
unsafe {
|
||||
let bytes = NSData::dataWithBytes_length_(
|
||||
nil,
|
||||
text_from_other_app.as_ptr() as *const c_void,
|
||||
text_from_other_app.len() as u64,
|
||||
);
|
||||
platform
|
||||
.0
|
||||
.lock()
|
||||
.pasteboard
|
||||
.setData_forType(bytes, NSPasteboardTypeString);
|
||||
}
|
||||
assert_eq!(
|
||||
platform.read_from_clipboard(),
|
||||
Some(ClipboardItem::new_string(text_from_other_app.to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
fn build_platform() -> MacPlatform {
|
||||
let platform = MacPlatform::new(false);
|
||||
platform.0.lock().pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
|
||||
platform
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,14 +177,7 @@ impl TestDispatcher {
|
||||
|
||||
// todo(localcc): add timings to tests
|
||||
match runnable {
|
||||
RunnableVariant::Meta(runnable) => {
|
||||
if !runnable.metadata().is_app_alive() {
|
||||
drop(runnable);
|
||||
self.state.lock().is_main_thread = was_main_thread;
|
||||
return true;
|
||||
}
|
||||
runnable.run()
|
||||
}
|
||||
RunnableVariant::Meta(runnable) => runnable.run(),
|
||||
RunnableVariant::Compat(runnable) => runnable.run(),
|
||||
};
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@ pub(crate) struct TestPlatform {
|
||||
current_clipboard_item: Mutex<Option<ClipboardItem>>,
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
current_primary_item: Mutex<Option<ClipboardItem>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
current_find_pasteboard_item: Mutex<Option<ClipboardItem>>,
|
||||
pub(crate) prompts: RefCell<TestPrompts>,
|
||||
screen_capture_sources: RefCell<Vec<TestScreenCaptureSource>>,
|
||||
pub opened_url: RefCell<Option<String>>,
|
||||
@@ -119,8 +117,6 @@ impl TestPlatform {
|
||||
current_clipboard_item: Mutex::new(None),
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
current_primary_item: Mutex::new(None),
|
||||
#[cfg(target_os = "macos")]
|
||||
current_find_pasteboard_item: Mutex::new(None),
|
||||
weak: weak.clone(),
|
||||
opened_url: Default::default(),
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -402,8 +398,9 @@ impl Platform for TestPlatform {
|
||||
false
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.current_clipboard_item.lock().clone()
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn write_to_primary(&self, item: ClipboardItem) {
|
||||
*self.current_primary_item.lock() = Some(item);
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
@@ -415,19 +412,8 @@ impl Platform for TestPlatform {
|
||||
self.current_primary_item.lock().clone()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn write_to_primary(&self, item: ClipboardItem) {
|
||||
*self.current_primary_item.lock() = Some(item);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn read_from_find_pasteboard(&self) -> Option<ClipboardItem> {
|
||||
self.current_find_pasteboard_item.lock().clone()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn write_to_find_pasteboard(&self, item: ClipboardItem) {
|
||||
*self.current_find_pasteboard_item.lock() = Some(item);
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
self.current_clipboard_item.lock().clone()
|
||||
}
|
||||
|
||||
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Task<Result<()>> {
|
||||
|
||||
@@ -73,14 +73,7 @@ impl WindowsDispatcher {
|
||||
|
||||
let mut timing = match runnable {
|
||||
RunnableVariant::Meta(runnable) => {
|
||||
let metadata = runnable.metadata();
|
||||
let location = metadata.location;
|
||||
|
||||
if !metadata.is_app_alive() {
|
||||
drop(runnable);
|
||||
return;
|
||||
}
|
||||
|
||||
let location = runnable.metadata().location;
|
||||
let timing = TaskTiming {
|
||||
location,
|
||||
start,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
fmt,
|
||||
iter::FusedIterator,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
@@ -10,9 +9,9 @@ use rand::{Rng, SeedableRng, rngs::SmallRng};
|
||||
use crate::Priority;
|
||||
|
||||
struct PriorityQueues<T> {
|
||||
high_priority: VecDeque<T>,
|
||||
medium_priority: VecDeque<T>,
|
||||
low_priority: VecDeque<T>,
|
||||
high_priority: Vec<T>,
|
||||
medium_priority: Vec<T>,
|
||||
low_priority: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> PriorityQueues<T> {
|
||||
@@ -43,9 +42,9 @@ impl<T> PriorityQueueState<T> {
|
||||
let mut queues = self.queues.lock();
|
||||
match priority {
|
||||
Priority::Realtime(_) => unreachable!(),
|
||||
Priority::High => queues.high_priority.push_back(item),
|
||||
Priority::Medium => queues.medium_priority.push_back(item),
|
||||
Priority::Low => queues.low_priority.push_back(item),
|
||||
Priority::High => queues.high_priority.push(item),
|
||||
Priority::Medium => queues.medium_priority.push(item),
|
||||
Priority::Low => queues.low_priority.push(item),
|
||||
};
|
||||
self.condvar.notify_one();
|
||||
Ok(())
|
||||
@@ -142,9 +141,9 @@ impl<T> PriorityQueueReceiver<T> {
|
||||
pub(crate) fn new() -> (PriorityQueueSender<T>, Self) {
|
||||
let state = PriorityQueueState {
|
||||
queues: parking_lot::Mutex::new(PriorityQueues {
|
||||
high_priority: VecDeque::new(),
|
||||
medium_priority: VecDeque::new(),
|
||||
low_priority: VecDeque::new(),
|
||||
high_priority: Vec::new(),
|
||||
medium_priority: Vec::new(),
|
||||
low_priority: Vec::new(),
|
||||
}),
|
||||
condvar: parking_lot::Condvar::new(),
|
||||
receiver_count: AtomicUsize::new(1),
|
||||
@@ -227,7 +226,7 @@ impl<T> PriorityQueueReceiver<T> {
|
||||
if !queues.high_priority.is_empty() {
|
||||
let flip = self.rand.random_ratio(P::High.probability(), mass);
|
||||
if flip {
|
||||
return Ok(queues.high_priority.pop_front());
|
||||
return Ok(queues.high_priority.pop());
|
||||
}
|
||||
mass -= P::High.probability();
|
||||
}
|
||||
@@ -235,7 +234,7 @@ impl<T> PriorityQueueReceiver<T> {
|
||||
if !queues.medium_priority.is_empty() {
|
||||
let flip = self.rand.random_ratio(P::Medium.probability(), mass);
|
||||
if flip {
|
||||
return Ok(queues.medium_priority.pop_front());
|
||||
return Ok(queues.medium_priority.pop());
|
||||
}
|
||||
mass -= P::Medium.probability();
|
||||
}
|
||||
@@ -243,7 +242,7 @@ impl<T> PriorityQueueReceiver<T> {
|
||||
if !queues.low_priority.is_empty() {
|
||||
let flip = self.rand.random_ratio(P::Low.probability(), mass);
|
||||
if flip {
|
||||
return Ok(queues.low_priority.pop_front());
|
||||
return Ok(queues.low_priority.pop());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -334,13 +334,9 @@ pub enum WhiteSpace {
|
||||
/// How to truncate text that overflows the width of the element
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum TextOverflow {
|
||||
/// Truncate the text at the end when it doesn't fit, and represent this truncation by
|
||||
/// displaying the provided string (e.g., "very long te…").
|
||||
/// Truncate the text when it doesn't fit, and represent this truncation by displaying the
|
||||
/// provided string.
|
||||
Truncate(SharedString),
|
||||
/// Truncate the text at the start when it doesn't fit, and represent this truncation by
|
||||
/// displaying the provided string at the beginning (e.g., "…ong text here").
|
||||
/// Typically more adequate for file paths where the end is more important than the beginning.
|
||||
TruncateStart(SharedString),
|
||||
}
|
||||
|
||||
/// How to align text within the element
|
||||
|
||||
@@ -75,21 +75,13 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the truncate overflowing text with an ellipsis (…) at the end if needed.
|
||||
/// Sets the truncate overflowing text with an ellipsis (…) if needed.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-overflow#ellipsis)
|
||||
fn text_ellipsis(mut self) -> Self {
|
||||
self.text_style().text_overflow = Some(TextOverflow::Truncate(ELLIPSIS));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the truncate overflowing text with an ellipsis (…) at the start if needed.
|
||||
/// Typically more adequate for file paths where the end is more important than the beginning.
|
||||
/// Note: This doesn't exist in Tailwind CSS.
|
||||
fn text_ellipsis_start(mut self) -> Self {
|
||||
self.text_style().text_overflow = Some(TextOverflow::TruncateStart(ELLIPSIS));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text overflow behavior of the element.
|
||||
fn text_overflow(mut self, overflow: TextOverflow) -> Self {
|
||||
self.text_style().text_overflow = Some(overflow);
|
||||
|
||||
@@ -64,8 +64,6 @@ impl ShapedLine {
|
||||
&self,
|
||||
origin: Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
align: TextAlign,
|
||||
align_width: Option<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Result<()> {
|
||||
@@ -73,8 +71,8 @@ impl ShapedLine {
|
||||
origin,
|
||||
&self.layout,
|
||||
line_height,
|
||||
align,
|
||||
align_width,
|
||||
TextAlign::default(),
|
||||
None,
|
||||
&self.decoration_runs,
|
||||
&[],
|
||||
window,
|
||||
@@ -89,8 +87,6 @@ impl ShapedLine {
|
||||
&self,
|
||||
origin: Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
align: TextAlign,
|
||||
align_width: Option<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Result<()> {
|
||||
@@ -98,8 +94,8 @@ impl ShapedLine {
|
||||
origin,
|
||||
&self.layout,
|
||||
line_height,
|
||||
align,
|
||||
align_width,
|
||||
TextAlign::default(),
|
||||
None,
|
||||
&self.decoration_runs,
|
||||
&[],
|
||||
window,
|
||||
|
||||
@@ -2,15 +2,6 @@ use crate::{FontId, FontRun, Pixels, PlatformTextSystem, SharedString, TextRun,
|
||||
use collections::HashMap;
|
||||
use std::{borrow::Cow, iter, sync::Arc};
|
||||
|
||||
/// Determines whether to truncate text from the start or end.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TruncateFrom {
|
||||
/// Truncate text from the start.
|
||||
Start,
|
||||
/// Truncate text from the end.
|
||||
End,
|
||||
}
|
||||
|
||||
/// The GPUI line wrapper, used to wrap lines of text to a given width.
|
||||
pub struct LineWrapper {
|
||||
platform_text_system: Arc<dyn PlatformTextSystem>,
|
||||
@@ -138,50 +129,29 @@ impl LineWrapper {
|
||||
}
|
||||
|
||||
/// Determines if a line should be truncated based on its width.
|
||||
///
|
||||
/// Returns the truncation index in `line`.
|
||||
pub fn should_truncate_line(
|
||||
&mut self,
|
||||
line: &str,
|
||||
truncate_width: Pixels,
|
||||
truncation_affix: &str,
|
||||
truncate_from: TruncateFrom,
|
||||
truncation_suffix: &str,
|
||||
) -> Option<usize> {
|
||||
let mut width = px(0.);
|
||||
let suffix_width = truncation_affix
|
||||
let suffix_width = truncation_suffix
|
||||
.chars()
|
||||
.map(|c| self.width_for_char(c))
|
||||
.fold(px(0.0), |a, x| a + x);
|
||||
let mut truncate_ix = 0;
|
||||
|
||||
match truncate_from {
|
||||
TruncateFrom::Start => {
|
||||
for (ix, c) in line.char_indices().rev() {
|
||||
if width + suffix_width < truncate_width {
|
||||
truncate_ix = ix;
|
||||
}
|
||||
|
||||
let char_width = self.width_for_char(c);
|
||||
width += char_width;
|
||||
|
||||
if width.floor() > truncate_width {
|
||||
return Some(truncate_ix);
|
||||
}
|
||||
}
|
||||
for (ix, c) in line.char_indices() {
|
||||
if width + suffix_width < truncate_width {
|
||||
truncate_ix = ix;
|
||||
}
|
||||
TruncateFrom::End => {
|
||||
for (ix, c) in line.char_indices() {
|
||||
if width + suffix_width < truncate_width {
|
||||
truncate_ix = ix;
|
||||
}
|
||||
|
||||
let char_width = self.width_for_char(c);
|
||||
width += char_width;
|
||||
let char_width = self.width_for_char(c);
|
||||
width += char_width;
|
||||
|
||||
if width.floor() > truncate_width {
|
||||
return Some(truncate_ix);
|
||||
}
|
||||
}
|
||||
if width.floor() > truncate_width {
|
||||
return Some(truncate_ix);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,23 +163,16 @@ impl LineWrapper {
|
||||
&mut self,
|
||||
line: SharedString,
|
||||
truncate_width: Pixels,
|
||||
truncation_affix: &str,
|
||||
truncation_suffix: &str,
|
||||
runs: &'a [TextRun],
|
||||
truncate_from: TruncateFrom,
|
||||
) -> (SharedString, Cow<'a, [TextRun]>) {
|
||||
if let Some(truncate_ix) =
|
||||
self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
|
||||
self.should_truncate_line(&line, truncate_width, truncation_suffix)
|
||||
{
|
||||
let result = match truncate_from {
|
||||
TruncateFrom::Start => {
|
||||
SharedString::from(format!("{truncation_affix}{}", &line[truncate_ix + 1..]))
|
||||
}
|
||||
TruncateFrom::End => {
|
||||
SharedString::from(format!("{}{truncation_affix}", &line[..truncate_ix]))
|
||||
}
|
||||
};
|
||||
let result =
|
||||
SharedString::from(format!("{}{}", &line[..truncate_ix], truncation_suffix));
|
||||
let mut runs = runs.to_vec();
|
||||
update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
|
||||
update_runs_after_truncation(&result, truncation_suffix, &mut runs);
|
||||
(result, Cow::Owned(runs))
|
||||
} else {
|
||||
(line, Cow::Borrowed(runs))
|
||||
@@ -282,35 +245,15 @@ impl LineWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_runs_after_truncation(
|
||||
result: &str,
|
||||
ellipsis: &str,
|
||||
runs: &mut Vec<TextRun>,
|
||||
truncate_from: TruncateFrom,
|
||||
) {
|
||||
fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
|
||||
let mut truncate_at = result.len() - ellipsis.len();
|
||||
match truncate_from {
|
||||
TruncateFrom::Start => {
|
||||
for (run_index, run) in runs.iter_mut().enumerate().rev() {
|
||||
if run.len <= truncate_at {
|
||||
truncate_at -= run.len;
|
||||
} else {
|
||||
run.len = truncate_at + ellipsis.len();
|
||||
runs.splice(..run_index, std::iter::empty());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
TruncateFrom::End => {
|
||||
for (run_index, run) in runs.iter_mut().enumerate() {
|
||||
if run.len <= truncate_at {
|
||||
truncate_at -= run.len;
|
||||
} else {
|
||||
run.len = truncate_at + ellipsis.len();
|
||||
runs.truncate(run_index + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (run_index, run) in runs.iter_mut().enumerate() {
|
||||
if run.len <= truncate_at {
|
||||
truncate_at -= run.len;
|
||||
} else {
|
||||
run.len = truncate_at + ellipsis.len();
|
||||
runs.truncate(run_index + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -560,7 +503,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_line_end() {
|
||||
fn test_truncate_line() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
fn perform_test(
|
||||
@@ -571,13 +514,8 @@ mod tests {
|
||||
) {
|
||||
let dummy_run_lens = vec![text.len()];
|
||||
let dummy_runs = generate_test_runs(&dummy_run_lens);
|
||||
let (result, dummy_runs) = wrapper.truncate_line(
|
||||
text.into(),
|
||||
px(220.),
|
||||
ellipsis,
|
||||
&dummy_runs,
|
||||
TruncateFrom::End,
|
||||
);
|
||||
let (result, dummy_runs) =
|
||||
wrapper.truncate_line(text.into(), px(220.), ellipsis, &dummy_runs);
|
||||
assert_eq!(result, expected);
|
||||
assert_eq!(dummy_runs.first().unwrap().len, result.len());
|
||||
}
|
||||
@@ -603,50 +541,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_line_start() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
fn perform_test(
|
||||
wrapper: &mut LineWrapper,
|
||||
text: &'static str,
|
||||
expected: &'static str,
|
||||
ellipsis: &str,
|
||||
) {
|
||||
let dummy_run_lens = vec![text.len()];
|
||||
let dummy_runs = generate_test_runs(&dummy_run_lens);
|
||||
let (result, dummy_runs) = wrapper.truncate_line(
|
||||
text.into(),
|
||||
px(220.),
|
||||
ellipsis,
|
||||
&dummy_runs,
|
||||
TruncateFrom::Start,
|
||||
);
|
||||
assert_eq!(result, expected);
|
||||
assert_eq!(dummy_runs.first().unwrap().len, result.len());
|
||||
}
|
||||
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"aaaa bbbb cccc ddddd eeee fff gg",
|
||||
"cccc ddddd eeee fff gg",
|
||||
"",
|
||||
);
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"aaaa bbbb cccc ddddd eeee fff gg",
|
||||
"…ccc ddddd eeee fff gg",
|
||||
"…",
|
||||
);
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"aaaa bbbb cccc ddddd eeee fff gg",
|
||||
"......dddd eeee fff gg",
|
||||
"......",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_multiple_runs_end() {
|
||||
fn test_truncate_multiple_runs() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
fn perform_test(
|
||||
@@ -659,7 +554,7 @@ mod tests {
|
||||
) {
|
||||
let dummy_runs = generate_test_runs(run_lens);
|
||||
let (result, dummy_runs) =
|
||||
wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs, TruncateFrom::End);
|
||||
wrapper.truncate_line(text.into(), line_width, "…", &dummy_runs);
|
||||
assert_eq!(result, expected);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
@@ -705,75 +600,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_truncate_multiple_runs_start() {
|
||||
let mut wrapper = build_wrapper();
|
||||
|
||||
#[track_caller]
|
||||
fn perform_test(
|
||||
wrapper: &mut LineWrapper,
|
||||
text: &'static str,
|
||||
expected: &str,
|
||||
run_lens: &[usize],
|
||||
result_run_len: &[usize],
|
||||
line_width: Pixels,
|
||||
) {
|
||||
let dummy_runs = generate_test_runs(run_lens);
|
||||
let (result, dummy_runs) = wrapper.truncate_line(
|
||||
text.into(),
|
||||
line_width,
|
||||
"…",
|
||||
&dummy_runs,
|
||||
TruncateFrom::Start,
|
||||
);
|
||||
assert_eq!(result, expected);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
}
|
||||
}
|
||||
// Case 0: Normal
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 12, ... }
|
||||
//
|
||||
// Truncate res: …ijkl (truncate_at = 9)
|
||||
// Run res: Run0 { string: …ijkl, len: 7, ... }
|
||||
perform_test(&mut wrapper, "abcdefghijkl", "…ijkl", &[12], &[7], px(50.));
|
||||
// Case 1: Drop some runs
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: …ghijkl (truncate_at = 7)
|
||||
// Runs res: Run0 { string: …gh, len: 5, ... }, Run1 { string: ijkl, len:
|
||||
// 4, ... }
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"abcdefghijkl",
|
||||
"…ghijkl",
|
||||
&[4, 4, 4],
|
||||
&[5, 4],
|
||||
px(70.),
|
||||
);
|
||||
// Case 2: Truncate at start of some run
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: abcdefgh… (truncate_at = 3)
|
||||
// Runs res: Run0 { string: …, len: 3, ... }, Run1 { string: efgh, len:
|
||||
// 4, ... }, Run2 { string: ijkl, len: 4, ... }
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"abcdefghijkl",
|
||||
"…efghijkl",
|
||||
&[4, 4, 4],
|
||||
&[3, 4, 4],
|
||||
px(90.),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_run_after_truncation_end() {
|
||||
fn test_update_run_after_truncation() {
|
||||
fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
|
||||
let mut dummy_runs = generate_test_runs(run_lens);
|
||||
update_runs_after_truncation(result, "…", &mut dummy_runs, TruncateFrom::End);
|
||||
update_runs_after_truncation(result, "…", &mut dummy_runs);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
}
|
||||
|
||||
@@ -876,9 +876,7 @@ pub struct Window {
|
||||
active: Rc<Cell<bool>>,
|
||||
hovered: Rc<Cell<bool>>,
|
||||
pub(crate) needs_present: Rc<Cell<bool>>,
|
||||
/// Tracks recent input event timestamps to determine if input is arriving at a high rate.
|
||||
/// Used to selectively enable VRR optimization only when input rate exceeds 60fps.
|
||||
pub(crate) input_rate_tracker: Rc<RefCell<InputRateTracker>>,
|
||||
pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
|
||||
last_input_modality: InputModality,
|
||||
pub(crate) refreshing: bool,
|
||||
pub(crate) activation_observers: SubscriberSet<(), AnyObserver>,
|
||||
@@ -899,51 +897,6 @@ struct ModifierState {
|
||||
saw_keystroke: bool,
|
||||
}
|
||||
|
||||
/// Tracks input event timestamps to determine if input is arriving at a high rate.
|
||||
/// Used for selective VRR (Variable Refresh Rate) optimization.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct InputRateTracker {
|
||||
timestamps: Vec<Instant>,
|
||||
window: Duration,
|
||||
inputs_per_second: u32,
|
||||
sustain_until: Instant,
|
||||
sustain_duration: Duration,
|
||||
}
|
||||
|
||||
impl Default for InputRateTracker {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timestamps: Vec::new(),
|
||||
window: Duration::from_millis(100),
|
||||
inputs_per_second: 60,
|
||||
sustain_until: Instant::now(),
|
||||
sustain_duration: Duration::from_secs(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputRateTracker {
|
||||
pub fn record_input(&mut self) {
|
||||
let now = Instant::now();
|
||||
self.timestamps.push(now);
|
||||
self.prune_old_timestamps(now);
|
||||
|
||||
let min_events = self.inputs_per_second as u128 * self.window.as_millis() / 1000;
|
||||
if self.timestamps.len() as u128 >= min_events {
|
||||
self.sustain_until = now + self.sustain_duration;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_high_rate(&self) -> bool {
|
||||
Instant::now() < self.sustain_until
|
||||
}
|
||||
|
||||
fn prune_old_timestamps(&mut self, now: Instant) {
|
||||
self.timestamps
|
||||
.retain(|&t| now.duration_since(t) <= self.window);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum DrawPhase {
|
||||
None,
|
||||
@@ -1094,7 +1047,7 @@ impl Window {
|
||||
let hovered = Rc::new(Cell::new(platform_window.is_hovered()));
|
||||
let needs_present = Rc::new(Cell::new(false));
|
||||
let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
|
||||
let input_rate_tracker = Rc::new(RefCell::new(InputRateTracker::default()));
|
||||
let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
|
||||
|
||||
platform_window
|
||||
.request_decorations(window_decorations.unwrap_or(WindowDecorations::Server));
|
||||
@@ -1122,7 +1075,7 @@ impl Window {
|
||||
let active = active.clone();
|
||||
let needs_present = needs_present.clone();
|
||||
let next_frame_callbacks = next_frame_callbacks.clone();
|
||||
let input_rate_tracker = input_rate_tracker.clone();
|
||||
let last_input_timestamp = last_input_timestamp.clone();
|
||||
move |request_frame_options| {
|
||||
let next_frame_callbacks = next_frame_callbacks.take();
|
||||
if !next_frame_callbacks.is_empty() {
|
||||
@@ -1135,12 +1088,12 @@ impl Window {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
// Keep presenting if input was recently arriving at a high rate (>= 60fps).
|
||||
// Once high-rate input is detected, we sustain presentation for 1 second
|
||||
// to prevent display underclocking during active input.
|
||||
// Keep presenting the current scene for 1 extra second since the
|
||||
// last input to prevent the display from underclocking the refresh rate.
|
||||
let needs_present = request_frame_options.require_presentation
|
||||
|| needs_present.get()
|
||||
|| (active.get() && input_rate_tracker.borrow_mut().is_high_rate());
|
||||
|| (active.get()
|
||||
&& last_input_timestamp.get().elapsed() < Duration::from_secs(1));
|
||||
|
||||
if invalidator.is_dirty() || request_frame_options.force_render {
|
||||
measure("frame duration", || {
|
||||
@@ -1148,6 +1101,7 @@ impl Window {
|
||||
.update(&mut cx, |_, window, cx| {
|
||||
let arena_clear_needed = window.draw(cx);
|
||||
window.present();
|
||||
// drop the arena elements after present to reduce latency
|
||||
arena_clear_needed.clear();
|
||||
})
|
||||
.log_err();
|
||||
@@ -1345,7 +1299,7 @@ impl Window {
|
||||
active,
|
||||
hovered,
|
||||
needs_present,
|
||||
input_rate_tracker,
|
||||
last_input_timestamp,
|
||||
last_input_modality: InputModality::Mouse,
|
||||
refreshing: false,
|
||||
activation_observers: SubscriberSet::new(),
|
||||
@@ -3737,6 +3691,8 @@ impl Window {
|
||||
/// Dispatch a mouse or keyboard event on the window.
|
||||
#[profiling::function]
|
||||
pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult {
|
||||
self.last_input_timestamp.set(Instant::now());
|
||||
|
||||
// Track whether this input was keyboard-based for focus-visible styling
|
||||
self.last_input_modality = match &event {
|
||||
PlatformInput::KeyDown(_) | PlatformInput::ModifiersChanged(_) => {
|
||||
@@ -3837,10 +3793,6 @@ impl Window {
|
||||
self.dispatch_key_event(any_key_event, cx);
|
||||
}
|
||||
|
||||
if self.invalidator.is_dirty() {
|
||||
self.input_rate_tracker.borrow_mut().record_input();
|
||||
}
|
||||
|
||||
DispatchEventResult {
|
||||
propagate: cx.propagate_event,
|
||||
default_prevented: self.default_prevented,
|
||||
@@ -4909,11 +4861,11 @@ impl<V: 'static + Render> WindowHandle<V> {
|
||||
where
|
||||
C: AppContext,
|
||||
{
|
||||
cx.update_window(self.any_handle, |root_view, _, _| {
|
||||
crate::Flatten::flatten(cx.update_window(self.any_handle, |root_view, _, _| {
|
||||
root_view
|
||||
.downcast::<V>()
|
||||
.map_err(|_| anyhow!("the type of the window's root view has changed"))
|
||||
})?
|
||||
}))
|
||||
}
|
||||
|
||||
/// Updates the root view of this window.
|
||||
|
||||
@@ -11,7 +11,7 @@ use gpui::{
|
||||
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled, Task, WeakEntity,
|
||||
Window, canvas, div, fill, img, opaque_grey, point, size,
|
||||
};
|
||||
use language::File as _;
|
||||
use language::{DiskState, File as _};
|
||||
use persistence::IMAGE_VIEWER;
|
||||
use project::{ImageItem, Project, ProjectPath, image_store::ImageItemEvent};
|
||||
use settings::Settings;
|
||||
@@ -195,7 +195,7 @@ impl Item for ImageView {
|
||||
}
|
||||
|
||||
fn has_deleted_file(&self, cx: &App) -> bool {
|
||||
self.image_item.read(cx).file.disk_state().is_deleted()
|
||||
self.image_item.read(cx).file.disk_state() == DiskState::Deleted
|
||||
}
|
||||
fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind {
|
||||
workspace::item::ItemBufferKind::Singleton
|
||||
|
||||
@@ -427,9 +427,6 @@ pub enum DiskState {
|
||||
Present { mtime: MTime },
|
||||
/// Deleted file that was previously present.
|
||||
Deleted,
|
||||
/// An old version of a file that was previously present
|
||||
/// usually from a version control system. e.g. A git blob
|
||||
Historic { was_deleted: bool },
|
||||
}
|
||||
|
||||
impl DiskState {
|
||||
@@ -439,7 +436,6 @@ impl DiskState {
|
||||
DiskState::New => None,
|
||||
DiskState::Present { mtime } => Some(mtime),
|
||||
DiskState::Deleted => None,
|
||||
DiskState::Historic { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,16 +444,6 @@ impl DiskState {
|
||||
DiskState::New => false,
|
||||
DiskState::Present { .. } => true,
|
||||
DiskState::Deleted => false,
|
||||
DiskState::Historic { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this state represents a deleted file.
|
||||
pub fn is_deleted(&self) -> bool {
|
||||
match self {
|
||||
DiskState::Deleted => true,
|
||||
DiskState::Historic { was_deleted } => *was_deleted,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1504,23 +1490,19 @@ impl Buffer {
|
||||
let (tx, rx) = futures::channel::oneshot::channel();
|
||||
let prev_version = self.text.version();
|
||||
self.reload_task = Some(cx.spawn(async move |this, cx| {
|
||||
let Some((new_mtime, load_bytes_task, encoding)) = this.update(cx, |this, cx| {
|
||||
let Some((new_mtime, new_text)) = this.update(cx, |this, cx| {
|
||||
let file = this.file.as_ref()?.as_local()?;
|
||||
Some((
|
||||
file.disk_state().mtime(),
|
||||
file.load_bytes(cx),
|
||||
this.encoding,
|
||||
))
|
||||
|
||||
Some((file.disk_state().mtime(), file.load(cx)))
|
||||
})?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let bytes = load_bytes_task.await?;
|
||||
let (cow, _encoding_used, _has_errors) = encoding.decode(&bytes);
|
||||
let new_text = cow.into_owned();
|
||||
|
||||
let diff = this.update(cx, |this, cx| this.diff(new_text, cx))?.await;
|
||||
let new_text = new_text.await?;
|
||||
let diff = this
|
||||
.update(cx, |this, cx| this.diff(new_text.clone(), cx))?
|
||||
.await;
|
||||
this.update(cx, |this, cx| {
|
||||
if this.version() == diff.base_version {
|
||||
this.finalize_last_transaction();
|
||||
@@ -2288,7 +2270,6 @@ impl Buffer {
|
||||
None => true,
|
||||
},
|
||||
DiskState::Deleted => false,
|
||||
DiskState::Historic { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user