Detailed changes
@@ -19,8 +19,6 @@ rustflags = [
"windows_slim_errors", # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
"-C",
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
- "-C",
- "link-arg=-fuse-ld=lld",
]
[env]
@@ -25,6 +25,8 @@ third-party = [
{ name = "reqwest", version = "0.11.27" },
# build of remote_server should not include scap / its x11 dependency
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
+ # build of remote_server should not need to include on libalsa through rodio
+ { name = "rodio", git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"},
]
[final-excludes]
@@ -32,7 +34,6 @@ workspace-members = [
"zed_extension_api",
# exclude all extensions
- "zed_emmet",
"zed_glsl",
"zed_html",
"zed_proto",
@@ -40,5 +41,4 @@ workspace-members = [
"slash_commands_example",
"zed_snippets",
"zed_test_extension",
- "zed_toml",
]
@@ -1,2 +1,5 @@
# Prevent GitHub from displaying comments within JSON files as errors.
*.json linguist-language=JSON-with-Comments
+
+# Ensure the WSL script always has LF line endings, even on Windows
+crates/zed/resources/windows/zed.sh text eol=lf
@@ -14,7 +14,7 @@ body:
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
- Any code must be sufficient to reproduce (include context!)
- - Code must as text, not just as a screenshot.
+ - Include code as text, not just as a screenshot.
- Issues with insufficient detail may be summarily closed.
-->
@@ -19,14 +19,27 @@ self-hosted-runner:
- namespace-profile-16x32-ubuntu-2004-arm
- namespace-profile-32x64-ubuntu-2004-arm
# Namespace Ubuntu 22.04 (Everything else)
- - namespace-profile-2x4-ubuntu-2204
- namespace-profile-4x8-ubuntu-2204
- namespace-profile-8x16-ubuntu-2204
- namespace-profile-16x32-ubuntu-2204
- namespace-profile-32x64-ubuntu-2204
+ # Namespace Ubuntu 24.04 (like ubuntu-latest)
+ - namespace-profile-2x4-ubuntu-2404
# Namespace Limited Preview
- namespace-profile-8x16-ubuntu-2004-arm-m4
- namespace-profile-8x32-ubuntu-2004-arm-m4
# Self Hosted Runners
- self-mini-macos
- self-32vcpu-windows-2022
+
+# Disable shellcheck because it doesn't like powershell
+# This should have been triggered with initial rollout of actionlint
+# but https://github.com/zed-industries/zed/pull/36693
+# somehow caused actionlint to actually check those windows jobs
+# where previously they were being skipped. Likely caused by an
+# unknown bug in actionlint where parsing of `runs-on: [ ]`
+# breaks something else. (yuck)
+paths:
+ .github/workflows/{ci,release_nightly}.yml:
+ ignore:
+ - "shellcheck"
@@ -20,168 +20,8 @@ runs:
with:
node-version: "18"
- - name: Configure crash dumps
- shell: powershell
- run: |
- # Record the start time for this CI run
- $runStartTime = Get-Date
- $runStartTimeStr = $runStartTime.ToString("yyyy-MM-dd HH:mm:ss")
- Write-Host "CI run started at: $runStartTimeStr"
-
- # Save the timestamp for later use
- echo "CI_RUN_START_TIME=$($runStartTime.Ticks)" >> $env:GITHUB_ENV
-
- # Create crash dump directory in workspace (non-persistent)
- $dumpPath = "$env:GITHUB_WORKSPACE\crash_dumps"
- New-Item -ItemType Directory -Force -Path $dumpPath | Out-Null
-
- Write-Host "Setting up crash dump detection..."
- Write-Host "Workspace dump path: $dumpPath"
-
- # Note: We're NOT modifying registry on stateful runners
- # Instead, we'll check default Windows crash locations after tests
-
- name: Run tests
shell: powershell
working-directory: ${{ inputs.working-directory }}
run: |
- $env:RUST_BACKTRACE = "full"
-
- # Enable Windows debugging features
- $env:_NT_SYMBOL_PATH = "srv*https://msdl.microsoft.com/download/symbols"
-
- # .NET crash dump environment variables (ephemeral)
- $env:COMPlus_DbgEnableMiniDump = "1"
- $env:COMPlus_DbgMiniDumpType = "4"
- $env:COMPlus_CreateDumpDiagnostics = "1"
-
cargo nextest run --workspace --no-fail-fast
- continue-on-error: true
-
- - name: Analyze crash dumps
- if: always()
- shell: powershell
- run: |
- Write-Host "Checking for crash dumps..."
-
- # Get the CI run start time from the environment
- $runStartTime = [DateTime]::new([long]$env:CI_RUN_START_TIME)
- Write-Host "Only analyzing dumps created after: $($runStartTime.ToString('yyyy-MM-dd HH:mm:ss'))"
-
- # Check all possible crash dump locations
- $searchPaths = @(
- "$env:GITHUB_WORKSPACE\crash_dumps",
- "$env:LOCALAPPDATA\CrashDumps",
- "$env:TEMP",
- "$env:GITHUB_WORKSPACE",
- "$env:USERPROFILE\AppData\Local\CrashDumps",
- "C:\Windows\System32\config\systemprofile\AppData\Local\CrashDumps"
- )
-
- $dumps = @()
- foreach ($path in $searchPaths) {
- if (Test-Path $path) {
- Write-Host "Searching in: $path"
- $found = Get-ChildItem "$path\*.dmp" -ErrorAction SilentlyContinue | Where-Object {
- $_.CreationTime -gt $runStartTime
- }
- if ($found) {
- $dumps += $found
- Write-Host " Found $($found.Count) dump(s) from this CI run"
- }
- }
- }
-
- if ($dumps) {
- Write-Host "Found $($dumps.Count) crash dump(s)"
-
- # Install debugging tools if not present
- $cdbPath = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe"
- if (-not (Test-Path $cdbPath)) {
- Write-Host "Installing Windows Debugging Tools..."
- $url = "https://go.microsoft.com/fwlink/?linkid=2237387"
- Invoke-WebRequest -Uri $url -OutFile winsdksetup.exe
- Start-Process -Wait winsdksetup.exe -ArgumentList "/features OptionId.WindowsDesktopDebuggers /quiet"
- }
-
- foreach ($dump in $dumps) {
- Write-Host "`n=================================="
- Write-Host "Analyzing crash dump: $($dump.Name)"
- Write-Host "Size: $([math]::Round($dump.Length / 1MB, 2)) MB"
- Write-Host "Time: $($dump.CreationTime)"
- Write-Host "=================================="
-
- # Set symbol path
- $env:_NT_SYMBOL_PATH = "srv*C:\symbols*https://msdl.microsoft.com/download/symbols"
-
- # Run analysis
- $analysisOutput = & $cdbPath -z $dump.FullName -c "!analyze -v; ~*k; lm; q" 2>&1 | Out-String
-
- # Extract key information
- if ($analysisOutput -match "ExceptionCode:\s*([\w]+)") {
- Write-Host "Exception Code: $($Matches[1])"
- if ($Matches[1] -eq "c0000005") {
- Write-Host "Exception Type: ACCESS VIOLATION"
- }
- }
-
- if ($analysisOutput -match "EXCEPTION_RECORD:\s*(.+)") {
- Write-Host "Exception Record: $($Matches[1])"
- }
-
- if ($analysisOutput -match "FAULTING_IP:\s*\n(.+)") {
- Write-Host "Faulting Instruction: $($Matches[1])"
- }
-
- # Save full analysis
- $analysisFile = "$($dump.FullName).analysis.txt"
- $analysisOutput | Out-File -FilePath $analysisFile
- Write-Host "`nFull analysis saved to: $analysisFile"
-
- # Print stack trace section
- Write-Host "`n--- Stack Trace Preview ---"
- $stackSection = $analysisOutput -split "STACK_TEXT:" | Select-Object -Last 1
- $stackLines = $stackSection -split "`n" | Select-Object -First 20
- $stackLines | ForEach-Object { Write-Host $_ }
- Write-Host "--- End Stack Trace Preview ---"
- }
-
- Write-Host "`n⚠️ Crash dumps detected! Download the 'crash-dumps' artifact for detailed analysis."
-
- # Copy dumps to workspace for artifact upload
- $artifactPath = "$env:GITHUB_WORKSPACE\crash_dumps_collected"
- New-Item -ItemType Directory -Force -Path $artifactPath | Out-Null
-
- foreach ($dump in $dumps) {
- $destName = "$($dump.Directory.Name)_$($dump.Name)"
- Copy-Item $dump.FullName -Destination "$artifactPath\$destName"
- if (Test-Path "$($dump.FullName).analysis.txt") {
- Copy-Item "$($dump.FullName).analysis.txt" -Destination "$artifactPath\$destName.analysis.txt"
- }
- }
-
- Write-Host "Copied $($dumps.Count) dump(s) to artifact directory"
- } else {
- Write-Host "No crash dumps from this CI run found"
- }
-
- - name: Upload crash dumps
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: crash-dumps-${{ github.run_id }}-${{ github.run_attempt }}
- path: |
- crash_dumps_collected/*.dmp
- crash_dumps_collected/*.txt
- if-no-files-found: ignore
- retention-days: 7
-
- - name: Check test results
- shell: powershell
- working-directory: ${{ inputs.working-directory }}
- run: |
- # Re-check test results to fail the job if tests failed
- if ($LASTEXITCODE -ne 0) {
- Write-Host "Tests failed with exit code: $LASTEXITCODE"
- exit $LASTEXITCODE
- }
@@ -8,7 +8,7 @@ on:
jobs:
update-collab-staging-tag:
if: github.repository_owner == 'zed-industries'
- runs-on: ubuntu-latest
+ runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -37,7 +37,7 @@ jobs:
run_nix: ${{ steps.filter.outputs.run_nix }}
run_actionlint: ${{ steps.filter.outputs.run_actionlint }}
runs-on:
- - ubuntu-latest
+ - namespace-profile-2x4-ubuntu-2404
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -81,6 +81,7 @@ jobs:
echo "run_license=false" >> "$GITHUB_OUTPUT"
echo "$CHANGED_FILES" | grep -qP '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' && \
+ echo "$GITHUB_REF_NAME" | grep -qvP '^v[0-9]+\.[0-9]+\.[0-9x](-pre)?$' && \
echo "run_nix=true" >> "$GITHUB_OUTPUT" || \
echo "run_nix=false" >> "$GITHUB_OUTPUT"
@@ -237,7 +238,7 @@ jobs:
uses: ./.github/actions/build_docs
actionlint:
- runs-on: ubuntu-latest
+ runs-on: namespace-profile-2x4-ubuntu-2404
if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true'
needs: [job_spec]
steps:
@@ -418,7 +419,7 @@ jobs:
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
- runs-on: [self-hosted, Windows, X64]
+ runs-on: [self-32vcpu-windows-2022]
steps:
- name: Environment Setup
run: |
@@ -458,7 +459,7 @@ jobs:
tests_pass:
name: Tests Pass
- runs-on: ubuntu-latest
+ runs-on: namespace-profile-2x4-ubuntu-2404
needs:
- job_spec
- style
@@ -784,7 +785,7 @@ jobs:
bundle-windows-x64:
timeout-minutes: 120
name: Create a Windows installer
- runs-on: [self-hosted, Windows, X64]
+ runs-on: [self-32vcpu-windows-2022]
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
# if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
needs: [windows_tests]
@@ -12,7 +12,7 @@ on:
jobs:
danger:
if: github.repository_owner == 'zed-industries'
- runs-on: ubuntu-latest
+ runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -59,7 +59,7 @@ jobs:
timeout-minutes: 60
name: Run tests on Windows
if: github.repository_owner == 'zed-industries'
- runs-on: [self-hosted, Windows, X64]
+ runs-on: [self-32vcpu-windows-2022]
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -206,9 +206,6 @@ jobs:
runs-on: github-8vcpu-ubuntu-2404
needs: tests
name: Build Zed on FreeBSD
- # env:
- # MYTOKEN : ${{ secrets.MYTOKEN }}
- # MYTOKEN2: "value2"
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
@@ -243,7 +240,6 @@ jobs:
bundle-nix:
name: Build and cache Nix package
- if: false
needs: tests
secrets: inherit
uses: ./.github/workflows/nix.yml
@@ -252,7 +248,7 @@ jobs:
timeout-minutes: 60
name: Create a Windows installer
if: github.repository_owner == 'zed-industries'
- runs-on: [self-hosted, Windows, X64]
+ runs-on: [self-32vcpu-windows-2022]
needs: windows-tests
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
@@ -294,7 +290,7 @@ jobs:
update-nightly-tag:
name: Update nightly tag
if: github.repository_owner == 'zed-industries'
- runs-on: ubuntu-latest
+ runs-on: namespace-profile-2x4-ubuntu-2404
needs:
- bundle-mac
- bundle-linux-x86
@@ -12,7 +12,7 @@ jobs:
shellcheck:
name: "ShellCheck Scripts"
if: github.repository_owner == 'zed-industries'
- runs-on: ubuntu-latest
+ runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -12,6 +12,19 @@
- Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead
* When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
+* When creating new crates, prefer specifying the library root path in `Cargo.toml` using `[lib] path = "...rs"` instead of the default `lib.rs`, to maintain consistent and descriptive naming (e.g., `gpui.rs` or `main.rs`).
+* Avoid creative additions unless explicitly requested
+* Use full words for variable names (no abbreviations like "q" for "queue")
+* Use variable shadowing to scope clones in async contexts for clarity, minimizing the lifetime of borrowed references.
+ Example:
+ ```rust
+ executor.spawn({
+ let task_ran = task_ran.clone();
+ async move {
+ *task_ran.borrow_mut() = true;
+ }
+ });
+ ```
# GPUI
@@ -27,6 +27,22 @@ By effectively engaging with the Zed team and community early in your process, w
We plan to set aside time each week to pair program with contributors on promising pull requests in Zed. This will be an experiment. We tend to prefer pairing over async code review on our team, and we'd like to see how well it works in an open source setting. If we're finding it difficult to get on the same page with async review, we may ask you to pair with us if you're open to it. The closer a contribution is to the goals outlined in our roadmap, the more likely we'll be to spend time pairing on it.
+## Mandatory PR contents
+
+Please ensure the PR contains
+
+- Before & after screenshots, if there are visual adjustments introduced.
+
+Examples of visual adjustments: tree-sitter query updates, UI changes, etc.
+
+- A disclosure of the AI assistance usage, if any was used.
+
+Any kind of AI assistance must be disclosed in the PR, along with the extent to which AI assistance was used (e.g. docs only vs. code generation).
+
+If the PR responses are being generated by an AI, disclose that as well.
+
+As a small exception, trivial tab-completion doesn't need to be disclosed, as long as it's limited to single keywords or short phrases.
+
## Tips to improve the chances of your PR getting reviewed and merged
- Discuss your plans ahead of time with the team
@@ -49,6 +65,8 @@ If you would like to add a new icon to the Zed icon theme, [open a Discussion](h
## Bird's-eye view of Zed
+We suggest you keep the [zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
+
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
- [`gpui`](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation.**
@@ -7,27 +7,31 @@ name = "acp_thread"
version = "0.1.0"
dependencies = [
"action_log",
- "agent",
"agent-client-protocol",
+ "agent_settings",
"anyhow",
"buffer_diff",
"collections",
"editor",
"env_logger 0.11.8",
+ "file_icons",
"futures 0.3.31",
"gpui",
"indoc",
"itertools 0.14.0",
"language",
+ "language_model",
"markdown",
"parking_lot",
+ "portable-pty",
"project",
"prompt_store",
- "rand 0.8.5",
+ "rand 0.9.1",
"serde",
"serde_json",
"settings",
"smol",
+ "task",
"tempfile",
"terminal",
"ui",
@@ -35,6 +39,27 @@ dependencies = [
"util",
"uuid",
"watch",
+ "which 6.0.3",
+ "workspace-hack",
+]
+
+[[package]]
+name = "acp_tools"
+version = "0.1.0"
+dependencies = [
+ "agent-client-protocol",
+ "collections",
+ "gpui",
+ "language",
+ "markdown",
+ "project",
+ "serde",
+ "serde_json",
+ "settings",
+ "theme",
+ "ui",
+ "util",
+ "workspace",
"workspace-hack",
]
@@ -54,7 +79,7 @@ dependencies = [
"log",
"pretty_assertions",
"project",
- "rand 0.8.5",
+ "rand 0.9.1",
"serde_json",
"settings",
"text",
@@ -129,7 +154,6 @@ dependencies = [
"component",
"context_server",
"convert_case 0.8.0",
- "feature_flags",
"fs",
"futures 0.3.31",
"git",
@@ -148,7 +172,7 @@ dependencies = [
"pretty_assertions",
"project",
"prompt_store",
- "rand 0.8.5",
+ "rand 0.9.1",
"ref-cast",
"rope",
"schemars",
@@ -166,16 +190,18 @@ dependencies = [
"uuid",
"workspace",
"workspace-hack",
+ "zed_env_vars",
"zstd",
]
[[package]]
name = "agent-client-protocol"
-version = "0.0.23"
+version = "0.2.0-alpha.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fad72b7b8ee4331b3a4c8d43c107e982a4725564b4ee658ae5c4e79d2b486e8"
+checksum = "08539e8d6b2ccca6cd00afdd42211698f7677adef09108a09414c11f1f45fdaf"
dependencies = [
"anyhow",
+ "async-broadcast",
"futures 0.3.31",
"log",
"parking_lot",
@@ -190,10 +216,12 @@ version = "0.1.0"
dependencies = [
"acp_thread",
"action_log",
+ "agent",
"agent-client-protocol",
"agent_servers",
"agent_settings",
"anyhow",
+ "assistant_context",
"assistant_tool",
"assistant_tools",
"chrono",
@@ -203,10 +231,12 @@ dependencies = [
"collections",
"context_server",
"ctor",
+ "db",
"editor",
"env_logger 0.11.8",
"fs",
"futures 0.3.31",
+ "git",
"gpui",
"gpui_tokio",
"handlebars 4.5.0",
@@ -220,8 +250,8 @@ dependencies = [
"log",
"lsp",
"open",
+ "parking_lot",
"paths",
- "portable-pty",
"pretty_assertions",
"project",
"prompt_store",
@@ -232,11 +262,14 @@ dependencies = [
"serde_json",
"settings",
"smol",
+ "sqlez",
"task",
+ "telemetry",
"tempfile",
"terminal",
"text",
"theme",
+ "thiserror 2.0.12",
"tree-sitter-rust",
"ui",
"unindent",
@@ -244,10 +277,11 @@ dependencies = [
"uuid",
"watch",
"web_search",
- "which 6.0.3",
"workspace-hack",
"worktree",
+ "zed_env_vars",
"zlog",
+ "zstd",
]
[[package]]
@@ -255,36 +289,37 @@ name = "agent_servers"
version = "0.1.0"
dependencies = [
"acp_thread",
+ "acp_tools",
+ "action_log",
"agent-client-protocol",
- "agentic-coding-protocol",
+ "agent_settings",
"anyhow",
+ "client",
"collections",
- "context_server",
"env_logger 0.11.8",
+ "fs",
"futures 0.3.31",
"gpui",
+ "gpui_tokio",
"indoc",
- "itertools 0.14.0",
"language",
+ "language_model",
+ "language_models",
"libc",
"log",
"nix 0.29.0",
- "paths",
"project",
- "rand 0.8.5",
- "schemars",
+ "reqwest_client",
"serde",
"serde_json",
"settings",
"smol",
- "strum 0.27.1",
+ "task",
"tempfile",
"thiserror 2.0.12",
"ui",
"util",
- "uuid",
"watch",
- "which 6.0.3",
"workspace-hack",
]
@@ -320,6 +355,7 @@ dependencies = [
"agent_settings",
"ai_onboarding",
"anyhow",
+ "arrayvec",
"assistant_context",
"assistant_slash_command",
"assistant_slash_commands",
@@ -346,7 +382,6 @@ dependencies = [
"gpui",
"html_to_markdown",
"http_client",
- "indexed_docs",
"indoc",
"inventory",
"itertools 0.14.0",
@@ -365,11 +400,12 @@ dependencies = [
"parking_lot",
"paths",
"picker",
+ "postage",
"pretty_assertions",
"project",
"prompt_store",
"proto",
- "rand 0.8.5",
+ "rand 0.9.1",
"release_channel",
"rope",
"rules_library",
@@ -379,6 +415,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"settings",
+ "shlex",
"smol",
"streaming_diff",
"task",
@@ -404,24 +441,6 @@ dependencies = [
"zed_actions",
]
-[[package]]
-name = "agentic-coding-protocol"
-version = "0.0.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
-dependencies = [
- "anyhow",
- "chrono",
- "derive_more 2.0.1",
- "futures 0.3.31",
- "log",
- "parking_lot",
- "schemars",
- "semver",
- "serde",
- "serde_json",
-]
-
[[package]]
name = "ahash"
version = "0.7.8"
@@ -464,6 +483,7 @@ dependencies = [
"client",
"cloud_llm_client",
"component",
+ "feature_flags",
"gpui",
"language_model",
"serde",
@@ -488,7 +508,7 @@ dependencies = [
"parking_lot",
"piper",
"polling",
- "regex-automata 0.4.9",
+ "regex-automata",
"rustix-openpty",
"serde",
"signal-hook",
@@ -812,7 +832,7 @@ dependencies = [
"project",
"prompt_store",
"proto",
- "rand 0.8.5",
+ "rand 0.9.1",
"regex",
"rpc",
"serde",
@@ -828,6 +848,7 @@ dependencies = [
"uuid",
"workspace",
"workspace-hack",
+ "zed_env_vars",
]
[[package]]
@@ -837,7 +858,7 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"extension",
"futures 0.3.31",
"gpui",
@@ -871,7 +892,6 @@ dependencies = [
"gpui",
"html_to_markdown",
"http_client",
- "indexed_docs",
"language",
"pretty_assertions",
"project",
@@ -901,7 +921,7 @@ dependencies = [
"clock",
"collections",
"ctor",
- "derive_more 0.99.19",
+ "derive_more",
"gpui",
"icons",
"indoc",
@@ -911,7 +931,7 @@ dependencies = [
"parking_lot",
"pretty_assertions",
"project",
- "rand 0.8.5",
+ "rand 0.9.1",
"regex",
"serde",
"serde_json",
@@ -938,7 +958,7 @@ dependencies = [
"cloud_llm_client",
"collections",
"component",
- "derive_more 0.99.19",
+ "derive_more",
"diffy",
"editor",
"feature_flags",
@@ -963,7 +983,7 @@ dependencies = [
"pretty_assertions",
"project",
"prompt_store",
- "rand 0.8.5",
+ "rand 0.9.1",
"regex",
"reqwest_client",
"rust-embed",
@@ -1261,26 +1281,6 @@ dependencies = [
"syn 2.0.101",
]
-[[package]]
-name = "async-stripe"
-version = "0.40.0"
-source = "git+https://github.com/zed-industries/async-stripe?rev=3672dd4efb7181aa597bf580bf5a2f5d23db6735#3672dd4efb7181aa597bf580bf5a2f5d23db6735"
-dependencies = [
- "chrono",
- "futures-util",
- "http-types",
- "hyper 0.14.32",
- "hyper-rustls 0.24.2",
- "serde",
- "serde_json",
- "serde_path_to_error",
- "serde_qs 0.10.1",
- "smart-default 0.6.0",
- "smol_str 0.1.24",
- "thiserror 1.0.69",
- "tokio",
-]
-
[[package]]
name = "async-tar"
version = "0.5.0"
@@ -1303,9 +1303,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
-version = "0.1.88"
+version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
@@ -1383,11 +1383,18 @@ name = "audio"
version = "0.1.0"
dependencies = [
"anyhow",
+ "async-tar",
"collections",
- "derive_more 0.99.19",
+ "crossbeam",
"gpui",
+ "libwebrtc",
+ "log",
"parking_lot",
"rodio",
+ "schemars",
+ "serde",
+ "settings",
+ "smol",
"util",
"workspace-hack",
]
@@ -2082,12 +2089,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
-[[package]]
-name = "base64"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
-
[[package]]
name = "base64"
version = "0.21.7"
@@ -2294,7 +2295,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.6.0"
-source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
+source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"ash",
"ash-window",
@@ -2327,7 +2328,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
-source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
+source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"proc-macro2",
"quote",
@@ -2337,7 +2338,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.2.0"
-source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
+source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -2354,19 +2355,6 @@ dependencies = [
"digest",
]
-[[package]]
-name = "blake3"
-version = "1.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
-dependencies = [
- "arrayref",
- "arrayvec",
- "cc",
- "cfg-if",
- "constant_time_eq 0.3.1",
-]
-
[[package]]
name = "block"
version = "0.1.6"
@@ -2464,7 +2452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
- "regex-automata 0.4.9",
+ "regex-automata",
"serde",
]
@@ -2481,7 +2469,7 @@ dependencies = [
"language",
"log",
"pretty_assertions",
- "rand 0.8.5",
+ "rand 0.9.1",
"rope",
"serde_json",
"sum_tree",
@@ -2627,6 +2615,7 @@ dependencies = [
"audio",
"client",
"collections",
+ "feature_flags",
"fs",
"futures 0.3.31",
"gpui",
@@ -2902,11 +2891,9 @@ dependencies = [
"language",
"log",
"postage",
- "rand 0.8.5",
"release_channel",
"rpc",
"settings",
- "sum_tree",
"text",
"time",
"util",
@@ -3073,10 +3060,9 @@ dependencies = [
"clock",
"cloud_api_client",
"cloud_llm_client",
- "cocoa 0.26.0",
"collections",
"credentials_provider",
- "derive_more 0.99.19",
+ "derive_more",
"feature_flags",
"fs",
"futures 0.3.31",
@@ -3086,10 +3072,11 @@ dependencies = [
"http_client_tls",
"httparse",
"log",
+ "objc2-foundation",
"parking_lot",
"paths",
"postage",
- "rand 0.8.5",
+ "rand 0.9.1",
"regex",
"release_channel",
"rpc",
@@ -3097,6 +3084,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
+ "serde_urlencoded",
"settings",
"sha2",
"smol",
@@ -3280,7 +3268,6 @@ dependencies = [
"anyhow",
"assistant_context",
"assistant_slash_command",
- "async-stripe",
"async-trait",
"async-tungstenite",
"audio",
@@ -3296,7 +3283,6 @@ dependencies = [
"chrono",
"client",
"clock",
- "cloud_llm_client",
"collab_ui",
"collections",
"command_palette_hooks",
@@ -3307,7 +3293,6 @@ dependencies = [
"dap_adapters",
"dashmap 6.1.0",
"debugger_ui",
- "derive_more 0.99.19",
"editor",
"envy",
"extension",
@@ -3323,7 +3308,6 @@ dependencies = [
"http_client",
"hyper 0.14.32",
"indoc",
- "jsonwebtoken",
"language",
"language_model",
"livekit_api",
@@ -3341,7 +3325,7 @@ dependencies = [
"prometheus",
"prompt_store",
"prost 0.9.0",
- "rand 0.8.5",
+ "rand 0.9.1",
"recent_projects",
"release_channel",
"remote",
@@ -3369,7 +3353,6 @@ dependencies = [
"telemetry_events",
"text",
"theme",
- "thiserror 2.0.12",
"time",
"tokio",
"toml 0.8.20",
@@ -3398,12 +3381,10 @@ dependencies = [
"collections",
"db",
"editor",
- "emojis",
"futures 0.3.31",
"fuzzy",
"gpui",
"http_client",
- "language",
"log",
"menu",
"notifications",
@@ -3411,7 +3392,6 @@ dependencies = [
"pretty_assertions",
"project",
"release_channel",
- "rich_text",
"rpc",
"schemars",
"serde",
@@ -3512,7 +3492,7 @@ name = "command_palette_hooks"
version = "0.1.0"
dependencies = [
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"gpui",
"workspace-hack",
]
@@ -3522,6 +3502,7 @@ name = "component"
version = "0.1.0"
dependencies = [
"collections",
+ "documented",
"gpui",
"inventory",
"parking_lot",
@@ -3584,12 +3565,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
-[[package]]
-name = "constant_time_eq"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
-
[[package]]
name = "context_server"
version = "0.1.0"
@@ -3871,7 +3846,7 @@ dependencies = [
"rustc-hash 1.1.0",
"rustybuzz 0.14.1",
"self_cell",
- "smol_str 0.2.2",
+ "smol_str",
"swash",
"sys-locale",
"ttf-parser 0.21.1",
@@ -3893,7 +3868,7 @@ dependencies = [
"jni",
"js-sys",
"libc",
- "mach2",
+ "mach2 0.4.2",
"ndk",
"ndk-context",
"num-derive",
@@ -4043,7 +4018,7 @@ checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3"
dependencies = [
"cfg-if",
"libc",
- "mach2",
+ "mach2 0.4.2",
]
[[package]]
@@ -4055,7 +4030,7 @@ dependencies = [
"cfg-if",
"crash-context",
"libc",
- "mach2",
+ "mach2 0.4.2",
"parking_lot",
]
@@ -4063,13 +4038,19 @@ dependencies = [
name = "crashes"
version = "0.1.0"
dependencies = [
+ "bincode",
"crash-handler",
"log",
+ "mach2 0.5.0",
"minidumper",
"paths",
"release_channel",
+ "serde",
+ "serde_json",
"smol",
+ "system_specs",
"workspace-hack",
+ "zstd",
]
[[package]]
@@ -4164,6 +4145,19 @@ dependencies = [
"itertools 0.10.5",
]
+[[package]]
+name = "crossbeam"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-epoch",
+ "crossbeam-queue",
+ "crossbeam-utils",
+]
+
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
@@ -4494,6 +4488,7 @@ dependencies = [
"tempfile",
"util",
"workspace-hack",
+ "zed_env_vars",
]
[[package]]
@@ -4670,27 +4665,6 @@ dependencies = [
"syn 2.0.101",
]
-[[package]]
-name = "derive_more"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
-dependencies = [
- "derive_more-impl",
-]
-
-[[package]]
-name = "derive_more-impl"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.101",
- "unicode-xid",
-]
-
[[package]]
name = "derive_refineable"
version = "0.1.0"
@@ -4711,7 +4685,6 @@ dependencies = [
"component",
"ctor",
"editor",
- "futures 0.3.31",
"gpui",
"indoc",
"language",
@@ -4720,7 +4693,7 @@ dependencies = [
"markdown",
"pretty_assertions",
"project",
- "rand 0.8.5",
+ "rand 0.9.1",
"serde",
"serde_json",
"settings",
@@ -4760,7 +4733,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291"
dependencies = [
- "nu-ansi-term 0.50.1",
+ "nu-ansi-term",
]
[[package]]
@@ -5065,6 +5038,7 @@ dependencies = [
"clock",
"collections",
"convert_case 0.8.0",
+ "criterion",
"ctor",
"dap",
"db",
@@ -5091,7 +5065,7 @@ dependencies = [
"parking_lot",
"pretty_assertions",
"project",
- "rand 0.8.5",
+ "rand 0.9.1",
"regex",
"release_channel",
"rpc",
@@ -5586,7 +5560,7 @@ dependencies = [
"parking_lot",
"paths",
"project",
- "rand 0.8.5",
+ "rand 0.9.1",
"release_channel",
"remote",
"reqwest_client",
@@ -5659,8 +5633,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [
"bit-set 0.5.3",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
@@ -5670,8 +5644,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set 0.8.0",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
@@ -5748,14 +5722,10 @@ dependencies = [
name = "feedback"
version = "0.1.0"
dependencies = [
- "client",
"editor",
"gpui",
- "human_bytes",
"menu",
- "release_channel",
- "serde",
- "sysinfo",
+ "system_specs",
"ui",
"urlencoding",
"util",
@@ -6190,17 +6160,6 @@ dependencies = [
"futures-util",
]
-[[package]]
-name = "futures-batch"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f444c45a1cb86f2a7e301469fd50a82084a60dadc25d94529a8312276ecb71a"
-dependencies = [
- "futures 0.3.31",
- "futures-timer",
- "pin-utils",
-]
-
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -6296,12 +6255,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
-[[package]]
-name = "futures-timer"
-version = "3.0.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
-
[[package]]
name = "futures-util"
version = "0.3.31"
@@ -6375,17 +6328,6 @@ dependencies = [
"windows-targets 0.48.5",
]
-[[package]]
-name = "getrandom"
-version = "0.1.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi 0.9.0+wasi-snapshot-preview1",
-]
-
[[package]]
name = "getrandom"
version = "0.2.15"
@@ -6442,7 +6384,7 @@ dependencies = [
"askpass",
"async-trait",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"futures 0.3.31",
"git2",
"gpui",
@@ -6450,7 +6392,7 @@ dependencies = [
"log",
"parking_lot",
"pretty_assertions",
- "rand 0.8.5",
+ "rand 0.9.1",
"regex",
"rope",
"schemars",
@@ -7336,8 +7278,8 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
+ "regex-automata",
+ "regex-syntax",
]
[[package]]
@@ -7472,7 +7414,7 @@ dependencies = [
"core-video",
"cosmic-text",
"ctor",
- "derive_more 0.99.19",
+ "derive_more",
"embed-resource",
"env_logger 0.11.8",
"etagere",
@@ -7503,7 +7445,7 @@ dependencies = [
"pathfinder_geometry",
"postage",
"profiling",
- "rand 0.8.5",
+ "rand 0.9.1",
"raw-window-handle",
"refineable",
"reqwest_client",
@@ -7518,6 +7460,7 @@ dependencies = [
"slotmap",
"smallvec",
"smol",
+ "stacksafe",
"strum 0.27.1",
"sum_tree",
"taffy",
@@ -7559,6 +7502,7 @@ dependencies = [
name = "gpui_tokio"
version = "0.1.0"
dependencies = [
+ "anyhow",
"gpui",
"tokio",
"util",
@@ -7882,6 +7826,12 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "hound"
+version = "3.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
+
[[package]]
name = "html5ever"
version = "0.27.0"
@@ -7983,34 +7933,13 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f"
-[[package]]
-name = "http-types"
-version = "2.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad"
-dependencies = [
- "anyhow",
- "async-channel 1.9.0",
- "base64 0.13.1",
- "futures-lite 1.13.0",
- "http 0.2.12",
- "infer",
- "pin-project-lite",
- "rand 0.7.3",
- "serde",
- "serde_json",
- "serde_qs 0.8.5",
- "serde_urlencoded",
- "url",
-]
-
[[package]]
name = "http_client"
version = "0.1.0"
dependencies = [
"anyhow",
"bytes 1.10.1",
- "derive_more 0.99.19",
+ "derive_more",
"futures 0.3.31",
"http 1.3.1",
"http-body 1.0.1",
@@ -1,6 +1,7 @@
[workspace]
resolver = "2"
members = [
+ "crates/acp_tools",
"crates/acp_thread",
"crates/action_log",
"crates/activity_indicator",
@@ -53,6 +54,8 @@ members = [
"crates/deepseek",
"crates/diagnostics",
"crates/docs_preprocessor",
+ "crates/edit_prediction",
+ "crates/edit_prediction_button",
"crates/editor",
"crates/eval",
"crates/explorer_command_injector",
@@ -81,21 +84,21 @@ members = [
"crates/http_client_tls",
"crates/icons",
"crates/image_viewer",
- "crates/indexed_docs",
- "crates/edit_prediction",
- "crates/edit_prediction_button",
"crates/inspector_ui",
"crates/install_cli",
"crates/jj",
"crates/jj_ui",
"crates/journal",
+ "crates/keymap_editor",
"crates/language",
"crates/language_extension",
"crates/language_model",
"crates/language_models",
+ "crates/language_onboarding",
"crates/language_selector",
"crates/language_tools",
"crates/languages",
+ "crates/line_ending_selector",
"crates/livekit_api",
"crates/livekit_client",
"crates/lmstudio",
@@ -130,6 +133,7 @@ members = [
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/release_channel",
+ "crates/scheduler",
"crates/remote",
"crates/remote_server",
"crates/repl",
@@ -140,12 +144,12 @@ members = [
"crates/rules_library",
"crates/schema_generator",
"crates/search",
- "crates/semantic_index",
"crates/semantic_version",
"crates/session",
"crates/settings",
"crates/settings_profile_selector",
"crates/settings_ui",
+ "crates/settings_ui_macros",
"crates/snippet",
"crates/snippet_provider",
"crates/snippets_ui",
@@ -158,6 +162,7 @@ members = [
"crates/supermaven",
"crates/supermaven_api",
"crates/svg_preview",
+ "crates/system_specs",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
@@ -190,6 +195,7 @@ members = [
"crates/x_ai",
"crates/zed",
"crates/zed_actions",
+ "crates/zed_env_vars",
"crates/zeta",
"crates/zeta_cli",
"crates/zlog",
@@ -199,7 +205,6 @@ members = [
# Extensions
#
- "extensions/emmet",
"extensions/glsl",
"extensions/html",
"extensions/proto",
@@ -207,7 +212,6 @@ members = [
"extensions/slash-commands-example",
"extensions/snippets",
"extensions/test-extension",
- "extensions/toml",
#
# Tooling
@@ -228,6 +232,7 @@ edition = "2024"
# Workspace member crates
#
+acp_tools = { path = "crates/acp_tools" }
acp_thread = { path = "crates/acp_thread" }
action_log = { path = "crates/action_log" }
agent = { path = "crates/agent" }
@@ -272,6 +277,7 @@ context_server = { path = "crates/context_server" }
copilot = { path = "crates/copilot" }
crashes = { path = "crates/crashes" }
credentials_provider = { path = "crates/credentials_provider" }
+crossbeam = "0.8.4"
dap = { path = "crates/dap" }
dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
@@ -296,9 +302,7 @@ git_hosting_providers = { path = "crates/git_hosting_providers" }
git_ui = { path = "crates/git_ui" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
-gpui = { path = "crates/gpui", default-features = false, features = [
- "http_client",
-] }
+gpui = { path = "crates/gpui", default-features = false }
gpui_macros = { path = "crates/gpui_macros" }
gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
@@ -306,7 +310,6 @@ http_client = { path = "crates/http_client" }
http_client_tls = { path = "crates/http_client_tls" }
icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" }
-indexed_docs = { path = "crates/indexed_docs" }
edit_prediction = { path = "crates/edit_prediction" }
edit_prediction_button = { path = "crates/edit_prediction_button" }
inspector_ui = { path = "crates/inspector_ui" }
@@ -314,13 +317,16 @@ install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
jj_ui = { path = "crates/jj_ui" }
journal = { path = "crates/journal" }
+keymap_editor = { path = "crates/keymap_editor" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_models = { path = "crates/language_models" }
+language_onboarding = { path = "crates/language_onboarding" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
+line_ending_selector = { path = "crates/line_ending_selector" }
livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" }
lmstudio = { path = "crates/lmstudio" }
@@ -358,20 +364,22 @@ proto = { path = "crates/proto" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
release_channel = { path = "crates/release_channel" }
+scheduler = { path = "crates/scheduler" }
remote = { path = "crates/remote" }
remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
+rodio = { git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"}
rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
search = { path = "crates/search" }
-semantic_index = { path = "crates/semantic_index" }
semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_ui = { path = "crates/settings_ui" }
+settings_ui_macros = { path = "crates/settings_ui_macros" }
snippet = { path = "crates/snippet" }
snippet_provider = { path = "crates/snippet_provider" }
snippets_ui = { path = "crates/snippets_ui" }
@@ -383,6 +391,7 @@ streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
+system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
@@ -416,6 +425,7 @@ worktree = { path = "crates/worktree" }
x_ai = { path = "crates/x_ai" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
+zed_env_vars = { path = "crates/zed_env_vars" }
zeta = { path = "crates/zeta" }
zlog = { path = "crates/zlog" }
zlog_settings = { path = "crates/zlog_settings" }
@@ -424,8 +434,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
-agentic-coding-protocol = "0.0.10"
-agent-client-protocol = "0.0.23"
+agent-client-protocol = { version = "0.2.0-alpha.8", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -439,6 +448,7 @@ async-fs = "2.1"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
async-recursion = "1.0.0"
async-tar = "0.5.0"
+async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.29.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
@@ -451,11 +461,13 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [
] }
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
+backtrace = "0.3"
base64 = "0.22"
+bincode = "1.2.1"
bitflags = "2.6.0"
-blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
-blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
-blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
+blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
+blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
+blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
@@ -495,6 +507,7 @@ handlebars = "4.3"
heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
+human_bytes = "0.4.1"
html5ever = "0.27.0"
http = "1.1"
http-body = "1.0"
@@ -516,7 +529,8 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
-lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" }
+lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "0874f8742fe55b4dc94308c1e3c0069710d8eeaf" }
+mach2 = "0.5"
markup5ever_rcdom = "0.3.0"
metal = "0.29"
minidumper = "0.8"
@@ -527,12 +541,38 @@ nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c80421
nix = "0.29"
num-format = "0.4.4"
objc = "0.2"
+objc2-foundation = { version = "0.3", default-features = false, features = [
+ "NSArray",
+ "NSAttributedString",
+ "NSBundle",
+ "NSCoder",
+ "NSData",
+ "NSDate",
+ "NSDictionary",
+ "NSEnumerator",
+ "NSError",
+ "NSGeometry",
+ "NSNotification",
+ "NSNull",
+ "NSObjCRuntime",
+ "NSObject",
+ "NSProcessInfo",
+ "NSRange",
+ "NSRunLoop",
+ "NSString",
+ "NSURL",
+ "NSUndoManager",
+ "NSValue",
+ "objc2-core-foundation",
+ "std"
+] }
open = "5.0.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
partial-json-fixer = "0.5.3"
parse_int = "0.9"
+pciid-parser = "0.8.0"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
@@ -551,7 +591,7 @@ prost-build = "0.9"
prost-types = "0.9"
pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9"
-rand = "0.8.5"
+rand = "0.9"
rayon = "1.8"
ref-cast = "1.0.24"
regex = "1.5"
@@ -564,7 +604,6 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
"socks",
"stream",
] }
-rodio = { version = "0.21.1", default-features = false }
rsa = "0.9.6"
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
"async-dispatcher-runtime",
@@ -584,7 +623,9 @@ serde_json_lenient = { version = "0.2", features = [
"preserve_order",
"raw_value",
] }
+serde_path_to_error = "0.1.17"
serde_repr = "0.1"
+serde_urlencoded = "0.7"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
@@ -592,6 +633,7 @@ simplelog = "0.12.2"
smallvec = { version = "1.6", features = ["union"] }
smol = "2.0"
sqlformat = "0.2"
+stacksafe = "0.1"
streaming-iterator = "0.1"
strsim = "0.11"
strum = { version = "0.27.0", features = ["derive"] }
@@ -618,7 +660,7 @@ tower-http = "0.4.4"
tree-sitter = { version = "0.25.6", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
tree-sitter-c = "0.23"
-tree-sitter-cpp = "0.23"
+tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
tree-sitter-css = "0.23"
tree-sitter-diff = "0.1.0"
tree-sitter-elixir = "0.3"
@@ -662,25 +704,9 @@ which = "6.0.0"
windows-core = "0.61"
wit-component = "0.221"
workspace-hack = "0.1.0"
-# We can switch back to the published version once https://github.com/infinitefield/yawc/pull/16 is merged and a new
-# version is released.
-yawc = { git = "https://github.com/deviant-forks/yawc", rev = "1899688f3e69ace4545aceb97b2a13881cf26142" }
+yawc = "0.2.5"
zstd = "0.11"
-[workspace.dependencies.async-stripe]
-git = "https://github.com/zed-industries/async-stripe"
-rev = "3672dd4efb7181aa597bf580bf5a2f5d23db6735"
-default-features = false
-features = [
- "runtime-tokio-hyper-rustls",
- "billing",
- "checkout",
- "events",
- # The features below are only enabled to get the `events` feature to build.
- "chrono",
- "connect",
-]
-
[workspace.dependencies.windows]
version = "0.61"
features = [
@@ -701,6 +727,7 @@ features = [
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
+ "Win32_Graphics_Hlsl",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",
@@ -818,39 +845,33 @@ unexpected_cfgs = { level = "allow" }
dbg_macro = "deny"
todo = "deny"
-# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so
-# warning on this rule produces a lot of noise.
-single_range_in_vec_init = "allow"
+# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
+# Remove when the lint gets promoted to `suspicious`.
+declare_interior_mutable_const = "deny"
+
+redundant_clone = "deny"
-# These are all of the rules that currently have violations in the Zed
-# codebase.
+# We currently do not restrict any style rules
+# as it slows down shipping code to Zed.
#
-# We'll want to drive this list down by either:
-# 1. fixing violations of the rule and begin enforcing it
-# 2. deciding we want to allow the rule permanently, at which point
-# we should codify that separately above.
+# Running ./script/clippy can take several minutes, and so it's
+# common to skip that step and let CI do it. Any unexpected failures
+# (which also take minutes to discover) thus require switching back
+# to an old branch, manual fixing, and re-pushing.
#
-# This list shouldn't be added to; it should only get shorter.
-# =============================================================================
-
-# There are a bunch of rules currently failing in the `style` group, so
-# allow all of those, for now.
+# In the future we could improve this by either making sure
+# Zed can surface clippy errors in diagnostics (in addition to the
+# rust-analyzer errors), or by having CI fix style nits automatically.
style = { level = "allow", priority = -1 }
-# Temporary list of style lints that we've fixed so far.
-module_inception = { level = "deny" }
-question_mark = { level = "deny" }
-redundant_closure = { level = "deny" }
-declare_interior_mutable_const = { level = "deny" }
# Individual rules that have violations in the codebase:
type_complexity = "allow"
-# We often return trait objects from `new` functions.
-new_ret_no_self = { level = "allow" }
-# We have a few `next` functions that differ in lifetimes
-# compared to Iterator::next. Yet, clippy complains about those.
-should_implement_trait = { level = "allow" }
let_underscore_future = "allow"
+# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so
+# warning on this rule produces a lot of noise.
+single_range_in_vec_init = "allow"
+
# in Rust it can be very tedious to reduce argument count without
# running afoul of the borrow checker.
too_many_arguments = "allow"
@@ -858,6 +879,9 @@ too_many_arguments = "allow"
# We often have large enum variants yet we rarely actually bother with splitting them up.
large_enum_variant = "allow"
+# Boolean expressions can be hard to read, requiring only the minimal form gets in the way
+nonminimal_bool = "allow"
+
[workspace.metadata.cargo-machete]
ignored = [
"bindgen",
@@ -0,0 +1,2 @@
+postgrest_llm: postgrest crates/collab/postgrest_llm.conf
+website: cd ../zed.dev; npm run dev -- --port=3000
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M10.5 8.75V10.5C8.43097 10.5 7.56903 10.5 5.5 10.5V10L10.5 6V5.5H5.5V7.25" stroke="black" stroke-width="1.5"/>
+ <path d="M10.5 8.75V10.5C8.43097 10.5 7.56903 10.5 5.5 10.5V10L10.5 6V5.5H5.5V7.25" stroke="black" stroke-width="1.2"/>
<path d="M1.5 8.5C1.77614 8.5 2 8.27614 2 8C2 7.72386 1.77614 7.5 1.5 7.5C1.22386 7.5 1 7.72386 1 8C1 8.27614 1.22386 8.5 1.5 8.5Z" fill="black"/>
<path d="M2.49976 6.33002C2.7759 6.33002 2.99976 6.10616 2.99976 5.83002C2.99976 5.55387 2.7759 5.33002 2.49976 5.33002C2.22361 5.33002 1.99976 5.55387 1.99976 5.83002C1.99976 6.10616 2.22361 6.33002 2.49976 6.33002Z" fill="black"/>
<path d="M2.49976 10.66C2.7759 10.66 2.99976 10.4361 2.99976 10.16C2.99976 9.88383 2.7759 9.65997 2.49976 9.65997C2.22361 9.65997 1.99976 9.88383 1.99976 10.16C1.99976 10.4361 2.22361 10.66 2.49976 10.66Z" fill="black"/>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.8989 5.77778L11.6434 4.52222C10.6384 3.55068 9.29673 3.00526 7.89893 3C6.57285 3 5.30103 3.52678 4.36343 4.46447C3.78887 5.03901 3.36856 5.73897 3.12921 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.8989 3V5.77778H10.1211" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.1012 10.2222L4.3568 11.4778C5.3618 12.4493 6.70342 12.9947 8.10122 13C9.42731 13 10.6991 12.4732 11.6368 11.5355C12.2163 10.956 12.6389 10.2487 12.8772 9.47994" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.87891 10.2222H3.10111V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8989 5.77778L11.6434 4.52222C10.6384 3.55068 9.29673 3.00526 7.89893 3C6.57285 3 5.30103 3.52678 4.36343 4.46447C3.78887 5.03901 3.36856 5.73897 3.12921 6.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8989 3V5.77778H10.1211" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.1012 10.2222L4.3568 11.4778C5.3618 12.4493 6.70342 12.9947 8.10122 13C9.42731 13 10.6991 12.4732 11.6368 11.5355C12.2163 10.956 12.6389 10.2487 12.8772 9.47994" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.87891 10.2222H3.10111V13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 13L12.5 8.5M8 13L3.5 8.5M8 13V3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 13L12.5 8.5M8 13L3.5 8.5M8 13V3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m2.5 10.667 2.667 2.666 2.666-2.666M5.167 13.333V2.667M11.833 6.667v-4H10.5M10.5 6.667h2.667M13.167 10.667a1.333 1.333 0 0 0-2.667 0V12a1.333 1.333 0 0 0 2.667 0v-1.333Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m2.5 10.667 2.667 2.666 2.666-2.666M5.167 13.333V2.667M11.833 6.667v-4H10.5M10.5 6.667h2.667M13.167 10.667a1.333 1.333 0 0 0-2.667 0V12a1.333 1.333 0 0 0 2.667 0v-1.333Z"/></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.25 4.25L11.125 11.125" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.75 4.25006V11.7501H4.25" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.25 4.25L11.125 11.125" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.75 4.25006V11.7501H4.25" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.5 7.50001L8 3M3.5 7.50001L8 12M3.5 7.50001H12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 7.50001L8 3M3.5 7.50001L8 12M3.5 7.50001H12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.5 8L8 12.5M12.5 8L8 3.5M12.5 8H3.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 8L8 12.5M12.5 8L8 3.5M12.5 8H3.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11 2L13 4.5L11 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.5 4.5H2.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 14L3 11.5L5 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 11.5H13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 2L13 4.5L11 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 4.5H2.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 14L3 11.5L5 9" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 11.5H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 3L12.5 7.5M8 3L3.5 7.5M8 3V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 3L12.5 7.5M8 3L3.5 7.5M8 3V13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.5 11.5L11.5 4.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.5 4.5L11.5 4.5L11.5 11.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.5 11.5L11.5 4.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.5 4.5L11.5 4.5L11.5 11.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.37288 4.48506L7.43539 10.6638C7.43539 10.9365 7.54373 11.1981 7.73655 11.3909C7.92938 11.5837 8.19092 11.6921 8.46362 11.6921C8.73632 11.6921 8.99785 11.5837 9.19068 11.3909C9.38351 11.1981 9.49184 10.9366 9.49184 10.6638L9.42933 4.48506C9.42933 3.93975 9.2127 3.41678 8.82711 3.03119C8.44152 2.6456 7.91855 2.42898 7.37324 2.42898C6.82794 2.42898 6.30496 2.6456 5.91937 3.03119C5.53378 3.41678 5.31716 3.93975 5.31716 4.48506L5.37968 10.6384C5.37636 11.0455 5.45368 11.4492 5.60718 11.8263C5.76067 12.2034 5.98731 12.5463 6.27401 12.8354C6.56071 13.1244 6.9018 13.3538 7.27761 13.5104C7.65341 13.667 8.0565 13.7476 8.46362 13.7476C8.87073 13.7476 9.27382 13.667 9.64963 13.5104C10.0254 13.3538 10.3665 13.1244 10.6532 12.8354C10.9399 12.5463 11.1666 12.2034 11.3201 11.8263C11.4736 11.4492 11.5509 11.0455 11.5476 10.6384L11.485 4.48506" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.6667 6C11.003 6.44823 11.2208 6.97398 11.3001 7.52867" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.9094 3.75732C13.7621 4.6095 14.3383 5.69876 14.5629 6.88315C14.7875 8.06754 14.6502 9.29213 14.1688 10.3973" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2.66675 2L13.6667 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.33333 4.66669L4.942 5.05802C4.85494 5.1456 4.75136 5.21504 4.63726 5.2623C4.52317 5.30957 4.40083 5.33372 4.27733 5.33335H2.66667C2.48986 5.33335 2.32029 5.40359 2.19526 5.52862C2.07024 5.65364 2 5.82321 2 6.00002V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3088 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2646 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8654V7.33335" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.21875 2.78136C7.28267 2.71719 7.36421 2.67345 7.45303 2.65568C7.54184 2.63791 7.63393 2.64691 7.71762 2.68154C7.80132 2.71618 7.87284 2.77488 7.92312 2.85022C7.97341 2.92555 8.0002 3.01412 8.00008 3.10469V3.56202" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.6667 6C11.003 6.44823 11.2208 6.97398 11.3001 7.52867" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.9094 3.75732C13.7621 4.6095 14.3383 5.69876 14.5629 6.88315C14.7875 8.06754 14.6502 9.29213 14.1688 10.3973" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.66675 2L13.6667 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.33333 4.66669L4.942 5.05802C4.85494 5.1456 4.75136 5.21504 4.63726 5.2623C4.52317 5.30957 4.40083 5.33372 4.27733 5.33335H2.66667C2.48986 5.33335 2.32029 5.40359 2.19526 5.52862C2.07024 5.65364 2 5.82321 2 6.00002V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3088 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2646 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8654V7.33335" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.21875 2.78136C7.28267 2.71719 7.36421 2.67345 7.45303 2.65568C7.54184 2.63791 7.63393 2.64691 7.71762 2.68154C7.80132 2.71618 7.87284 2.77488 7.92312 2.85022C7.97341 2.92555 8.0002 3.01412 8.00008 3.10469V3.56202" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 3.13467C7.99987 3.04181 7.97223 2.95107 7.92057 2.8739C7.86892 2.79674 7.79557 2.7366 7.70977 2.70108C7.62397 2.66557 7.52958 2.65626 7.43849 2.67434C7.34741 2.69242 7.26373 2.73707 7.198 2.80266L4.942 5.058C4.85494 5.14558 4.75136 5.21502 4.63726 5.26228C4.52317 5.30954 4.40083 5.33369 4.27733 5.33333H2.66667C2.48986 5.33333 2.32029 5.40357 2.19526 5.52859C2.07024 5.65362 2 5.82319 2 6V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3087 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2645 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8653V3.13467Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.6667 6C11.0995 6.57699 11.3334 7.27877 11.3334 8C11.3334 8.72123 11.0995 9.42301 10.6667 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.9094 12.2427C13.4666 11.6855 13.9085 11.0241 14.2101 10.2961C14.5116 9.56815 14.6668 8.78793 14.6668 7.99999C14.6668 7.21205 14.5116 6.43183 14.2101 5.70387C13.9085 4.97591 13.4666 4.31448 12.9094 3.75732" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 3.13467C7.99987 3.04181 7.97223 2.95107 7.92057 2.8739C7.86892 2.79674 7.79557 2.7366 7.70977 2.70108C7.62397 2.66557 7.52958 2.65626 7.43849 2.67434C7.34741 2.69242 7.26373 2.73707 7.198 2.80266L4.942 5.058C4.85494 5.14558 4.75136 5.21502 4.63726 5.26228C4.52317 5.30954 4.40083 5.33369 4.27733 5.33333H2.66667C2.48986 5.33333 2.32029 5.40357 2.19526 5.52859C2.07024 5.65362 2 5.82319 2 6V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3087 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2645 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8653V3.13467Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.6667 6C11.0995 6.57699 11.3334 7.27877 11.3334 8C11.3334 8.72123 11.0995 9.42301 10.6667 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.9094 12.2427C13.4666 11.6855 13.9085 11.0241 14.2101 10.2961C14.5116 9.56815 14.6668 8.78793 14.6668 7.99999C14.6668 7.21205 14.5116 6.43183 14.2101 5.70387C13.9085 4.97591 13.4666 4.31448 12.9094 3.75732" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.79998 4C6.50183 4.00002 6.21436 4.10574 5.99358 4.29657L2.19677 7.57657C2.1348 7.63013 2.08528 7.69545 2.05139 7.76832C2.01751 7.8412 2 7.92001 2 7.99971C2 8.07941 2.01751 8.15823 2.05139 8.23111C2.08528 8.30398 2.1348 8.36929 2.19677 8.42286L5.99358 11.7034C6.21436 11.8943 6.50183 12 6.79998 12H12.8C13.1183 12 13.4235 11.8796 13.6485 11.6653C13.8736 11.4509 14 11.1602 14 10.8571V5.14286C14 4.83975 13.8736 4.54906 13.6485 4.33474C13.4235 4.12041 13.1183 4 12.8 4H6.79998Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.5 6.5L11.5 9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.5 6.5L8.5 9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.79998 4C6.50183 4.00002 6.21436 4.10574 5.99358 4.29657L2.19677 7.57657C2.1348 7.63013 2.08528 7.69545 2.05139 7.76832C2.01751 7.8412 2 7.92001 2 7.99971C2 8.07941 2.01751 8.15823 2.05139 8.23111C2.08528 8.30398 2.1348 8.36929 2.19677 8.42286L5.99358 11.7034C6.21436 11.8943 6.50183 12 6.79998 12H12.8C13.1183 12 13.4235 11.8796 13.6485 11.6653C13.8736 11.4509 14 11.1602 14 10.8571V5.14286C14 4.83975 13.8736 4.54906 13.6485 4.33474C13.4235 4.12041 13.1183 4 12.8 4H6.79998Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.5 6.5L11.5 9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.5 6.5L8.5 9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M5.33333 5.8C5.33333 5.05739 5.61429 4.3452 6.11438 3.8201C6.61448 3.295 7.29276 3 8 3C8.70724 3 9.38552 3.295 9.88562 3.8201C10.3857 4.3452 10.6667 5.05739 10.6667 5.8C10.6667 9.06667 12 10 12 10H4C4 10 5.33333 9.06667 5.33333 5.8Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
- <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M5.33333 5.8C5.33333 5.05739 5.61429 4.3452 6.11438 3.8201C6.61448 3.295 7.29276 3 8 3C8.70724 3 9.38552 3.295 9.88562 3.8201C10.3857 4.3452 10.6667 5.05739 10.6667 5.8C10.6667 9.06667 12 10 12 10H4C4 10 5.33333 9.06667 5.33333 5.8Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M4.86142 8.6961C4.47786 9.66547 4 9.99997 4 9.99997H12C12 9.99997 10.6667 9.06664 10.6667 5.79997C10.6667 5.05737 10.3857 4.34518 9.88562 3.82007C9.52389 3.44026 9.06893 3.18083 8.57722 3.06635" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
- <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M4.86142 8.6961C4.47786 9.66547 4 9.99997 4 9.99997H12C12 9.99997 10.6667 9.06664 10.6667 5.79997C10.6667 5.05737 10.3857 4.34518 9.88562 3.82007C9.52389 3.44026 9.06893 3.18083 8.57722 3.06635" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="4.5" cy="4.5" r="2.5" fill="black"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M10.6667 5.8C10.6667 5.05739 10.3857 4.3452 9.88562 3.8201C9.38552 3.295 8.70724 3 8 3C7.29276 3 6.61448 3.295 6.11438 3.8201C5.61428 4.3452 5.33333 5.05739 5.33333 5.8C5.33333 9.06667 4 10 4 10H7.375" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
- <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
- <path d="M4 3L12.5 11.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+ <path d="M10.6667 5.8C10.6667 5.05739 10.3857 4.3452 9.88562 3.8201C9.38552 3.295 8.70724 3 8 3C7.29276 3 6.61448 3.295 6.11438 3.8201C5.61428 4.3452 5.33333 5.05739 5.33333 5.8C5.33333 9.06667 4 10 4 10H7.375" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M4 3L12.5 11.5" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M5.33333 5.8C5.33333 5.05739 5.61429 4.3452 6.11438 3.8201C6.61448 3.295 7.29276 3 8 3C8.70724 3 9.38552 3.295 9.88562 3.8201C10.3857 4.3452 10.6667 5.05739 10.6667 5.8C10.6667 9.06667 12 10 12 10H4C4 10 5.33333 9.06667 5.33333 5.8Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
- <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
- <path d="M12 2.02081C12.617 2.89491 13.0754 3.88797 13.2528 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
- <path d="M4 2.02081C3.38299 2.89491 2.92461 3.88797 2.74719 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M5.33333 5.8C5.33333 5.05739 5.61429 4.3452 6.11438 3.8201C6.61448 3.295 7.29276 3 8 3C8.70724 3 9.38552 3.295 9.88562 3.8201C10.3857 4.3452 10.6667 5.05739 10.6667 5.8C10.6667 9.06667 12 10 12 10H4C4 10 5.33333 9.06667 5.33333 5.8Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M6 12.5C6.19692 12.8028 6.48641 13.0554 6.83822 13.2313C7.19004 13.4072 7.59127 13.5 8 13.5C8.40873 13.5 8.80996 13.4072 9.16178 13.2313C9.51359 13.0554 9.80308 12.8028 10 12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M12 2.02081C12.617 2.89491 13.0754 3.88797 13.2528 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M4 2.02081C3.38299 2.89491 2.92461 3.88797 2.74719 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 10.667a1.333 1.333 0 1 0-2.667 0V12A1.333 1.333 0 1 0 12 12v-1.333ZM6.667 4A1.333 1.333 0 0 0 4 4v1.333a1.333 1.333 0 1 0 2.667 0V4ZM4 13.333h2.667M9.333 6.667H12M4 9.333h1.333v4M9.333 2.667h1.334v4"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12 10.667a1.333 1.333 0 1 0-2.667 0V12A1.333 1.333 0 1 0 12 12v-1.333ZM6.667 4A1.333 1.333 0 0 0 4 4v1.333a1.333 1.333 0 1 0 2.667 0V4ZM4 13.333h2.667M9.333 6.667H12M4 9.333h1.333v4M9.333 2.667h1.334v4"/></svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M9.29787 2.8462C9.41607 2.90046 9.51178 2.98975 9.5701 3.10016C9.62841 3.21057 9.64605 3.3359 9.62028 3.45666L8.96749 6.51117H12.2195C12.334 6.51115 12.446 6.54191 12.5423 6.59976C12.6386 6.65762 12.7151 6.74013 12.7627 6.83748C12.8102 6.93482 12.8269 7.04291 12.8106 7.14885C12.7943 7.2548 12.7458 7.35413 12.6709 7.43504L7.49631 13.0184C7.40998 13.1115 7.29318 13.1752 7.16411 13.1997C7.03504 13.2241 6.90092 13.2081 6.78264 13.1539C6.66437 13.0997 6.56859 13.0104 6.5102 12.9C6.4518 12.7896 6.43408 12.6643 6.45979 12.5435L7.11259 9.48899H3.86054C3.74609 9.489 3.63405 9.45825 3.53776 9.40039C3.44147 9.34254 3.36498 9.26003 3.31742 9.16268C3.26986 9.06534 3.25322 8.95725 3.26949 8.85131C3.28576 8.74536 3.33423 8.64603 3.40916 8.56513L8.58377 2.98169C8.67012 2.88856 8.78699 2.82478 8.91616 2.80028C9.04533 2.77576 9.17953 2.79192 9.29787 2.8462Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9.29787 2.8462C9.41607 2.90046 9.51178 2.98975 9.5701 3.10016C9.62841 3.21057 9.64605 3.3359 9.62028 3.45666L8.96749 6.51117H12.2195C12.334 6.51115 12.446 6.54191 12.5423 6.59976C12.6386 6.65762 12.7151 6.74013 12.7627 6.83748C12.8102 6.93482 12.8269 7.04291 12.8106 7.14885C12.7943 7.2548 12.7458 7.35413 12.6709 7.43504L7.49631 13.0184C7.40998 13.1115 7.29318 13.1752 7.16411 13.1997C7.03504 13.2241 6.90092 13.2081 6.78264 13.1539C6.66437 13.0997 6.56859 13.0104 6.5102 12.9C6.4518 12.7896 6.43408 12.6643 6.45979 12.5435L7.11259 9.48899H3.86054C3.74609 9.489 3.63405 9.45825 3.53776 9.40039C3.44147 9.34254 3.36498 9.26003 3.31742 9.16268C3.26986 9.06534 3.25322 8.95725 3.26949 8.85131C3.28576 8.74536 3.33423 8.64603 3.40916 8.56513L8.58377 2.98169C8.67012 2.88856 8.78699 2.82478 8.91616 2.80028C9.04533 2.77576 9.17953 2.79192 9.29787 2.8462Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 12.125v-8.25c0-.365.132-.714.366-.972.235-.258.552-.403.884-.403H12v11H5.25c-.332 0-.65-.145-.884-.403A1.448 1.448 0 0 1 4 12.125Zm0 0c0-.365.132-.714.366-.972.235-.258.552-.403.884-.403H12"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4 12.125v-8.25c0-.365.132-.714.366-.972.235-.258.552-.403.884-.403H12v11H5.25c-.332 0-.65-.145-.884-.403A1.448 1.448 0 0 1 4 12.125Zm0 0c0-.365.132-.714.366-.972.235-.258.552-.403.884-.403H12"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.5 10.5V3.643c0-.303.113-.594.315-.808.202-.215.476-.335.762-.335H9.5"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.5 9.5h-.667c-.353 0-.692.105-.942.293-.25.187-.391.442-.391.707 0 .265.14.52.39.707.25.188.59.293.943.293H4.5M13.5 11.25H7.577c-.286 0-.56.118-.762.33a1.151 1.151 0 0 0-.315.795m0 0c0 .298.113.585.315.796.202.21.476.329.762.329H13.5v-9H7.577c-.286 0-.56.119-.762.33a1.151 1.151 0 0 0-.315.795v6.75Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.5 10.5V3.643c0-.303.113-.594.315-.808.202-.215.476-.335.762-.335H9.5"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4.5 9.5h-.667c-.353 0-.692.105-.942.293-.25.187-.391.442-.391.707 0 .265.14.52.39.707.25.188.59.293.943.293H4.5M13.5 11.25H7.577c-.286 0-.56.118-.762.33a1.151 1.151 0 0 0-.315.795m0 0c0 .298.113.585.315.796.202.21.476.329.762.329H13.5v-9H7.577c-.286 0-.56.119-.762.33a1.151 1.151 0 0 0-.315.795v6.75Z"/></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.17279 8.26346C4.87566 8.62402 5.68419 8.72168 6.4527 8.53885C7.2212 8.35601 7.89913 7.90471 8.36433 7.26626C8.82953 6.62781 9.0514 5.8442 8.98996 5.05664C8.92852 4.26908 8.58781 3.52936 8.02922 2.97078C7.47064 2.41219 6.73092 2.07148 5.94336 2.01004C5.1558 1.9486 4.37219 2.17047 3.73374 2.63567C3.09529 3.10087 2.64399 3.7788 2.46115 4.5473C2.27832 5.31581 2.37598 6.12435 2.73654 6.82721L2 9L4.17279 8.26346Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.07168 11C7.16761 11.4537 7.35843 11.8857 7.63567 12.2662C8.10087 12.9047 8.7788 13.356 9.5473 13.5388C10.3158 13.7217 11.1243 13.624 11.8272 13.2634L14 14L13.2635 11.8272C13.624 11.1243 13.7217 10.3158 13.5388 9.54728C13.356 8.77877 12.9047 8.10084 12.2663 7.63564C11.8858 7.3584 11.4537 7.16759 11 7.07166" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.17279 8.26346C4.87566 8.62402 5.68419 8.72168 6.4527 8.53885C7.2212 8.35601 7.89913 7.90471 8.36433 7.26626C8.82953 6.62781 9.0514 5.8442 8.98996 5.05664C8.92852 4.26908 8.58781 3.52936 8.02922 2.97078C7.47064 2.41219 6.73092 2.07148 5.94336 2.01004C5.1558 1.9486 4.37219 2.17047 3.73374 2.63567C3.09529 3.10087 2.64399 3.7788 2.46115 4.5473C2.27832 5.31581 2.37598 6.12435 2.73654 6.82721L2 9L4.17279 8.26346Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.07168 11C7.16761 11.4537 7.35843 11.8857 7.63567 12.2662C8.10087 12.9047 8.7788 13.356 9.5473 13.5388C10.3158 13.7217 11.1243 13.624 11.8272 13.2634L14 14L13.2635 11.8272C13.624 11.1243 13.7217 10.3158 13.5388 9.54728C13.356 8.77877 12.9047 8.10084 12.2663 7.63564C11.8858 7.3584 11.4537 7.16759 11 7.07166" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.625 8.98121L7.03402 10.7714L11.3437 4.75989" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.625 8.98121L7.03402 10.7714L11.3437 4.75989" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.94873 9.02564L7.48722 10.0513L10.0514 6.46149" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.5"/>
+<path d="M5.94873 9.02564L7.48722 10.0513L10.0514 6.46149" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.5999 4.38336L4.99996 10.9833L2 7.98332" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M14 6.78339L9.50009 11.2833L8.6001 10.3833" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.5999 4.38336L4.99996 10.9833L2 7.98332" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14 6.78339L9.50009 11.2833L8.6001 10.3833" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.15186 6.47321L7.99258 10.1696L11.8483 6.47321" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.15186 6.47321L7.99258 10.1696L11.8483 6.47321" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.55361 4.15179L5.85718 7.99251L9.55361 11.8482" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.55361 4.15179L5.85718 7.99251L9.55361 11.8482" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.44653 4.16071L10.1608 8.00143L6.44653 11.8571" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.44653 4.16071L10.1608 8.00143L6.44653 11.8571" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.15186 9.56252L7.99258 5.86609L11.8483 9.56252" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.15186 9.56252L7.99258 5.86609L11.8483 9.56252" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.66675 10L8.00008 13.3333L11.3334 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.66675 6.00002L8.00008 2.66669L11.3334 6.00002" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.66675 10L8.00008 13.3333L11.3334 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.66675 6.00002L8.00008 2.66669L11.3334 6.00002" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.54492 6.5C6.66248 6.16582 6.8945 5.88404 7.19991 5.70455C7.50532 5.52506 7.86439 5.45945 8.21354 5.51934C8.56268 5.57922 8.87937 5.76075 9.1075 6.03175C9.33564 6.30276 9.4605 6.64576 9.45997 7C9.45997 8.00002 7.95994 8.50003 7.95994 8.50003" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 10.5H8.005" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.54492 6.5C6.66248 6.16582 6.8945 5.88404 7.19991 5.70455C7.50532 5.52506 7.86439 5.45945 8.21354 5.51934C8.56268 5.57922 8.87937 5.76075 9.1075 6.03175C9.33564 6.30276 9.4605 6.64576 9.45997 7C9.45997 8.00002 7.95994 8.50003 7.95994 8.50003" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 10.5H8.005" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.70581 4.5L11.294 11.5M11.294 4.5L4.70581 11.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M4.70581 4.5L11.294 11.5M11.294 4.5L4.70581 11.5" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.001 9v4l-2-2M8.001 13l2-2"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.436 10.389a4.215 4.215 0 0 1-1.424-3.484 4.227 4.227 0 0 1 1.92-3.236 4.19 4.19 0 0 1 5.335.665 4.22 4.22 0 0 1 .96 1.677H11.3c.584 0 1.151.19 1.618.54a2.71 2.71 0 0 1 .913 3.116A2.709 2.709 0 0 1 12.762 11"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8.001 9v4l-2-2M8.001 13l2-2"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.436 10.389a4.215 4.215 0 0 1-1.424-3.484 4.227 4.227 0 0 1 1.92-3.236 4.19 4.19 0 0 1 5.335.665 4.22 4.22 0 0 1 .96 1.677H11.3c.584 0 1.151.19 1.618.54a2.71 2.71 0 0 1 .913 3.116A2.709 2.709 0 0 1 12.762 11"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11.75 10.5 2.5-2.5-2.5-2.5M4.25 5.5 1.75 8l2.5 2.5M9.563 3 6.437 13"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m11.75 10.5 2.5-2.5-2.5-2.5M4.25 5.5 1.75 8l2.5 2.5M9.563 3 6.437 13"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 12.8a4.8 4.8 0 1 0 0-9.6 4.8 4.8 0 0 0 0 9.6Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 9.2a1.2 1.2 0 1 0 0-2.4 1.2 1.2 0 0 0 0 2.4ZM8 2v1.2M8 14v-1.2M11 13.196l-.6-1.038M7.4 6.962 5 2.804M13.196 11l-1.038-.6M2.804 5l1.038.6M9.2 8H14M2 8h1.2M13.196 5l-1.038.6M2.804 11l1.038-.6M11 2.804l-.6 1.038M7.4 9.038 5 13.196"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12.8a4.8 4.8 0 1 0 0-9.6 4.8 4.8 0 0 0 0 9.6Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 9.2a1.2 1.2 0 1 0 0-2.4 1.2 1.2 0 0 0 0 2.4ZM8 2v1.2M8 14v-1.2M11 13.196l-.6-1.038M7.4 6.962 5 2.804M13.196 11l-1.038-.6M2.804 5l1.038.6M9.2 8H14M2 8h1.2M13.196 5l-1.038.6M2.804 11l1.038-.6M11 2.804l-.6 1.038M7.4 9.038 5 13.196"/></svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.5 6.12487L7.64656 1.97852C7.84183 1.78327 8.1584 1.78328 8.35366 1.97853L12.5 6.12487" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M3.5 6.12487L7.64656 1.97852C7.84183 1.78327 8.1584 1.78328 8.35366 1.97853L12.5 6.12487" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,9 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.44643 8.76593C6.83106 8.76593 7.14286 9.0793 7.14286 9.46588V10.9825C7.14286 11.369 6.83106 11.6824 6.44643 11.6824C6.06181 11.6824 5.75 11.369 5.75 10.9825V9.46588C5.75 9.0793 6.06181 8.76593 6.44643 8.76593Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.57168 8.76593C9.95631 8.76593 10.2681 9.0793 10.2681 9.46588V10.9825C10.2681 11.369 9.95631 11.6824 9.57168 11.6824C9.18705 11.6824 8.87524 11.369 8.87524 10.9825V9.46588C8.87524 9.0793 9.18705 8.76593 9.57168 8.76593Z" fill="black"/>
-<path d="M7.99976 4.17853C7.99976 6.67853 5.83695 7.28202 4.30332 7.28202C2.76971 7.28202 2.44604 6.1547 2.44604 4.76409C2.44604 3.37347 3.68929 2.24615 5.2229 2.24615C6.75651 2.24615 7.99976 2.78791 7.99976 4.17853Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.5"/>
-<path d="M8 4.17853C8 6.67853 10.1628 7.28202 11.6965 7.28202C13.2301 7.28202 13.5537 6.1547 13.5537 4.76409C13.5537 3.37347 12.3105 2.24615 10.7769 2.24615C9.24325 2.24615 8 2.78791 8 4.17853Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.5"/>
-<path d="M12.5894 6.875C12.5894 6.875 13.3413 7.35585 13.7144 8.08398C14.0876 8.81212 14.0894 10.4985 13.7144 11.1064C13.3395 11.7143 12.8931 12.1429 11.7637 12.7543C10.6344 13.3657 9.143 13.7321 9.143 13.7321H6.85728C6.85728 13.7321 5.37513 13.4107 4.23656 12.7543C3.09798 12.0978 2.55371 11.6786 2.28585 11.1064C2.01799 10.5342 1.92871 8.85715 2.28585 8.08398C2.64299 7.31081 3.42871 6.875 3.42871 6.875" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
+<path d="M7.99976 4.17853C7.99976 6.67853 5.83695 7.28202 4.30332 7.28202C2.76971 7.28202 2.44604 6.1547 2.44604 4.76409C2.44604 3.37347 3.68929 2.24615 5.2229 2.24615C6.75651 2.24615 7.99976 2.78791 7.99976 4.17853Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.2"/>
+<path d="M8 4.17853C8 6.67853 10.1628 7.28202 11.6965 7.28202C13.2301 7.28202 13.5537 6.1547 13.5537 4.76409C13.5537 3.37347 12.3105 2.24615 10.7769 2.24615C9.24325 2.24615 8 2.78791 8 4.17853Z" fill="black" fill-opacity="0.5" stroke="black" stroke-width="1.2"/>
+<path d="M12.5894 6.875C12.5894 6.875 13.3413 7.35585 13.7144 8.08398C14.0876 8.81212 14.0894 10.4985 13.7144 11.1064C13.3395 11.7143 12.8931 12.1429 11.7637 12.7543C10.6344 13.3657 9.143 13.7321 9.143 13.7321H6.85728C6.85728 13.7321 5.37513 13.4107 4.23656 12.7543C3.09798 12.0978 2.55371 11.6786 2.28585 11.1064C2.01799 10.5342 1.92871 8.85715 2.28585 8.08398C2.64299 7.31081 3.42871 6.875 3.42871 6.875" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
<path d="M11.9375 12.6016V7.33636L13.9052 7.99224V10.9255L11.9375 12.6016Z" fill="black" fill-opacity="0.75"/>
<path d="M4.01793 12.6016V7.33636L2.05029 7.99224V10.9255L4.01793 12.6016Z" fill="black" fill-opacity="0.75"/>
</svg>
@@ -1 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12.286 6H7.048C6.469 6 6 6.469 6 7.048v5.238c0 .578.469 1.047 1.048 1.047h5.238c.578 0 1.047-.469 1.047-1.047V7.048c0-.579-.469-1.048-1.047-1.048Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.714 10a1.05 1.05 0 0 1-1.047-1.048V3.714a1.05 1.05 0 0 1 1.047-1.047h5.238A1.05 1.05 0 0 1 10 3.714"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12.486 6.2H7.24795C6.66895 6.2 6.19995 6.669 6.19995 7.248V12.486C6.19995 13.064 6.66895 13.533 7.24795 13.533H12.486C13.064 13.533 13.533 13.064 13.533 12.486V7.248C13.533 6.669 13.064 6.2 12.486 6.2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.91712 10.203C3.63951 10.2022 3.37351 10.0915 3.1773 9.89511C2.98109 9.69872 2.87064 9.43261 2.87012 9.155V3.917C2.87091 3.63956 2.98147 3.37371 3.17765 3.17753C3.37383 2.98135 3.63968 2.87079 3.91712 2.87H9.15512C9.43273 2.87053 9.69883 2.98097 9.89523 3.17718C10.0916 3.37339 10.2023 3.63939 10.2031 3.917" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.8 2h2.4M8 9.2l1.8-1.8M8 14a4.8 4.8 0 1 0 0-9.6A4.8 4.8 0 0 0 8 14Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.8 2h2.4M8 9.2l1.8-1.8M8 14a4.8 4.8 0 1 0 0-9.6A4.8 4.8 0 0 0 8 14Z"/></svg>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.6863 11.3137 2 8 2C4.6863 2 2 4.6863 2 8C2 11.3137 4.6863 14 8 14Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M14 8L11 8" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 5V2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 14V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.6863 11.3137 2 8 2C4.6863 2 2 4.6863 2 8C2 11.3137 4.6863 14 8 14Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14 8L11 8" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 8H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 5V2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 14V11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11 13H10.4C9.76346 13 9.15302 12.7893 8.70296 12.4142C8.25284 12.0391 8 11.5304 8 11V5C8 4.46957 8.25284 3.96086 8.70296 3.58579C9.15302 3.21071 9.76346 3 10.4 3H11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 13H5.6C6.23654 13 6.84698 12.7893 7.29704 12.4142C7.74716 12.0391 8 11.5304 8 11V5C8 4.46957 7.74716 3.96086 7.29704 3.58579C6.84698 3.21071 6.23654 3 5.6 3H5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 13H10.4C9.76346 13 9.15302 12.7893 8.70296 12.4142C8.25284 12.0391 8 11.5304 8 11V5C8 4.46957 8.25284 3.96086 8.70296 3.58579C9.15302 3.21071 9.76346 3 10.4 3H11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 13H5.6C6.23654 13 6.84698 12.7893 7.29704 12.4142C7.74716 12.0391 8 11.5304 8 11V5C8 4.46957 7.74716 3.96086 7.29704 3.58579C6.84698 3.21071 6.23654 3 5.6 3H5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.333 8h9.334"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 8h9.334"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.017 5.625c2.974 0 5.385-.804 5.385-1.795 0-.991-2.41-1.795-5.385-1.795-2.973 0-5.384.804-5.384 1.795 0 .991 2.41 1.795 5.384 1.795Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.633 3.83v8.376c-.003.288.201.571.596.827.394.256.967.477 1.671.643a13.12 13.12 0 0 0 2.373.314c.854.04 1.725.01 2.54-.085M13.402 3.83v1.795M13.402 8.018l-1.795 2.991H14l-1.795 2.992"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.633 8.018c0 .28.198.556.575.805.378.25.925.467 1.599.634.673.167 1.454.279 2.28.327.827.048 1.676.032 2.48-.049"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8.017 5.625c2.974 0 5.385-.804 5.385-1.795 0-.991-2.41-1.795-5.385-1.795-2.973 0-5.384.804-5.384 1.795 0 .991 2.41 1.795 5.384 1.795Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.633 3.83v8.376c-.003.288.201.571.596.827.394.256.967.477 1.671.643a13.12 13.12 0 0 0 2.373.314c.854.04 1.725.01 2.54-.085M13.402 3.83v1.795M13.402 8.018l-1.795 2.991H14l-1.795 2.992"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.633 8.018c0 .28.198.556.575.805.378.25.925.467 1.599.634.673.167 1.454.279 2.28.327.827.048 1.676.032 2.48-.049"/></svg>
@@ -1,12 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.44727 2.19177L6.38617 3.11055" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.64722 3.11055L10.553 2.19177" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.66298 6.07369L5.66298 5.10997C5.64886 4.80689 5.69884 4.50419 5.80993 4.22016C5.92101 3.93613 6.09088 3.67665 6.3093 3.45738C6.52771 3.23811 6.79013 3.0636 7.08074 2.94437C7.37134 2.82514 7.68409 2.76367 8.00011 2.76367C8.31614 2.76367 8.62889 2.82514 8.91949 2.94437C9.21008 3.0636 9.4725 3.23811 9.69092 3.45738C9.90933 3.67665 10.0792 3.93613 10.1903 4.22016C10.3014 4.50419 10.3514 4.80689 10.3373 5.10997V6.07369" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.00017 13.1366C6.09924 13.1366 4.54395 11.6686 4.54395 9.87441V8.24333C4.54395 7.66653 4.7867 7.11337 5.21882 6.70552C5.65092 6.29767 6.237 6.06854 6.8481 6.06854H9.15225C9.76335 6.06854 10.3494 6.29767 10.7815 6.70552C11.2136 7.11337 11.4564 7.66653 11.4564 8.24333V9.87441C11.4564 11.6686 9.9011 13.1366 8.00017 13.1366Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.54343 6.22415C3.43167 6.10894 2.51001 5.12967 2.51001 3.91998" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.54367 8.83472H2.2395" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2.35449 13.4175C2.35449 12.2078 3.33376 11.1709 4.54345 11.1134" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.1673 3.91998C13.1673 5.12967 12.2455 6.10894 11.1511 6.22415" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.7605 8.83472H11.4563" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.4563 11.1134C12.666 11.1709 13.6453 12.2078 13.6453 13.4175" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.44727 2.19177L6.38617 3.11055" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.64722 3.11055L10.553 2.19177" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.66298 6.07369L5.66298 5.10997C5.64886 4.80689 5.69884 4.50419 5.80993 4.22016C5.92101 3.93613 6.09088 3.67665 6.3093 3.45738C6.52771 3.23811 6.79013 3.0636 7.08074 2.94437C7.37134 2.82514 7.68409 2.76367 8.00011 2.76367C8.31614 2.76367 8.62889 2.82514 8.91949 2.94437C9.21008 3.0636 9.4725 3.23811 9.69092 3.45738C9.90933 3.67665 10.0792 3.93613 10.1903 4.22016C10.3014 4.50419 10.3514 4.80689 10.3373 5.10997V6.07369" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.00017 13.1366C6.09924 13.1366 4.54395 11.6686 4.54395 9.87441V8.24333C4.54395 7.66653 4.7867 7.11337 5.21882 6.70552C5.65092 6.29767 6.237 6.06854 6.8481 6.06854H9.15225C9.76335 6.06854 10.3494 6.29767 10.7815 6.70552C11.2136 7.11337 11.4564 7.66653 11.4564 8.24333V9.87441C11.4564 11.6686 9.9011 13.1366 8.00017 13.1366Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.54343 6.22415C3.43167 6.10894 2.51001 5.12967 2.51001 3.91998" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.54367 8.83472H2.2395" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.35449 13.4175C2.35449 12.2078 3.33376 11.1709 4.54345 11.1134" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.1673 3.91998C13.1673 5.12967 12.2455 6.10894 11.1511 6.22415" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.7605 8.83472H11.4563" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.4563 11.1134C12.666 11.1709 13.6453 12.2078 13.6453 13.4175" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.167 3v10M7.167 3l6 5-6 5V3Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4.167 3v10M7.167 3l6 5-6 5V3Z"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" clip-path="url(#a)"><path d="m13 3 2-2M1 15l2-2M4.202 13.53a1.598 1.598 0 0 0 2.266 0L8 11.997 4.003 8 2.47 9.532a1.6 1.6 0 0 0 0 2.265l1.732 1.733ZM5 9l1.5-1.5M7 11l1.5-1.5M8 4.003 11.997 8l1.533-1.532a1.599 1.599 0 0 0 0-2.266L11.798 2.47a1.598 1.598 0 0 0-2.266 0L8 4.003Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="m13 3 2-2M1 15l2-2M4.202 13.53a1.598 1.598 0 0 0 2.266 0L8 11.997 4.003 8 2.47 9.532a1.6 1.6 0 0 0 0 2.265l1.732 1.733ZM5 9l1.5-1.5M7 11l1.5-1.5M8 4.003 11.997 8l1.533-1.532a1.599 1.599 0 0 0 0-2.266L11.798 2.47a1.598 1.598 0 0 0-2.266 0L8 4.003Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.8889 2H4.11111C3.49746 2 3 2.59695 3 3.33333V12.6667C3 13.403 3.49746 14 4.11111 14H11.8889C12.5025 14 13 13.403 13 12.6667V3.33333C13 2.59695 12.5025 2 11.8889 2Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 6H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 10H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.8889 2H4.11111C3.49746 2 3 2.59695 3 3.33333V12.6667C3 13.403 3.49746 14 4.11111 14H11.8889C12.5025 14 13 13.403 13 12.6667V3.33333C13 2.59695 12.5025 2 11.8889 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 6H6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 10H6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2 2L14 14M5.81044 2.41392C6.89676 1.98976 8.08314 1.89138 9.22449 2.13079C10.3658 2.37021 11.4127 2.93705 12.237 3.76199C13.0613 4.58693 13.6273 5.6342 13.8658 6.77573C14.1044 7.91727 14.0051 9.10357 13.5801 10.1896M12.2484 12.2484C11.1176 13.3558 9.59562 13.9724 8.01292 13.9642C6.43021 13.956 4.91467 13.3236 3.79552 12.2045C2.67636 11.0853 2.044 9.56979 2.03578 7.98708C2.02757 6.40438 2.64417 4.88236 3.75165 3.75165" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2 2L14 14M5.81044 2.41392C6.89676 1.98976 8.08314 1.89138 9.22449 2.13079C10.3658 2.37021 11.4127 2.93705 12.237 3.76199C13.0613 4.58693 13.6273 5.6342 13.8658 6.77573C14.1044 7.91727 14.0051 9.10357 13.5801 10.1896M12.2484 12.2484C11.1176 13.3558 9.59562 13.9724 8.01292 13.9642C6.43021 13.956 4.91467 13.3236 3.79552 12.2045C2.67636 11.0853 2.044 9.56979 2.03578 7.98708C2.02757 6.40438 2.64417 4.88236 3.75165 3.75165" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14 11.333A6 6 0 0 0 4 6.867l-1 .9"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M2 4.667v4h4"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12a.667.667 0 1 0 0-1.333A.667.667 0 0 0 8 12Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M14 11.333A6 6 0 0 0 4 6.867l-1 .9"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M2 4.667v4h4"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12a.667.667 0 1 0 0-1.333A.667.667 0 0 0 8 12Z"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.333 10 8 14.667 12.667 10M8 5.333v9.334"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 2.667a.667.667 0 1 0 0-1.334.667.667 0 0 0 0 1.334Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 10 8 14.667 12.667 10M8 5.333v9.334"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 2.667a.667.667 0 1 0 0-1.334.667.667 0 0 0 0 1.334Z"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.333 6 8 1.333 12.667 6M8 10.667V1.333"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 13.333a.667.667 0 1 1 0 1.334.667.667 0 0 1 0-1.334Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 6 8 1.333 12.667 6M8 10.667V1.333"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 13.333a.667.667 0 1 1 0 1.334.667.667 0 0 1 0-1.334Z"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2 11.333a6 6 0 0 1 10-4.466l1 .9"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M14 4.667v4h-4"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12a.667.667 0 1 1 0-1.333A.667.667 0 0 1 8 12Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2 11.333a6 6 0 0 1 10-4.466l1 .9"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M14 4.667v4h-4"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12a.667.667 0 1 1 0-1.333A.667.667 0 0 1 8 12Z"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 3v8M4 7h8M4 13h8"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 3v8M4 7h8M4 13h8"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m2 2 12 12M4.269 4.27a4.2 4.2 0 0 0 1.93 7.93h5.1c.267 0 .53-.039.785-.116M13.72 10.7a2.699 2.699 0 0 0-2.42-3.9h-1.074A4.204 4.204 0 0 0 6.8 3.842"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m2 2 12 12M4.269 4.27a4.2 4.2 0 0 0 1.93 7.93h5.1c.267 0 .53-.039.785-.116M13.72 10.7a2.699 2.699 0 0 0-2.42-3.9h-1.074A4.204 4.204 0 0 0 6.8 3.842"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 9.667v2.222A1.111 1.111 0 0 1 11.889 13H4.11A1.111 1.111 0 0 1 3 11.889V9.667M5.222 6.889 8 9.667l2.778-2.778M8 9.667V3"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13 9.667v2.222A1.111 1.111 0 0 1 11.889 13H4.11A1.111 1.111 0 0 1 3 11.889V9.667M5.222 6.889 8 9.667l2.778-2.778M8 9.667V3"/></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M13 4C13.5523 4 14 4.44772 14 5V11C14 11.5523 13.5523 12 13 12H3C2.44772 12 2 11.5523 2 11V5C2 4.44772 2.44772 4 3 4H13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
- <path d="M13.5 5L7.9999 8.5L2.5 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M13 4C13.5523 4 14 4.44772 14 5V11C14 11.5523 13.5523 12 13 12H3C2.44772 12 2 11.5523 2 11V5C2 4.44772 2.44772 4 3 4H13Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M13.5 5L7.9999 8.5L2.5 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m5.015 12.983-2.567-2.382c-.597-.554-.597-1.385 0-1.884L8.179 3.4c.597-.554 1.493-.554 2.03 0L13.552 6.5c.597.554.597 1.385 0 1.884l-4.955 4.598M14 12.983H5M4.5 7.483l5 5"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m5.015 12.983-2.567-2.382c-.597-.554-.597-1.385 0-1.884L8.179 3.4c.597-.554 1.493-.554 2.03 0L13.552 6.5c.597.554.597 1.385 0 1.884l-4.955 4.598M14 12.983H5M4.5 7.483l5 5"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 6V3h3M3 3l5 5M8 3a5 5 0 1 1-5 5"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3 6V3h3M3 3l5 5M8 3a5 5 0 1 1-5 5"/></svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.437 11.0461L13.4831 8L10.437 4.95392" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 8L8 8" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.6553 13.4659H4.21843C3.89528 13.4659 3.58537 13.3375 3.35687 13.109C3.12837 12.8805 3 12.5706 3 12.2475V3.71843C3 3.39528 3.12837 3.08537 3.35687 2.85687C3.58537 2.62837 3.89528 2.5 4.21843 2.5H6.6553" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.437 11.0461L13.4831 8L10.437 4.95392" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 8L8 8" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.6553 13.4659H4.21843C3.89528 13.4659 3.58537 13.3375 3.35687 13.109C3.12837 12.8805 3 12.5706 3 12.2475V3.71843C3 3.39528 3.12837 3.08537 3.35687 2.85687C3.58537 2.62837 3.89528 2.5 4.21843 2.5H6.6553" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.1998 9.60002L7.9998 12.8M7.9998 12.8L4.7998 9.60002M7.9998 12.8V6.40002" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.33325 3.73334H10.6666" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M11.1998 9.60002L7.9998 12.8M7.9998 12.8L4.7998 9.60002M7.9998 12.8V6.40002" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.33325 3.73334H10.6666" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.80005 6.93334L8.00005 3.73334M8.00005 3.73334L11.2 6.93334M8.00005 3.73334V10.1333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.3335 12.8H10.6668" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M4.80005 6.93334L8.00005 3.73334M8.00005 3.73334L11.2 6.93334M8.00005 3.73334V10.1333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.3335 12.8H10.6668" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" clip-path="url(#a)"><path d="M8 14.667v-4M8 5.333v-4M2.667 8H1.333M6.667 8H5.333M10.667 8H9.333M14.667 8h-1.334M10 12.667l-2 2-2-2M10 3.333l-2-2-2 2"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="M8 14.667v-4M8 5.333v-4M2.667 8H1.333M6.667 8H5.333M10.667 8H9.333M14.667 8h-1.334M10 12.667l-2 2-2-2M10 3.333l-2-2-2 2"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.0375 8.2088C1.9875 8.07409 1.9875 7.92592 2.0375 7.79122C2.5245 6.61039 3.35114 5.60076 4.41264 4.89031C5.47414 4.17986 6.72268 3.8006 7.99999 3.8006C9.2773 3.8006 10.5258 4.17986 11.5873 4.89031C12.6488 5.60076 13.4755 6.61039 13.9625 7.79122C14.0125 7.92592 14.0125 8.07409 13.9625 8.2088C13.4755 9.38962 12.6488 10.3993 11.5873 11.1097C10.5258 11.8202 9.2773 12.1994 7.99999 12.1994C6.72268 12.1994 5.47414 11.8202 4.41264 11.1097C3.35114 10.3993 2.5245 9.38962 2.0375 8.2088Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.0001 9.79988C8.99416 9.79988 9.80001 8.99404 9.80001 7.99998C9.80001 7.00592 8.99416 6.20007 8.0001 6.20007C7.00604 6.20007 6.2002 7.00592 6.2002 7.99998C6.2002 8.99404 7.00604 9.79988 8.0001 9.79988Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.0375 8.2088C1.9875 8.07409 1.9875 7.92592 2.0375 7.79122C2.5245 6.61039 3.35114 5.60076 4.41264 4.89031C5.47414 4.17986 6.72268 3.8006 7.99999 3.8006C9.2773 3.8006 10.5258 4.17986 11.5873 4.89031C12.6488 5.60076 13.4755 6.61039 13.9625 7.79122C14.0125 7.92592 14.0125 8.07409 13.9625 8.2088C13.4755 9.38962 12.6488 10.3993 11.5873 11.1097C10.5258 11.8202 9.2773 12.1994 7.99999 12.1994C6.72268 12.1994 5.47414 11.8202 4.41264 11.1097C3.35114 10.3993 2.5245 9.38962 2.0375 8.2088Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.0001 9.79988C8.99416 9.79988 9.80001 8.99404 9.80001 7.99998C9.80001 7.00592 8.99416 6.20007 8.0001 6.20007C7.00604 6.20007 6.2002 7.00592 6.2002 7.99998C6.2002 8.99404 7.00604 9.79988 8.0001 9.79988Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.875 2H4.25c-.332 0-.65.126-.884.351-.234.226-.366.53-.366.849v9.6c0 .318.132.623.366.849.235.225.552.351.884.351h7.5c.332 0 .65-.127.884-.351.234-.225.366-.53.366-.85V5L9.875 2Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 2v2.667A1.333 1.333 0 0 0 10.333 6H13"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M9.875 2H4.25c-.332 0-.65.126-.884.351-.234.226-.366.53-.366.849v9.6c0 .318.132.623.366.849.235.225.552.351.884.351h7.5c.332 0 .65-.127.884-.351.234-.225.366-.53.366-.85V5L9.875 2Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M9 2v2.667A1.333 1.333 0 0 0 10.333 6H13"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.8 8.3 5.6 9.8l1.2 1.5M9.2 8.3l1.2 1.5-1.2 1.5M9.2 2v2.4a1.2 1.2 0 0 0 1.2 1.2h2.4"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.8 2H4.4a1.2 1.2 0 0 0-1.2 1.2v9.6A1.2 1.2 0 0 0 4.4 14h7.2a1.2 1.2 0 0 0 1.2-1.2V5l-3-3Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.8 8.3 5.6 9.8l1.2 1.5M9.2 8.3l1.2 1.5-1.2 1.5M9.2 2v2.4a1.2 1.2 0 0 0 1.2 1.2h2.4"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M9.8 2H4.4a1.2 1.2 0 0 0-1.2 1.2v9.6A1.2 1.2 0 0 0 4.4 14h7.2a1.2 1.2 0 0 0 1.2-1.2V5l-3-3Z"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.8 2H4.4a1.2 1.2 0 0 0-1.2 1.2v9.6A1.2 1.2 0 0 0 4.4 14h7.2a1.2 1.2 0 0 0 1.2-1.2V5l-3-3ZM6.2 6.8h3.6M8 8.6V5M6.2 11h3.6"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M9.8 2H4.4a1.2 1.2 0 0 0-1.2 1.2v9.6A1.2 1.2 0 0 0 4.4 14h7.2a1.2 1.2 0 0 0 1.2-1.2V5l-3-3ZM6.2 6.8h3.6M8 8.6V5M6.2 11h3.6"/></svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13 11V11.8374C13 11.9431 12.9665 12.046 12.9044 12.1315L12.1498 13.1691C12.0557 13.2985 11.9054 13.375 11.7454 13.375H4.25461C4.09464 13.375 3.94433 13.2985 3.85024 13.1691L3.09563 12.1315C3.03348 12.046 3 11.9431 3 11.8374V3" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M13 11V11.8374C13 11.9431 12.9665 12.046 12.9044 12.1315L12.1498 13.1691C12.0557 13.2985 11.9054 13.375 11.7454 13.375H4.25461C4.09464 13.375 3.94433 13.2985 3.85024 13.1691L3.09563 12.1315C3.03348 12.046 3 11.9431 3 11.8374V3" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
<path d="M3 13V11L8 12H13V13H3Z" fill="black"/>
-<path d="M6.63246 3.04418C7.44914 3.31641 8 4.08069 8 4.94155V11.7306C8 12.0924 7.62757 12.3345 7.29693 12.1875L3.79693 10.632C3.61637 10.5518 3.5 10.3727 3.5 10.1751V2.69374C3.5 2.35246 3.83435 2.11148 4.15811 2.2194L6.63246 3.04418Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.5 3C8.67157 3 8 3.67157 8 4.5V13C8 12.1954 11.2366 12.0382 12.5017 12.0075C12.7778 12.0008 13 11.7761 13 11.5V3.5C13 3.22386 12.7761 3 12.5 3H9.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.63246 3.04418C7.44914 3.31641 8 4.08069 8 4.94155V11.7306C8 12.0924 7.62757 12.3345 7.29693 12.1875L3.79693 10.632C3.61637 10.5518 3.5 10.3727 3.5 10.1751V2.69374C3.5 2.35246 3.83435 2.11148 4.15811 2.2194L6.63246 3.04418Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 3C8.67157 3 8 3.67157 8 4.5V13C8 12.1954 11.2366 12.0382 12.5017 12.0075C12.7778 12.0008 13 11.7761 13 11.5V3.5C13 3.22386 12.7761 3 12.5 3H9.5Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 5H11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M3 8H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M3 11H9" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M3 5H11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M3 8H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M3 11H9" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 13C6.10457 13 7 12.1046 7 11C7 9.89543 6.10457 9 5 9C3.89543 9 3 9.89543 3 11C3 12.1046 3.89543 13 5 13Z" stroke="black" stroke-width="1.5"/>
-<path d="M11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5C9 6.10457 9.89543 7 11 7Z" fill="black" stroke="black" stroke-width="1.5"/>
-<path d="M4.625 3.625V8.375" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M11 7C11 9.20914 9.20914 11 7 11" stroke="black" stroke-width="1.5"/>
+<path d="M5 13C6.10457 13 7 12.1046 7 11C7 9.89543 6.10457 9 5 9C3.89543 9 3 9.89543 3 11C3 12.1046 3.89543 13 5 13Z" stroke="black" stroke-width="1.2"/>
+<path d="M11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5C9 6.10457 9.89543 7 11 7Z" fill="black" stroke="black" stroke-width="1.2"/>
+<path d="M4.625 3.625V8.375" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M11 7C11 9.20914 9.20914 11 7 11" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M10.5 8.75V10.5C8.43097 10.5 7.56903 10.5 5.5 10.5V10L10.5 6V5.5H5.5V7.25" stroke="black" stroke-width="1.5"/>
+ <path d="M10.5 8.75V10.5C8.43097 10.5 7.56903 10.5 5.5 10.5V10L10.5 6V5.5H5.5V7.25" stroke="black" stroke-width="1.2"/>
<path d="M1.5 8.5C1.77614 8.5 2 8.27614 2 8C2 7.72386 1.77614 7.5 1.5 7.5C1.22386 7.5 1 7.72386 1 8C1 8.27614 1.22386 8.5 1.5 8.5Z" fill="black"/>
<path d="M2.49976 6.33002C2.7759 6.33002 2.99976 6.10616 2.99976 5.83002C2.99976 5.55387 2.7759 5.33002 2.49976 5.33002C2.22361 5.33002 1.99976 5.55387 1.99976 5.83002C1.99976 6.10616 2.22361 6.33002 2.49976 6.33002Z" fill="black"/>
<path d="M2.49976 10.66C2.7759 10.66 2.99976 10.4361 2.99976 10.16C2.99976 9.88383 2.7759 9.65997 2.49976 9.65997C2.22361 9.65997 1.99976 9.88383 1.99976 10.16C1.99976 10.4361 2.22361 10.66 2.49976 10.66Z" fill="black"/>
@@ -1,8 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.5 5V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.5 3V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.5 5.33334V10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.5 4V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 6.66666V8.66666" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.5 5V11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.5 3V13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 5.33334V10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.5 4V12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5 6.66666V8.66666" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13 11V11.8374C13 11.9431 12.9665 12.046 12.9044 12.1315L12.1498 13.1691C12.0557 13.2985 11.9054 13.375 11.7454 13.375H4.25461C4.09464 13.375 3.94433 13.2985 3.85024 13.1691L3.09563 12.1315C3.03348 12.046 3 11.9431 3 11.8374V3" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M13 11V11.8374C13 11.9431 12.9665 12.046 12.9044 12.1315L12.1498 13.1691C12.0557 13.2985 11.9054 13.375 11.7454 13.375H4.25461C4.09464 13.375 3.94433 13.2985 3.85024 13.1691L3.09563 12.1315C3.03348 12.046 3 11.9431 3 11.8374V3" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
<path d="M3 13V11L8 12H13V13H3Z" fill="black"/>
-<path d="M6.63246 3.04418C7.44914 3.31641 8 4.08069 8 4.94155V11.7306C8 12.0924 7.62757 12.3345 7.29693 12.1875L3.79693 10.632C3.61637 10.5518 3.5 10.3727 3.5 10.1751V2.69374C3.5 2.35246 3.83435 2.11148 4.15811 2.2194L6.63246 3.04418Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.5 3C8.67157 3 8 3.67157 8 4.5V13C8 12.1954 11.2366 12.0382 12.5017 12.0075C12.7778 12.0008 13 11.7761 13 11.5V3.5C13 3.22386 12.7761 3 12.5 3H9.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.63246 3.04418C7.44914 3.31641 8 4.08069 8 4.94155V11.7306C8 12.0924 7.62757 12.3345 7.29693 12.1875L3.79693 10.632C3.61637 10.5518 3.5 10.3727 3.5 10.1751V2.69374C3.5 2.35246 3.83435 2.11148 4.15811 2.2194L6.63246 3.04418Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 3C8.67157 3 8 3.67157 8 4.5V13C8 12.1954 11.2366 12.0382 12.5017 12.0075C12.7778 12.0008 13 11.7761 13 11.5V3.5C13 3.22386 12.7761 3 12.5 3H9.5Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M11.0426 5.32305L11.0426 5.32306L11.0457 5.32471C12.4534 6.05668 13.25 7.21804 13.25 8.42984C13.25 9.40862 12.7315 10.3471 11.7886 11.0652C10.845 11.7839 9.50819 12.25 8 12.25C6.49181 12.25 5.155 11.7839 4.21141 11.0652C3.2685 10.3471 2.75 9.40862 2.75 8.42984C2.75 7.21804 3.54655 6.05668 4.95426 5.32471L4.95427 5.32473L4.95849 5.3225C5.44976 5.06306 5.93128 4.79038 6.4063 4.50125C6.82126 4.25139 7.14467 4.05839 7.42857 3.92422C7.71398 3.78934 7.88783 3.75 8 3.75C8.28571 3.75 8.57685 3.89469 9.43489 4.41073L9.43488 4.41075L9.43944 4.41345C9.47377 4.43377 9.50881 4.45453 9.54456 4.47572C9.94472 4.71289 10.4345 5.00316 11.0426 5.32305Z" stroke="black" stroke-width="1.5"/>
+ <path d="M11.0426 5.32305L11.0426 5.32306L11.0457 5.32471C12.4534 6.05668 13.25 7.21804 13.25 8.42984C13.25 9.40862 12.7315 10.3471 11.7886 11.0652C10.845 11.7839 9.50819 12.25 8 12.25C6.49181 12.25 5.155 11.7839 4.21141 11.0652C3.2685 10.3471 2.75 9.40862 2.75 8.42984C2.75 7.21804 3.54655 6.05668 4.95426 5.32471L4.95427 5.32473L4.95849 5.3225C5.44976 5.06306 5.93128 4.79038 6.4063 4.50125C6.82126 4.25139 7.14467 4.05839 7.42857 3.92422C7.71398 3.78934 7.88783 3.75 8 3.75C8.28571 3.75 8.57685 3.89469 9.43489 4.41073L9.43488 4.41075L9.43944 4.41345C9.47377 4.43377 9.50881 4.45453 9.54456 4.47572C9.94472 4.71289 10.4345 5.00316 11.0426 5.32305Z" stroke="black" stroke-width="1.2"/>
<path d="M13 7C15.5 12.5 7.92993 15 3.92993 11" stroke="black" stroke-width="1.25"/>
<circle cx="6" cy="7.75" r="1" fill="black"/>
<circle cx="10" cy="7.75" r="1" fill="black"/>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.63281 6.66406L7.99344 9.89844L11.3672 6.66406" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.63281 6.66406L7.99344 9.89844L11.3672 6.66406" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.35938 4.63281L6.125 7.99344L9.35938 11.3672" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.35938 4.63281L6.125 7.99344L9.35938 11.3672" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.64062 4.64062L9.89062 8.00125L6.64062 11.375" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.64062 4.64062L9.89062 8.00125L6.64062 11.375" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.63281 9.36719L7.99344 6.13281L11.3672 9.36719" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.63281 9.36719L7.99344 6.13281L11.3672 9.36719" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.78125 3C3.90625 3 3.90625 4.5 3.90625 5.5C3.90625 6.5 3.40625 7.50106 2.40625 8C3.40625 8.50106 3.90625 9.5 3.90625 10.5C3.90625 11.5 3.90625 13 5.78125 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.2422 3C12.1172 3 12.1172 4.5 12.1172 5.5C12.1172 6.5 12.6172 7.50106 13.6172 8C12.6172 8.50106 12.1172 9.5 12.1172 10.5C12.1172 11.5 12.1172 13 10.2422 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.78125 3C3.90625 3 3.90625 4.5 3.90625 5.5C3.90625 6.5 3.40625 7.50106 2.40625 8C3.40625 8.50106 3.90625 9.5 3.90625 10.5C3.90625 11.5 3.90625 13 5.78125 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.2422 3C12.1172 3 12.1172 4.5 12.1172 5.5C12.1172 6.5 12.6172 7.50106 13.6172 8C12.6172 8.50106 12.1172 9.5 12.1172 10.5C12.1172 11.5 12.1172 13 10.2422 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3061 6.5778C11.2118 7.65229 5.59818 7.64305 3.55456 6.49718C3.34826 6.38151 3.00857 6.53238 3.02549 6.76829C3.25878 10.0209 5.09256 13 8.49998 13C11.8648 13 13.6714 10.1058 13.9591 6.91373C13.9819 6.66029 13.5325 6.46164 13.3061 6.5778Z" fill="black"/>
<path d="M10.0555 5.53646C10.4444 6.0547 11.9998 6.57297 12 4.49998C12.0002 2.42709 9.66679 2.94528 8.50013 4.49998C7.33348 6.05467 4.99991 6.57294 5 4.5C5.00009 2.42706 6.55548 2.94528 6.94443 3.46352" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
- <circle cx="4" cy="10.5" r="1.75" stroke="black" stroke-width="1.5"/>
+ <circle cx="4" cy="10.5" r="1.75" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.46115 9.43419C8.30678 9.43419 9.92229 8.43411 9.92229 6.21171C9.92229 3.98933 8.30678 2.98926 6.46115 2.98926C4.61553 2.98926 3 3.98933 3 6.21171C3 7.028 3.21794 7.67935 3.58519 8.17685C3.7184 8.35732 3.69033 8.77795 3.58387 8.97539C3.32908 9.44793 3.81048 9.9657 4.33372 9.84571C4.72539 9.75597 5.13621 9.63447 5.49574 9.4715C5.62736 9.41181 5.7727 9.38777 5.91631 9.40402C6.09471 9.42416 6.27678 9.43419 6.46115 9.43419Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.3385 7.24835C12.7049 7.74561 12.9224 8.39641 12.9224 9.2117C12.9224 10.028 12.7044 10.6793 12.3372 11.1768C12.204 11.3573 12.232 11.7779 12.3385 11.9754C12.5933 12.4479 12.1119 12.9657 11.5886 12.8457C11.197 12.756 10.7862 12.6345 10.4266 12.4715C10.295 12.4118 10.1497 12.3878 10.0061 12.404C9.82765 12.4242 9.64558 12.4342 9.46121 12.4342C8.61469 12.4342 7.81658 12.2238 7.20055 11.7816" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.3385 7.24835C12.7049 7.74561 12.9224 8.39641 12.9224 9.2117C12.9224 10.028 12.7044 10.6793 12.3372 11.1768C12.204 11.3573 12.232 11.7779 12.3385 11.9754C12.5933 12.4479 12.1119 12.9657 11.5886 12.8457C11.197 12.756 10.7862 12.6345 10.4266 12.4715C10.295 12.4118 10.1497 12.3878 10.0061 12.404C9.82765 12.4242 9.64558 12.4342 9.46121 12.4342C8.61469 12.4342 7.81658 12.2238 7.20055 11.7816" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M12 11.25H11.25V12V13.25H7.31066L2.35809 8.29743L3.60151 4.56717L7.81937 2.88003L13.25 8.31066V11.25H12Z" stroke="black" stroke-width="1.5"/>
+ <path d="M12 11.25H11.25V12V13.25H7.31066L2.35809 8.29743L3.60151 4.56717L7.81937 2.88003L13.25 8.31066V11.25H12Z" stroke="black" stroke-width="1.2"/>
<path d="M10.928 11.4328L3.5 4.5H9.89645C9.96275 4.5 10.0263 4.52634 10.0732 4.57322L13.4268 7.92678C13.4737 7.97366 13.5 8.03725 13.5 8.10355V11.25C13.5 11.3881 13.3881 11.5 13.25 11.5H11.0985C11.0352 11.5 10.9743 11.476 10.928 11.4328Z" fill="black"/>
<path d="M4 11L4.5 5C3.97221 4.7361 3.33305 5.00085 3.14645 5.56066L2.19544 8.41368C2.07566 8.77302 2.16918 9.16918 2.43702 9.43702L4 11Z" fill="black"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.99993 6.85713C11.1558 6.85713 13.7142 5.83379 13.7142 4.57142C13.7142 3.30905 11.1558 2.28571 7.99993 2.28571C4.84402 2.28571 2.28564 3.30905 2.28564 4.57142C2.28564 5.83379 4.84402 6.85713 7.99993 6.85713Z" fill="black" stroke="black" stroke-width="1.5"/>
-<path d="M13.7142 4.57141V11.4286C13.7142 12.691 11.1558 13.7143 7.99993 13.7143C4.84402 13.7143 2.28564 12.691 2.28564 11.4286V4.57141" stroke="black" stroke-width="1.5"/>
-<path d="M13.7142 8C13.7142 9.26237 11.1558 10.2857 7.99993 10.2857C4.84402 10.2857 2.28564 9.26237 2.28564 8" stroke="black" stroke-width="1.5"/>
+<path d="M7.99993 6.85713C11.1558 6.85713 13.7142 5.83379 13.7142 4.57142C13.7142 3.30905 11.1558 2.28571 7.99993 2.28571C4.84402 2.28571 2.28564 3.30905 2.28564 4.57142C2.28564 5.83379 4.84402 6.85713 7.99993 6.85713Z" fill="black" stroke="black" stroke-width="1.2"/>
+<path d="M13.7142 4.57141V11.4286C13.7142 12.691 11.1558 13.7143 7.99993 13.7143C4.84402 13.7143 2.28564 12.691 2.28564 11.4286V4.57141" stroke="black" stroke-width="1.2"/>
+<path d="M13.7142 8C13.7142 9.26237 11.1558 10.2857 7.99993 10.2857C4.84402 10.2857 2.28564 9.26237 2.28564 8" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.5 3L8.5 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 6.5H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 13H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.5 3L8.5 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 6.5H12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 13H12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M13.5413 8.31248C13.6529 8.11911 13.6529 7.88086 13.5413 7.68748L11.0413 3.35736C10.9296 3.16398 10.7233 3.04486 10.5 3.04486H5.5C5.27671 3.04486 5.07038 3.16398 4.95873 3.35736L2.45873 7.68748C2.34709 7.88086 2.34709 8.11911 2.45873 8.31248L4.95873 12.6426C5.07038 12.836 5.27671 12.9551 5.5 12.9551H10.5C10.7233 12.9551 10.9296 12.836 11.0413 12.6426L13.5413 8.31248Z" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
+ <path d="M13.5413 8.31248C13.6529 8.11911 13.6529 7.88086 13.5413 7.68748L11.0413 3.35736C10.9296 3.16398 10.7233 3.04486 10.5 3.04486H5.5C5.27671 3.04486 5.07038 3.16398 4.95873 3.35736L2.45873 7.68748C2.34709 7.88086 2.34709 8.11911 2.45873 8.31248L4.95873 12.6426C5.07038 12.836 5.27671 12.9551 5.5 12.9551H10.5C10.7233 12.9551 10.9296 12.836 11.0413 12.6426L13.5413 8.31248Z" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
<path d="M7.74994 5.14432C7.90464 5.055 8.09524 5.055 8.24994 5.14432L10.348 6.35564C10.5027 6.44496 10.598 6.61002 10.598 6.78866V9.2113C10.598 9.38994 10.5027 9.555 10.348 9.64432L8.24994 10.8556C8.09524 10.945 7.90464 10.945 7.74994 10.8556L5.65186 9.64432C5.49716 9.555 5.40186 9.38994 5.40186 9.2113V6.78866C5.40186 6.61002 5.49716 6.44496 5.65186 6.35564L7.74994 5.14432Z" fill="black"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 5H11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M3 8H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M3 11H9" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M3 5H11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M3 8H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M3 11H9" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.26046 3.97337C8.3527 4.17617 8.4795 4.47151 8.57375 4.69341C8.65258 4.87898 8.83437 4.99999 9.03599 4.99999H12.5C12.7761 4.99999 13 5.22385 13 5.49999V12.125C13 12.4011 12.7761 12.625 12.5 12.625H3.5C3.22386 12.625 3 12.4011 3 12.125V3.86932C3 3.59318 3.22386 3.36932 3.5 3.36932H7.34219C7.74141 3.36932 8.09483 3.60924 8.26046 3.97337Z" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M8.26046 3.97337C8.3527 4.17617 8.4795 4.47151 8.57375 4.69341C8.65258 4.87898 8.83437 4.99999 9.03599 4.99999H12.5C12.7761 4.99999 13 5.22385 13 5.49999V12.125C13 12.4011 12.7761 12.625 12.5 12.625H3.5C3.22386 12.625 3 12.4011 3 12.125V3.86932C3 3.59318 3.22386 3.36932 3.5 3.36932H7.34219C7.74141 3.36932 8.09483 3.60924 8.26046 3.97337Z" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.42782 7.2487C4.43495 6.97194 4.65009 6.75 4.91441 6.75H13.5293C13.7935 6.75 14.007 6.97194 13.9998 7.2487C13.9628 8.6885 13.7533 12.75 12.5721 12.75H3.375C4.55631 12.75 4.3907 8.6885 4.42782 7.2487Z" fill="black" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
-<path d="M5.19598 12.625H3.66515C3.42618 12.625 3.22289 12.4453 3.18626 12.2017L1.94333 3.93602C1.89776 3.63295 2.12496 3.35938 2.42223 3.35938H5.78585C6.11241 3.35938 6.41702 3.52903 6.59618 3.81071L6.94517 4.35938H9.92811C10.4007 4.35938 10.8044 4.71102 10.8836 5.1917L11.1251 6.65624" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.42782 7.2487C4.43495 6.97194 4.65009 6.75 4.91441 6.75H13.5293C13.7935 6.75 14.007 6.97194 13.9998 7.2487C13.9628 8.6885 13.7533 12.75 12.5721 12.75H3.375C4.55631 12.75 4.3907 8.6885 4.42782 7.2487Z" fill="black" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
+<path d="M5.19598 12.625H3.66515C3.42618 12.625 3.22289 12.4453 3.18626 12.2017L1.94333 3.93602C1.89776 3.63295 2.12496 3.35938 2.42223 3.35938H5.78585C6.11241 3.35938 6.41702 3.52903 6.59618 3.81071L6.94517 4.35938H9.92811C10.4007 4.35938 10.8044 4.71102 10.8836 5.1917L11.1251 6.65624" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.3352 13.2519H12.375M4.49719 13.2519L8.00001 2.74811L11.5028 13.2519M3.625 13.2519H5.6648M9.74908 9.16761H6.25095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.3352 13.2519H12.375M4.49719 13.2519L8.00001 2.74811L11.5028 13.2519M3.625 13.2519H5.6648M9.74908 9.16761H6.25095" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 13C6.10457 13 7 12.1046 7 11C7 9.89543 6.10457 9 5 9C3.89543 9 3 9.89543 3 11C3 12.1046 3.89543 13 5 13Z" stroke="black" stroke-width="1.5"/>
-<path d="M11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5C9 6.10457 9.89543 7 11 7Z" fill="black" stroke="black" stroke-width="1.5"/>
-<path d="M4.625 3.625V8.375" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M11 7C11 9.20914 9.20914 11 7 11" stroke="black" stroke-width="1.5"/>
+<path d="M5 13C6.10457 13 7 12.1046 7 11C7 9.89543 6.10457 9 5 9C3.89543 9 3 9.89543 3 11C3 12.1046 3.89543 13 5 13Z" stroke="black" stroke-width="1.2"/>
+<path d="M11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5C9 6.10457 9.89543 7 11 7Z" fill="black" stroke="black" stroke-width="1.2"/>
+<path d="M4.625 3.625V8.375" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M11 7C11 9.20914 9.20914 11 7 11" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.3848 9.30444C7.3848 9.30444 7.53254 10.2646 8.53248 10.0882C9.53242 9.91193 9.36378 8.95549 9.36378 8.95549" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="#FF7676" stroke-opacity="0.52" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="#FF7676" stroke-opacity="0.52" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.54155 5.54157C6.12355 4.90104 6.01688 2.62541 7.22875 2.3985C8.44063 2.17158 9.19097 4.33148 9.91982 4.6814C10.6487 5.03133 12.8517 4.3028 13.4381 5.38734C14.0244 6.47188 12.1395 7.95973 12.026 8.64088C11.9126 9.32203 13.3614 11.2416 12.4675 12.1701C11.5736 13.0986 9.73005 11.7545 8.90486 11.8834C8.07966 12.0123 6.79244 13.9095 5.67367 13.3502C4.55491 12.7909 5.16702 10.5455 4.82437 9.87612C4.48171 9.20673 2.34028 8.54978 2.4525 7.35049C2.56471 6.15121 4.95956 6.1821 5.54155 5.54157Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="6.25098" cy="7.75" r="0.75" fill="black"/>
<circle cx="10.1035" cy="7.25" r="0.75" fill="black"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 5L10.5981 9.5H5.40192L8 5Z" stroke="black" stroke-width="1.5"/>
-<path d="M8 3L12.3301 5.5V10.5L8 13L3.66987 10.5V5.5L8 3Z" stroke="black" stroke-width="1.5"/>
+<path d="M8 5L10.5981 9.5H5.40192L8 5Z" stroke="black" stroke-width="1.2"/>
+<path d="M8 3L12.3301 5.5V10.5L8 13L3.66987 10.5V5.5L8 3Z" stroke="black" stroke-width="1.2"/>
<circle cx="3.5" cy="5.5" r="1" fill="black"/>
<circle cx="12.5" cy="5.5" r="1" fill="black"/>
<circle cx="8" cy="3" r="1" fill="black"/>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.2795 3.63849L8.7478 12.0142" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M7.26626 3.99597L4.73462 12.3717" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M4.15991 6.37988H12.9099" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M3.09839 9.62408H11.8484" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M11.2795 3.63849L8.7478 12.0142" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M7.26626 3.99597L4.73462 12.3717" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M4.15991 6.37988H12.9099" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M3.09839 9.62408H11.8484" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.5"/>
+<rect x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.2"/>
<path d="M6.74217 7.13317V4.26172V4.13672H6.61717H5.50781H5.38281V4.26172V8.8824V9.07567L5.55908 8.9964L6.33378 8.64803L6.33378 8.64803L6.33389 8.64798L6.33443 8.64774L6.3347 8.64762L6.3369 8.64667L6.34717 8.64225C6.35635 8.63832 6.37013 8.63248 6.38816 8.62501C6.42423 8.61006 6.47729 8.58857 6.54454 8.56272C6.67911 8.511 6.87014 8.44194 7.09531 8.3729C7.54765 8.2342 8.12948 8.09817 8.66592 8.09817C8.92095 8.09817 9.05676 8.16584 9.12979 8.241C9.2037 8.31708 9.23311 8.42118 9.23311 8.53361V11.7383V11.8633H9.35811H10.4922H10.6172V11.7383V8.53361V8.53322C10.6172 8.43725 10.6172 7.81276 10.1093 7.32276C9.86619 7.08825 9.42187 6.80777 8.69373 6.80777C8.00022 6.80777 7.28721 6.96253 6.74217 7.13317ZM8.45652 5.91932L8.29694 6.12171H8.55468H9.66094H9.71713L9.75443 6.07969C10.2672 5.502 10.5291 4.91889 10.616 4.27855L10.6353 4.13672H10.4922H9.38592H9.28153L9.26292 4.23944C9.1561 4.82914 8.88874 5.37113 8.45652 5.91932Z" fill="black" stroke="black" stroke-width="0.25"/>
<path d="M5.38281 11.7383V12.01L5.58915 11.8332L6.83447 10.766L6.94523 10.671L6.83447 10.5761L5.58915 9.50891L5.38281 9.33207V9.60382V11.7383Z" fill="black" stroke="black" stroke-width="0.25"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.15741 4.17108L6.84277 11.8289" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M4.74951 6L2.74951 8L4.74951 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.25 10L13.25 8L11.25 6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.15741 4.17108L6.84277 11.8289" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M4.74951 6L2.74951 8L4.74951 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.25 10L13.25 8L11.25 6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 4C7.91421 4 8.25 3.66421 8.25 3.25C8.25 2.83579 7.91421 2.5 7.5 2.5C7.08579 2.5 6.75 2.83579 6.75 3.25C6.75 3.66421 7.08579 4 7.5 4Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 8L10 6L12 8H8Z" fill="black"/>
-<path d="M3 11L6 8L8.375 10.375" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7 9L8.5 7.5L10 6L11.5 7.5L13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.375 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H8.35938M10.6406 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11.125" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M3 11L6 8L8.375 10.375" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7 9L8.5 7.5L10 6L11.5 7.5L13 9" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.375 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H8.35938M10.6406 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11.125" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.99219 8.56632C6.5 9 10.5415 8.99989 12 7.99995C13.4585 7 12.5 9.49999 12.5 10" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M11.5 13C9 13.5781 6 13.5938 4 13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M10.0156 10.9844C8.51562 11.2031 6.5 11.2031 5 10.8906" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M6.5625 6.5C6.34375 6 6.06838 4.93125 6.99999 4.03125C7.93161 3.13125 8.58082 3.33636 9.00002 2" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M9.18477 6.50002C8.88168 6.05002 8.40637 5.71875 9.00014 5.40625C9.5939 5.09375 10.3126 4.65625 10.8751 3.53125" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M4.99219 8.56632C6.5 9 10.5415 8.99989 12 7.99995C13.4585 7 12.5 9.49999 12.5 10" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M11.5 13C9 13.5781 6 13.5938 4 13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M10.0156 10.9844C8.51562 11.2031 6.5 11.2031 5 10.8906" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M6.5625 6.5C6.34375 6 6.06838 4.93125 6.99999 4.03125C7.93161 3.13125 8.58082 3.33636 9.00002 2" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M9.18477 6.50002C8.88168 6.05002 8.40637 5.71875 9.00014 5.40625C9.5939 5.09375 10.3126 4.65625 10.8751 3.53125" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.5"/>
+<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 6.5C3.25 5.80964 3.80964 5.25 4.5 5.25H11.5C12.1904 5.25 12.75 5.80964 12.75 6.5V12.5C12.75 13.1904 12.1904 13.75 11.5 13.75H4.5C3.80964 13.75 3.25 13.1904 3.25 12.5V6.5ZM8.75 9.66146C8.90559 9.48517 9 9.25361 9 9C9 8.44772 8.55228 8 8 8C7.44772 8 7 8.44772 7 9C7 9.25361 7.09441 9.48517 7.25 9.66146V11C7.25 11.4142 7.58579 11.75 8 11.75C8.41421 11.75 8.75 11.4142 8.75 11V9.66146Z" fill="black"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13 13L10.4138 10.4138M3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 13L10.4138 10.4138M3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,8 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.00005 4.76556L4.76569 2.74996M6.00005 4.76556L3.75 4.76563M6.00005 4.76556L7.25006 4.7656" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M10.0232 11.2311L11.2675 13.2406M10.0232 11.2311L12.2732 11.2199M10.0232 11.2311L8.7732 11.2373" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M9.99025 4.91551L10.9985 2.77781M9.99025 4.91551L8.75599 3.03419M9.99025 4.91551L10.6759 5.9607" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M6.0323 11.1009L5.03465 13.2436M6.0323 11.1009L7.27585 12.9761M6.0323 11.1009L5.34151 10.0592" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M11.883 8.19023L14.2466 8.19287M11.883 8.19023L13.0602 6.27268M11.883 8.19023L11.229 9.25547" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M4.12354 7.8356L1.76002 7.84465M4.12354 7.8356L2.95585 9.75894M4.12354 7.8356L4.7723 6.76713" stroke="black" stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M6.00005 4.76556L4.76569 2.74996M6.00005 4.76556L3.75 4.76563M6.00005 4.76556L7.25006 4.7656" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M10.0232 11.2311L11.2675 13.2406M10.0232 11.2311L12.2732 11.2199M10.0232 11.2311L8.7732 11.2373" stroke="black" stroke-opacity="0.5" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M9.99025 4.91551L10.9985 2.77781M9.99025 4.91551L8.75599 3.03419M9.99025 4.91551L10.6759 5.9607" stroke="black" stroke-opacity="0.5" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M6.0323 11.1009L5.03465 13.2436M6.0323 11.1009L7.27585 12.9761M6.0323 11.1009L5.34151 10.0592" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M11.883 8.19023L14.2466 8.19287M11.883 8.19023L13.0602 6.27268M11.883 8.19023L11.229 9.25547" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M4.12354 7.8356L1.76002 7.84465M4.12354 7.8356L2.95585 9.75894M4.12354 7.8356L4.7723 6.76713" stroke="black" stroke-opacity="0.5" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,8 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.03125 3.96875C3.03125 3.41647 3.47897 2.96875 4.03125 2.96875H6V13H4.03125C3.47897 13 3.03125 12.5523 3.03125 12V3.96875Z" fill="black"/>
-<path d="M12.5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H12.5C12.7761 13 13 12.7761 13 12.5V3.5C13 3.22386 12.7761 3 12.5 3Z" stroke="black" stroke-width="1.5"/>
-<path d="M10.5 5.75H8.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.5 8H8.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.5 10.25H8.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 3V14" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H12.5C12.7761 13 13 12.7761 13 12.5V3.5C13 3.22386 12.7761 3 12.5 3Z" stroke="black" stroke-width="1.2"/>
+<path d="M10.5 5.75H8.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.5 8H8.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.5 10.25H8.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 3V14" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.62671 4.88474L7.99977 7.78519M2.62671 4.88474L2.63131 10.9001L8.00436 13.8005M2.62671 4.88474L5.31111 3.54213M7.99977 7.78519L8.00436 13.8005M7.99977 7.78519L10.6841 6.33086M8.00436 13.8005L13.3729 10.8919L13.3683 4.87654M5.31111 3.54213L7.9955 2.19952L13.3683 4.87654M5.31111 3.54213L10.6841 6.33086M10.6841 6.33086L13.3683 4.87654" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.62671 4.88474L7.99977 7.78519M2.62671 4.88474L2.63131 10.9001L8.00436 13.8005M2.62671 4.88474L5.31111 3.54213M7.99977 7.78519L8.00436 13.8005M7.99977 7.78519L10.6841 6.33086M8.00436 13.8005L13.3729 10.8919L13.3683 4.87654M5.31111 3.54213L7.9955 2.19952L13.3683 4.87654M5.31111 3.54213L10.6841 6.33086M10.6841 6.33086L13.3683 4.87654" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.03125 13.5625V7.78125L2.5625 4.9375V10.75L8.03125 13.5625Z" fill="black"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M13 9C13 8.32138 12.9375 7.5 12.7188 6.75C12.0625 7.53125 10.875 8.1875 10 8.5C10.75 5.90625 9.5625 3.1875 8 3C8 4.96875 7.625 5.90625 6.5 7.5C5 5 3.5 6.5 3 7C3.5 7.5 4.21832 8.24064 4.34375 9.3125C4.6875 12.25 6.75 13 8.5 13C10.25 13 10.5 11 12.5 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M13 9C13 8.32138 12.9375 7.5 12.7188 6.75C12.0625 7.53125 10.875 8.1875 10 8.5C10.75 5.90625 9.5625 3.1875 8 3C8 4.96875 7.625 5.90625 6.5 7.5C5 5 3.5 6.5 3 7C3.5 7.5 4.21832 8.24064 4.34375 9.3125C4.6875 12.25 6.75 13 8.5 13C10.25 13 10.5 11 12.5 12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.03125 9.15625C5.87694 9.15625 6.5625 8.47069 6.5625 7.625C6.5625 6.77931 5.87694 6.09375 5.03125 6.09375C4.18556 6.09375 3.5 6.77931 3.5 7.625C3.5 8.47069 4.18556 9.15625 5.03125 9.15625Z" fill="black"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 4V12M12 8H4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M8 4V12M12 8H4" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,12 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M3 3.86328H9.51563" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
- <path d="M12 3.86328H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
- <path d="M10.6406 6.62628H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
- <path d="M5.79688 6.62628H8.15625" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
- <path d="M3 6.62628H3.35937" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
- <path d="M8.15625 9.37372H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
- <path d="M3 9.37372H5.64062" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
- <path d="M3 12.1094H4.54687" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
- <path d="M6.97656 12.1094H9.35938" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
- <path d="M11.8203 12.1094H13" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+ <path d="M3 3.86328H9.51563" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+ <path d="M12 3.86328H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+ <path d="M10.6406 6.62628H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+ <path d="M5.79688 6.62628H8.15625" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+ <path d="M3 6.62628H3.35937" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+ <path d="M8.15625 9.37372H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+ <path d="M3 9.37372H5.64062" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+ <path d="M3 12.1094H4.54687" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+ <path d="M6.97656 12.1094H9.35938" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+ <path d="M11.8203 12.1094H13" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.03125 3V3.03125M3.03125 3.03125V9M3.03125 3.03125C3.03125 5 6 5 6 5M3.03125 9C3.03125 11 6 11 6 11M3.03125 9V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.03125 3V3.03125M3.03125 3.03125V9M3.03125 3.03125C3.03125 5 6 5 6 5M3.03125 9C3.03125 11 6 11 6 11M3.03125 9V12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="8" y="2.5" width="6" height="5" rx="1.5" fill="black"/>
<rect x="8" y="8.46875" width="6" height="5.0625" rx="1.5" fill="black"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.18452 2.91638C6.01625 2.91638 4.98489 3.77623 4.91991 4.94678H4.72024C3.81569 4.94678 3 5.63731 3 6.58698V8.10978C3 9.05945 3.81569 9.74998 4.72024 9.74998H5.33631C5.67376 9.74998 6.02976 9.48559 6.02976 9.06153C6.02976 8.46056 6.51694 7.97338 7.11791 7.97338H8.27976C9.18431 7.97338 10 7.28286 10 6.33318V5.06418C10 3.83417 8.93913 2.91638 7.73214 2.91638H7.18452Z" stroke="black" stroke-width="1.5"/>
-<path d="M8.79613 13.0836C9.97889 13.0836 11.0103 12.2025 11.0702 11.0191H11.2738C12.1885 11.0191 13 10.3146 13 9.36187V7.8135C13 6.86077 12.1885 6.15625 11.2738 6.15625H10.6544C10.3099 6.15625 9.96057 6.42749 9.96057 6.84577C9.96057 7.46262 9.46051 7.96268 8.84365 7.96268H7.69494C6.78027 7.96268 5.96875 8.6672 5.96875 9.61993V10.9102C5.96875 12.148 7.02678 13.0836 8.24554 13.0836H8.79613Z" stroke="black" stroke-width="1.5"/>
+<path d="M7.18452 2.91638C6.01625 2.91638 4.98489 3.77623 4.91991 4.94678H4.72024C3.81569 4.94678 3 5.63731 3 6.58698V8.10978C3 9.05945 3.81569 9.74998 4.72024 9.74998H5.33631C5.67376 9.74998 6.02976 9.48559 6.02976 9.06153C6.02976 8.46056 6.51694 7.97338 7.11791 7.97338H8.27976C9.18431 7.97338 10 7.28286 10 6.33318V5.06418C10 3.83417 8.93913 2.91638 7.73214 2.91638H7.18452Z" stroke="black" stroke-width="1.2"/>
+<path d="M8.79613 13.0836C9.97889 13.0836 11.0103 12.2025 11.0702 11.0191H11.2738C12.1885 11.0191 13 10.3146 13 9.36187V7.8135C13 6.86077 12.1885 6.15625 11.2738 6.15625H10.6544C10.3099 6.15625 9.96057 6.42749 9.96057 6.84577C9.96057 7.46262 9.46051 7.96268 8.84365 7.96268H7.69494C6.78027 7.96268 5.96875 8.6672 5.96875 9.61993V10.9102C5.96875 12.148 7.02678 13.0836 8.24554 13.0836H8.79613Z" stroke="black" stroke-width="1.2"/>
<path d="M7.20312 6.01758C7.64323 6.01758 8 5.6608 8 5.2207C8 4.7806 7.64323 4.42383 7.20312 4.42383C6.76302 4.42383 6.40625 4.7806 6.40625 5.2207C6.40625 5.6608 6.76302 6.01758 7.20312 6.01758Z" fill="black"/>
<path d="M8.79687 11.5939C9.23698 11.5939 9.59375 11.2372 9.59375 10.7971C9.59375 10.357 9.23698 10.0002 8.79687 10.0002C8.35677 10.0002 8 10.357 8 10.7971C8 11.2372 8.35677 11.5939 8.79687 11.5939Z" fill="black"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.99988 13C5.97267 13 4.22723 11.7936 3.44238 10.0595M7.99988 3C10.1122 3 11.9185 4.30981 12.6511 6.16152" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99988 13C5.97267 13 4.22723 11.7936 3.44238 10.0595M7.99988 3C10.1122 3 11.9185 4.30981 12.6511 6.16152" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.65625 3.29688C3.00143 3.29688 3.28125 3.01705 3.28125 2.67188C3.28125 2.3267 3.00143 2.04688 2.65625 2.04688C2.31107 2.04688 2.03125 2.3267 2.03125 2.67188C2.03125 3.01705 2.31107 3.29688 2.65625 3.29688Z" fill="black"/>
<path d="M4.71094 3.29688C5.05612 3.29688 5.33594 3.01705 5.33594 2.67188C5.33594 2.3267 5.05612 2.04688 4.71094 2.04688C4.36576 2.04688 4.08594 2.3267 4.08594 2.67188C4.08594 3.01705 4.36576 3.29688 4.71094 3.29688Z" fill="black"/>
<path d="M5.96094 4.99219C6.30612 4.99219 6.58594 4.71237 6.58594 4.36719C6.58594 4.02201 6.30612 3.74219 5.96094 3.74219C5.61576 3.74219 5.33594 4.02201 5.33594 4.36719C5.33594 4.71237 5.61576 4.99219 5.96094 4.99219Z" fill="black"/>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M8.5 6.88672C10.433 6.88672 12 8.45372 12 10.3867V13M12 13L14 11M12 13L10 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M8.5 6.88672C10.433 6.88672 12 8.45372 12 10.3867V13M12 13L14 11M12 13L10 11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.27935 10.9821C5.32063 10.4038 4.9204 9.89049 4.35998 9.80276L3.60081 9.68387C3.37979 9.64945 3.20167 9.48001 3.15225 9.25614L3.01378 8.63511C2.96382 8.41235 3.05233 8.1807 3.23696 8.05125L3.8631 7.61242C4.33337 7.28297 4.47456 6.6369 4.18621 6.13364L3.79467 5.45092C3.68118 5.25261 3.69801 5.00374 3.83757 4.82321L4.22314 4.32436C4.3627 4.14438 4.59621 4.06994 4.81071 4.13772L5.57531 4.37769C6.11944 4.54879 6.70048 4.26159 6.90683 3.71886L7.1811 2.99782C7.26255 2.78395 7.46345 2.64285 7.68772 2.6423L8.31007 2.64063C8.53434 2.64007 8.73579 2.78006 8.81834 2.99337L9.09965 3.72275C9.30821 4.26214 9.88655 4.54712 10.429 4.37714L11.1632 4.14716C11.3772 4.07994 11.6096 4.15382 11.7492 4.3327L12.1374 4.83099" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.27935 10.9821C5.32063 10.4038 4.9204 9.89049 4.35998 9.80276L3.60081 9.68387C3.37979 9.64945 3.20167 9.48001 3.15225 9.25614L3.01378 8.63511C2.96382 8.41235 3.05233 8.1807 3.23696 8.05125L3.8631 7.61242C4.33337 7.28297 4.47456 6.6369 4.18621 6.13364L3.79467 5.45092C3.68118 5.25261 3.69801 5.00374 3.83757 4.82321L4.22314 4.32436C4.3627 4.14438 4.59621 4.06994 4.81071 4.13772L5.57531 4.37769C6.11944 4.54879 6.70048 4.26159 6.90683 3.71886L7.1811 2.99782C7.26255 2.78395 7.46345 2.64285 7.68772 2.6423L8.31007 2.64063C8.53434 2.64007 8.73579 2.78006 8.81834 2.99337L9.09965 3.72275C9.30821 4.26214 9.88655 4.54712 10.429 4.37714L11.1632 4.14716C11.3772 4.07994 11.6096 4.15382 11.7492 4.3327L12.1374 4.83099" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5827 1.27457C10.4978 2.48798 6.32365 2.94703 3.49893 2.99561C3.22283 3.00036 3.00001 3.22384 3.00001 3.49998L3.00002 6.00003C3.00002 6.27617 3.22284 6.50038 3.49895 6.49563C6.42334 6.44533 10.7942 5.95506 12.7954 4.64327C12.927 4.557 13 4.40741 13 4.25004L13 1.50027C13 1.29426 12.7607 1.17094 12.5827 1.27457Z" fill="black"/>
<path d="M12.3072 6.51584C12.6851 6.6855 12.8539 7.12936 12.6842 7.50724C12.5145 7.88511 12.0707 8.05391 11.6928 7.88425L12.3072 6.51584ZM3 5.02142C4.32178 5.02142 6.01669 5.1159 7.68605 5.34579C9.34359 5.57406 11.0313 5.94302 12.3072 6.51584L11.6928 7.88425C10.611 7.39853 9.08921 7.05318 7.48142 6.83177C5.88546 6.61199 4.25922 6.52142 3 6.52142L3 5.02142Z" fill="black"/>
-<path d="M3 10.0214C5.581 10.0214 9.64229 10.3915 12 11.45" stroke="black" stroke-width="1.5"/>
+<path d="M3 10.0214C5.581 10.0214 9.64229 10.3915 12 11.45" stroke="black" stroke-width="1.2"/>
<path d="M12.1401 10.0067C9.94879 11.0472 6.13586 11.4503 3.49893 11.4956C3.22283 11.5004 3.00001 11.7238 3.00001 12L3.00002 14.5C3.00002 14.7762 3.22284 15.0004 3.49895 14.9956C6.42334 14.9453 10.7942 14.4551 12.7954 13.1433C12.927 13.057 13 12.9074 13 12.75L13 10.5002C13 10.0882 12.5123 9.82995 12.1401 10.0067Z" fill="black"/>
<path d="M12.1401 5.75668C9.94879 6.7972 6.13586 7.20026 3.49893 7.24561C3.22283 7.25036 3.00001 7.47384 3.00001 7.74998L3.00002 10.25C3.00002 10.5262 3.22284 10.7504 3.49895 10.7456C6.42334 10.6953 10.7942 10.2051 12.7954 8.89327C12.927 8.807 13 8.65741 13 8.50004L13 6.25023C13 5.83821 12.5123 5.57995 12.1401 5.75668Z" fill="black"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.03125 13C3.46875 12.2812 3.68556 12.0378 4.0625 11.5312M4.0625 11.5312C4.0625 9.86595 4.27768 8.02844 4.75687 7M4.0625 11.5312C4.75687 10.5981 6.6875 8.57812 7.92188 7.54688M4.0625 11.5312C7.875 11.5312 10.0507 9.46738 11.4062 8.03125C11.5818 7.84528 11.2307 7.34164 10.9157 6.96235C10.7718 6.78906 10.8964 6.50073 11.1213 6.48823C11.6657 6.45798 12.3874 6.36175 12.5 6.06684C12.7544 5.4003 12.9585 4.2437 13.0409 3.28832C13.0541 3.13644 12.9264 3.01119 12.7745 3.0243C10.5824 3.21343 8.22052 3.5262 6.5 4.82764" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M3.03125 13C3.46875 12.2812 3.68556 12.0378 4.0625 11.5312M4.0625 11.5312C4.0625 9.86595 4.27768 8.02844 4.75687 7M4.0625 11.5312C4.75687 10.5981 6.6875 8.57812 7.92188 7.54688M4.0625 11.5312C7.875 11.5312 10.0507 9.46738 11.4062 8.03125C11.5818 7.84528 11.2307 7.34164 10.9157 6.96235C10.7718 6.78906 10.8964 6.50073 11.1213 6.48823C11.6657 6.45798 12.3874 6.36175 12.5 6.06684C12.7544 5.4003 12.9585 4.2437 13.0409 3.28832C13.0541 3.13644 12.9264 3.01119 12.7745 3.0243C10.5824 3.21343 8.22052 3.5262 6.5 4.82764" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6 6H10" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M8 6V11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H5M11 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M6 6H10" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M8 6V11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H5M11 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2.65625 3H12.8437C13.1199 3 13.3438 3.22386 13.3438 3.5V10.3438M13.3438 13H3.15625C2.88011 13 2.65625 12.7761 2.65625 12.5V5.65625" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M10 8.01562L6.65625 10.3125V5.6875L10 8.01562Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.65625 3H12.8437C13.1199 3 13.3438 3.22386 13.3438 3.5V10.3438M13.3438 13H3.15625C2.88011 13 2.65625 12.7761 2.65625 12.5V5.65625" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M10 8.01562L6.65625 10.3125V5.6875L10 8.01562Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 9.13502L4.578 3.21202L4.47016 3.02509C4.42551 2.9477 4.34295 2.90002 4.25361 2.90002H2.43302C2.24057 2.90002 2.12029 3.10836 2.21651 3.27503L7.7835 12.917C7.87972 13.0837 8.12028 13.0837 8.2165 12.917L13.7835 3.27503C13.8797 3.10836 13.7594 2.90002 13.567 2.90002H11.7443C11.655 2.90002 11.5725 2.94767 11.5278 3.02502L8 9.13502Z" fill="black"/>
-<path d="M3.5 3.65002H6.80469L8 5.73596L9.20312 3.65002H12.5234" stroke="black" stroke-width="1.5"/>
+<path d="M3.5 3.65002H6.80469L8 5.73596L9.20312 3.65002H12.5234" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.5"/>
+<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 6.5C3.25 5.80964 3.80964 5.25 4.5 5.25H11.5C12.1904 5.25 12.75 5.80964 12.75 6.5V12.5C12.75 13.1904 12.1904 13.75 11.5 13.75H4.5C3.80964 13.75 3.25 13.1904 3.25 12.5V6.5ZM8.75 9.66146C8.90559 9.48517 9 9.25361 9 9C9 8.44772 8.55228 8 8 8C7.44772 8 7 8.44772 7 9C7 9.25361 7.09441 9.48517 7.25 9.66146V11C7.25 11.4142 7.58579 11.75 8 11.75C8.41421 11.75 8.75 11.4142 8.75 11V9.66146Z" fill="black"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" clip-path="url(#a)"><path d="M13 13.5v-8L9.5 2h-6a.5.5 0 0 0-.5.5V6"/><path d="M9 2v4h4M8 9.5V13h1a1.75 1.75 0 0 0 0-3.5H8ZM6 13V9.5L4.25 12 2.5 9.5V13"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="M13 13.5v-8L9.5 2h-6a.5.5 0 0 0-.5.5V6"/><path d="M9 2v4h4M8 9.5V13h1a1.75 1.75 0 0 0 0-3.5H8ZM6 13V9.5L4.25 12 2.5 9.5V13"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.27935 10.9821C5.32063 10.4038 4.9204 9.89049 4.35998 9.80276L3.60081 9.68387C3.37979 9.64945 3.20167 9.48001 3.15225 9.25614L3.01378 8.63511C2.96382 8.41235 3.05233 8.1807 3.23696 8.05125L3.8631 7.61242C4.33337 7.28297 4.47456 6.6369 4.18621 6.13364L3.79467 5.45092C3.68118 5.25261 3.69801 5.00374 3.83757 4.82321L4.22314 4.32436C4.3627 4.14438 4.59621 4.06994 4.81071 4.13772L5.57531 4.37769C6.11944 4.54879 6.70048 4.26159 6.90683 3.71886L7.1811 2.99782C7.26255 2.78395 7.46345 2.64285 7.68772 2.6423L8.31007 2.64063C8.53434 2.64007 8.73579 2.78006 8.81834 2.99337L9.09965 3.72275C9.30821 4.26214 9.88655 4.54712 10.429 4.37714L11.1632 4.14716C11.3772 4.07994 11.6096 4.15382 11.7492 4.3327L12.1374 4.83099" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.27935 10.9821C5.32063 10.4038 4.9204 9.89049 4.35998 9.80276L3.60081 9.68387C3.37979 9.64945 3.20167 9.48001 3.15225 9.25614L3.01378 8.63511C2.96382 8.41235 3.05233 8.1807 3.23696 8.05125L3.8631 7.61242C4.33337 7.28297 4.47456 6.6369 4.18621 6.13364L3.79467 5.45092C3.68118 5.25261 3.69801 5.00374 3.83757 4.82321L4.22314 4.32436C4.3627 4.14438 4.59621 4.06994 4.81071 4.13772L5.57531 4.37769C6.11944 4.54879 6.70048 4.26159 6.90683 3.71886L7.1811 2.99782C7.26255 2.78395 7.46345 2.64285 7.68772 2.6423L8.31007 2.64063C8.53434 2.64007 8.73579 2.78006 8.81834 2.99337L9.09965 3.72275C9.30821 4.26214 9.88655 4.54712 10.429 4.37714L11.1632 4.14716C11.3772 4.07994 11.6096 4.15382 11.7492 4.3327L12.1374 4.83099" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.87504 2H4.25001C3.91848 2 3.60055 2.12643 3.36612 2.35148C3.1317 2.57652 3 2.88174 3 3.2V12.8C3 13.1182 3.1317 13.4234 3.36612 13.6485C3.60055 13.8735 3.91848 14 4.25001 14H11.75C12.0816 14 12.3995 13.8735 12.6339 13.6485C12.8683 13.4234 13 13.1182 13 12.8V5L9.87504 2Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 2V4.66666C9 5.02029 9.14048 5.35942 9.39053 5.60948C9.64059 5.85952 9.97972 6 10.3333 6H13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.4534 8.5H5.73267" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.2672 10.7207H5.73267" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.87504 2H4.25001C3.91848 2 3.60055 2.12643 3.36612 2.35148C3.1317 2.57652 3 2.88174 3 3.2V12.8C3 13.1182 3.1317 13.4234 3.36612 13.6485C3.60055 13.8735 3.91848 14 4.25001 14H11.75C12.0816 14 12.3995 13.8735 12.6339 13.6485C12.8683 13.4234 13 13.1182 13 12.8V5L9.87504 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 2V4.66666C9 5.02029 9.14048 5.35942 9.39053 5.60948C9.64059 5.85952 9.97972 6 10.3333 6H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.4534 8.5H5.73267" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.2672 10.7207H5.73267" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6 6H10" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M8 6V11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H5M11 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M6 6H10" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M8 6V11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M5 3H3.5C3.22386 3 3 3.22386 3 3.5V12.5C3 12.7761 3.22386 13 3.5 13H5M11 3H12.5C12.7761 3 13 3.22386 13 3.5V12.5C13 12.7761 12.7761 13 12.5 13H11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 2.5V3.5M3 3.5V9M3 3.5C3 5.46875 5.96875 5 5.96875 5M3 9C3 11 5.96875 11 5.96875 11M3 9V12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 2.5V3.5M3 3.5V9M3 3.5C3 5.46875 5.96875 5 5.96875 5M3 9C3 11 5.96875 11 5.96875 11M3 9V12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 3H9.5C8.67157 3 8 3.67157 8 4.5V5.5C8 6.32843 8.67157 7 9.5 7H12C12.8284 7 13.5 6.32843 13.5 5.5V4.5C13.5 3.67157 12.8284 3 12 3Z" fill="black"/>
<path d="M12 9H9.5C8.67157 9 8 9.67157 8 10.5V11.5C8 12.3284 8.67157 13 9.5 13H12C12.8284 13 13.5 12.3284 13.5 11.5V10.5C13.5 9.67157 12.8284 9 12 9Z" fill="black"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.9416 2.99643C13.08 2.79636 12.9568 2.5 12.7352 2.5H3.26475C3.04317 2.5 2.91999 2.79636 3.0584 2.99643L6.04033 7.30646C6.24713 7.60535 6.35981 7.97674 6.35981 8.3596C6.35981 9.18422 6.35981 11.4639 6.35981 12.891C6.35981 13.2285 6.59643 13.5 6.88831 13.5H9.11168C9.40357 13.5 9.64019 13.2285 9.64019 12.891C9.64019 11.4639 9.64019 9.18422 9.64019 8.3596C9.64019 7.97674 9.75289 7.60535 9.95969 7.30646L12.9416 2.99643Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.9416 2.99643C13.08 2.79636 12.9568 2.5 12.7352 2.5H3.26475C3.04317 2.5 2.91999 2.79636 3.0584 2.99643L6.04033 7.30646C6.24713 7.60535 6.35981 7.97674 6.35981 8.3596C6.35981 9.18422 6.35981 11.4639 6.35981 12.891C6.35981 13.2285 6.59643 13.5 6.88831 13.5H9.11168C9.40357 13.5 9.64019 13.2285 9.64019 12.891C9.64019 11.4639 9.64019 9.18422 9.64019 8.3596C9.64019 7.97674 9.75289 7.60535 9.95969 7.30646L12.9416 2.99643Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5.5 9.868c.474 0 .928-.18 1.263-.5.335-.321.523-.756.523-1.21 0-.944-.357-1.369-.715-2.053C5.806 4.64 6.411 3.331 8 2c.357 1.71 1.429 3.353 2.857 4.447C12.286 7.542 13 8.842 13 10.21c0 .63-.13 1.252-.38 1.833a4.781 4.781 0 0 1-1.084 1.554 5.021 5.021 0 0 1-1.623 1.038 5.191 5.191 0 0 1-3.826 0 5.02 5.02 0 0 1-1.623-1.038 4.78 4.78 0 0 1-1.083-1.554A4.615 4.615 0 0 1 3 10.211c0-.79.31-1.57.714-2.053 0 .454.188.889.523 1.21.335.32.79.5 1.263.5Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5.5 9.868c.474 0 .928-.18 1.263-.5.335-.321.523-.756.523-1.21 0-.944-.357-1.369-.715-2.053C5.806 4.64 6.411 3.331 8 2c.357 1.71 1.429 3.353 2.857 4.447C12.286 7.542 13 8.842 13 10.21c0 .63-.13 1.252-.38 1.833a4.781 4.781 0 0 1-1.084 1.554 5.021 5.021 0 0 1-1.623 1.038 5.191 5.191 0 0 1-3.826 0 5.02 5.02 0 0 1-1.623-1.038 4.78 4.78 0 0 1-1.083-1.554A4.615 4.615 0 0 1 3 10.211c0-.79.31-1.57.714-2.053 0 .454.188.889.523 1.21.335.32.79.5 1.263.5Z"/></svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.8 13C13.1183 13 13.4235 12.8761 13.6486 12.6554C13.8735 12.4349 14 12.1356 14 11.8236V5.94118C14 5.62916 13.8735 5.32992 13.6486 5.10929C13.4235 4.88866 13.1183 4.76471 12.8 4.76471H8.06C7.8593 4.76664 7.66133 4.71919 7.48418 4.6267C7.30703 4.53421 7.15637 4.39964 7.046 4.2353L6.56 3.52941C6.45073 3.36675 6.30199 3.23322 6.1271 3.14082C5.95221 3.04842 5.75666 3.00004 5.558 3H3.2C2.88174 3 2.57651 3.12395 2.35148 3.34458C2.12643 3.56521 2 3.86445 2 4.17647V11.8236C2 12.1356 2.12643 12.4349 2.35148 12.6554C2.57651 12.8761 2.88174 13 3.2 13H12.8Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8 13C13.1183 13 13.4235 12.8761 13.6486 12.6554C13.8735 12.4349 14 12.1356 14 11.8236V5.94118C14 5.62916 13.8735 5.32992 13.6486 5.10929C13.4235 4.88866 13.1183 4.76471 12.8 4.76471H8.06C7.8593 4.76664 7.66133 4.71919 7.48418 4.6267C7.30703 4.53421 7.15637 4.39964 7.046 4.2353L6.56 3.52941C6.45073 3.36675 6.30199 3.23322 6.1271 3.14082C5.95221 3.04842 5.75666 3.00004 5.558 3H3.2C2.88174 3 2.57651 3.12395 2.35148 3.34458C2.12643 3.56521 2 3.86445 2 4.17647V11.8236C2 12.1356 2.12643 12.4349 2.35148 12.6554C2.57651 12.8761 2.88174 13 3.2 13H12.8Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.42782 7.2487C4.43495 6.97194 4.65009 6.75 4.91441 6.75H13.5293C13.7935 6.75 14.007 6.97194 13.9998 7.2487C13.9628 8.6885 13.7533 12.75 12.5721 12.75H3.375C4.55631 12.75 4.3907 8.6885 4.42782 7.2487Z" fill="black" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
-<path d="M5.19598 12.625H3.66515C3.42618 12.625 3.22289 12.4453 3.18626 12.2017L1.94333 3.93602C1.89776 3.63295 2.12496 3.35938 2.42223 3.35938H5.78585C6.11241 3.35938 6.41702 3.52903 6.59618 3.81071L6.94517 4.35938H9.92811C10.4007 4.35938 10.8044 4.71102 10.8836 5.1917L11.1251 6.65624" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.42782 7.2487C4.43495 6.97194 4.65009 6.75 4.91441 6.75H13.5293C13.7935 6.75 14.007 6.97194 13.9998 7.2487C13.9628 8.6885 13.7533 12.75 12.5721 12.75H3.375C4.55631 12.75 4.3907 8.6885 4.42782 7.2487Z" fill="black" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
+<path d="M5.19598 12.625H3.66515C3.42618 12.625 3.22289 12.4453 3.18626 12.2017L1.94333 3.93602C1.89776 3.63295 2.12496 3.35938 2.42223 3.35938H5.78585C6.11241 3.35938 6.41702 3.52903 6.59618 3.81071L6.94517 4.35938H9.92811C10.4007 4.35938 10.8044 4.71102 10.8836 5.1917L11.1251 6.65624" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0001 13.9999L12.7334 12.7333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3333 13.3334C12.4378 13.3334 13.3333 12.4379 13.3333 11.3334C13.3333 10.2288 12.4378 9.33337 11.3333 9.33337C10.2287 9.33337 9.33325 10.2288 9.33325 11.3334C9.33325 12.4379 10.2287 13.3334 11.3333 13.3334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 13H3.2C2.88174 13 2.57651 12.8761 2.35148 12.6554C2.12643 12.4349 2 12.1356 2 11.8236V4.17647C2 3.86445 2.12643 3.56521 2.35148 3.34458C2.57651 3.12395 2.88174 3 3.2 3H5.558C5.75666 3.00004 5.95221 3.04842 6.1271 3.14082C6.30199 3.23322 6.45073 3.36675 6.56 3.52941L7.046 4.2353C7.15637 4.39964 7.30703 4.53421 7.48418 4.6267C7.66133 4.71919 7.8593 4.76664 8.06 4.76471H12.8C13.1183 4.76471 13.4235 4.88866 13.6486 5.10929C13.8735 5.32992 14 5.62916 14 5.94118V7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 13H3.2C2.88174 13 2.57651 12.8761 2.35148 12.6554C2.12643 12.4349 2 12.1356 2 11.8236V4.17647C2 3.86445 2.12643 3.56521 2.35148 3.34458C2.57651 3.12395 2.88174 3 3.2 3H5.558C5.75666 3.00004 5.95221 3.04842 6.1271 3.14082C6.30199 3.23322 6.45073 3.36675 6.56 3.52941L7.046 4.2353C7.15637 4.39964 7.30703 4.53421 7.48418 4.6267C7.66133 4.71919 7.8593 4.76664 8.06 4.76471H12.8C13.1183 4.76471 13.4235 4.88866 13.6486 5.10929C13.8735 5.32992 14 5.62916 14 5.94118V7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 5V3h10v2M6 13h4M8 3v10"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3 5V3h10v2M6 13h4M8 3v10"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14 9.333h-3.333M10.667 10.667V8.333a1.667 1.667 0 1 1 3.333 0v2.334"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M3 8.667h4"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m2 10.667 3-6 3 6"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M14 9.333h-3.333M10.667 10.667V8.333a1.667 1.667 0 1 1 3.333 0v2.334"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M3 8.667h4"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m2 10.667 3-6 3 6"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.27 8h5.626a2.5 2.5 0 1 1 0 5h-5a.625.625 0 0 1-.625-.625v-8.75A.625.625 0 0 1 4.896 3H9.27a2.5 2.5 0 1 1 0 5"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4.27 8h5.626a2.5 2.5 0 1 1 0 5h-5a.625.625 0 0 1-.625-.625v-8.75A.625.625 0 0 1 4.896 3H9.27a2.5 2.5 0 1 1 0 5"/></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10 11.3334L13.3333 8.00002L10 4.66669" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2.66675 12V10.6667C2.66675 9.95942 2.9477 9.28115 3.4478 8.78105C3.94789 8.28095 4.62617 8 5.33341 8H13.3334" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 11.3334L13.3333 8.00002L10 4.66669" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.66675 12V10.6667C2.66675 9.95942 2.9477 9.28115 3.4478 8.78105C3.94789 8.28095 4.62617 8 5.33341 8H13.3334" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5 3v7M11.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM4.5 13a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM11 6a5 5 0 0 1-5 5"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5 3v7M11.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM4.5 13a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM11 6a5 5 0 0 1-5 5"/></svg>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.5 14C5.32843 14 6 13.3284 6 12.5C6 11.6716 5.32843 11 4.5 11C3.67157 11 3 11.6716 3 12.5C3 13.3284 3.67157 14 4.5 14Z" stroke="black" stroke-width="1.5"/>
-<path d="M4.5 11V5.5" stroke="black" stroke-width="1.5"/>
-<path d="M4.5 10C4.5 10 4.875 8 6.5 8C7.29195 8 9.00787 8 9.87553 8C10.773 8 11.5 7.32843 11.5 6.5V5.5" stroke="black" stroke-width="1.5"/>
-<path d="M4.5 6C5.32843 6 6 5.32843 6 4.5C6 3.67157 5.32843 3 4.5 3C3.67157 3 3 3.67157 3 4.5C3 5.32843 3.67157 6 4.5 6Z" stroke="black" stroke-width="1.5"/>
-<path d="M11.5 6C12.3284 6 13 5.32843 13 4.5C13 3.67157 12.3284 3 11.5 3C10.6716 3 10 3.67157 10 4.5C10 5.32843 10.6716 6 11.5 6Z" stroke="black" stroke-width="1.5"/>
+<path d="M4.5 14C5.32843 14 6 13.3284 6 12.5C6 11.6716 5.32843 11 4.5 11C3.67157 11 3 11.6716 3 12.5C3 13.3284 3.67157 14 4.5 14Z" stroke="black" stroke-width="1.2"/>
+<path d="M4.5 11V5.5" stroke="black" stroke-width="1.2"/>
+<path d="M4.5 10C4.5 10 4.875 8 6.5 8C7.29195 8 9.00787 8 9.87553 8C10.773 8 11.5 7.32843 11.5 6.5V5.5" stroke="black" stroke-width="1.2"/>
+<path d="M4.5 6C5.32843 6 6 5.32843 6 4.5C6 3.67157 5.32843 3 4.5 3C3.67157 3 3 3.67157 3 4.5C3 5.32843 3.67157 6 4.5 6Z" stroke="black" stroke-width="1.2"/>
+<path d="M11.5 6C12.3284 6 13 5.32843 13 4.5C13 3.67157 12.3284 3 11.5 3C10.6716 3 10 3.67157 10 4.5C10 5.32843 10.6716 6 11.5 6Z" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.849 14.288v-2.515a3.018 3.018 0 0 0-.629-2.201c1.887 0 3.773-1.258 3.773-3.459.05-.786-.17-1.559-.629-2.2a4.65 4.65 0 0 0 0-2.201s-.629 0-1.886.943a13.533 13.533 0 0 0-5.03 0c-1.259-.943-1.887-.943-1.887-.943a4.35 4.35 0 0 0 0 2.2 3.398 3.398 0 0 0-.63 2.201c0 2.201 1.887 3.459 3.774 3.459a2.965 2.965 0 0 0-.63 2.2v2.516"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.076 11.773c-2.836 1.258-3.144-1.258-4.402-1.258"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M9.849 14.288v-2.515a3.018 3.018 0 0 0-.629-2.201c1.887 0 3.773-1.258 3.773-3.459.05-.786-.17-1.559-.629-2.2a4.65 4.65 0 0 0 0-2.201s-.629 0-1.886.943a13.533 13.533 0 0 0-5.03 0c-1.259-.943-1.887-.943-1.887-.943a4.35 4.35 0 0 0 0 2.2 3.398 3.398 0 0 0-.63 2.201c0 2.201 1.887 3.459 3.774 3.459a2.965 2.965 0 0 0-.63 2.2v2.516"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.076 11.773c-2.836 1.258-3.144-1.258-4.402-1.258"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-width="1.5" d="m11.748 3.015-2.893 9.573M7.161 3.424l-2.893 9.572M3.611 6.148h10M2.398 9.856h10"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-width="1.2" d="m11.748 3.015-2.893 9.573M7.161 3.424l-2.893 9.572M3.611 6.148h10M2.398 9.856h10"/></svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2 8C2 9.18669 2.35189 10.3467 3.01118 11.3334C3.67047 12.3201 4.60754 13.0892 5.7039 13.5433C6.80026 13.9974 8.00666 14.1162 9.17054 13.8847C10.3344 13.6532 11.4035 13.0818 12.2426 12.2426C13.0818 11.4035 13.6532 10.3344 13.8847 9.17054C14.1162 8.00666 13.9974 6.80026 13.5433 5.7039C13.0892 4.60754 12.3201 3.67047 11.3334 3.01118C10.3467 2.35189 9.18669 2 8 2C6.32263 2.00631 4.71265 2.66082 3.50667 3.82667L2 5.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2 2V5.33333H5.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 5V8.5L10 9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2 8C2 9.18669 2.35189 10.3467 3.01118 11.3334C3.67047 12.3201 4.60754 13.0892 5.7039 13.5433C6.80026 13.9974 8.00666 14.1162 9.17054 13.8847C10.3344 13.6532 11.4035 13.0818 12.2426 12.2426C13.0818 11.4035 13.6532 10.3344 13.8847 9.17054C14.1162 8.00666 13.9974 6.80026 13.5433 5.7039C13.0892 4.60754 12.3201 3.67047 11.3334 3.01118C10.3467 2.35189 9.18669 2 8 2C6.32263 2.00631 4.71265 2.66082 3.50667 3.82667L2 5.33333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2 2V5.33333H5.33333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 5V8.5L10 9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.889 3H4.11C3.497 3 3 3.497 3 4.111v7.778C3 12.503 3.497 13 4.111 13h7.778c.614 0 1.111-.498 1.111-1.111V4.11C13 3.497 12.502 3 11.889 3Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.333 7.444a1.111 1.111 0 1 0 0-2.222 1.111 1.111 0 0 0 0 2.222ZM13 9.667l-1.714-1.715a1.11 1.11 0 0 0-1.571 0L4.667 13"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M11.889 3H4.11C3.497 3 3 3.497 3 4.111v7.778C3 12.503 3.497 13 4.111 13h7.778c.614 0 1.111-.498 1.111-1.111V4.11C13 3.497 12.502 3 11.889 3Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.333 7.444a1.111 1.111 0 1 0 0-2.222 1.111 1.111 0 0 0 0 2.222ZM13 9.667l-1.714-1.715a1.11 1.11 0 0 0-1.571 0L4.667 13"/></svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.5"/>
-<path d="M7 11H8M8 11H9M8 11V8.1C8 8.04477 7.95523 8 7.9 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="black" stroke-width="1.2"/>
+<path d="M7 11H8M8 11H9M8 11V8.1C8 8.04477 7.95523 8 7.9 8H7" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
<path d="M8 6.5C8.55228 6.5 9 6.05228 9 5.5C9 4.94772 8.55228 4.5 8 4.5C7.44772 4.5 7 4.94772 7 5.5C7 6.05228 7.44772 6.5 8 6.5Z" fill="black"/>
</svg>
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.78125 3C3.90625 3 3.90625 4.5 3.90625 5.5C3.90625 6.5 3.40625 7.50106 2.40625 8C3.40625 8.50106 3.90625 9.5 3.90625 10.5C3.90625 11.5 3.90625 13 5.78125 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.2422 3C12.1172 3 12.1172 4.5 12.1172 5.5C12.1172 6.5 12.6172 7.50106 13.6172 8C12.6172 8.50106 12.1172 9.5 12.1172 10.5C12.1172 11.5 12.1172 13 10.2422 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.75 5.5h.007M8 8h.007M9.25 5.5h.007M10.5 8h.007M11.75 5.5h.007M4.25 5.5h.007M4.875 10.5h6.25M5.5 8h.007M13 3H3c-.69 0-1.25.56-1.25 1.25v7.5c0 .69.56 1.25 1.25 1.25h10c.69 0 1.25-.56 1.25-1.25v-7.5C14.25 3.56 13.69 3 13 3Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.75 5.5h.007M8 8h.007M9.25 5.5h.007M10.5 8h.007M11.75 5.5h.007M4.25 5.5h.007M4.875 10.5h6.25M5.5 8h.007M13 3H3c-.69 0-1.25.56-1.25 1.25v7.5c0 .69.56 1.25 1.25 1.25h10c.69 0 1.25-.56 1.25-1.25v-7.5C14.25 3.56 13.69 3 13 3Z"/></svg>
@@ -1,3 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 3L5.5 5.5M8 8L5.5 5.5M5.5 5.5L3 8M5.5 5.5L8 3" stroke="black" stroke-width="1.5"/>
+<path d="M3 3L5.5 5.5M8 8L5.5 5.5M5.5 5.5L3 8M5.5 5.5L8 3" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.6667 4L13.3334 13.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 4V13.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.33325 5.33331V13.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2.66675 2.66669V13.3334" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.6667 4L13.3334 13.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 4V13.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.33325 5.33331V13.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.66675 2.66669V13.3334" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 13.667h8M4 2.333h8M5 11l3-6 3 6M6 9h4"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4 13.667h8M4 2.333h8M5 11l3-6 3 6M6 9h4"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.857 6.857 4.286 5.43 2.857 4M2.857 12l1.429-1.429-1.429-1.428M6.857 4.571h6.286M6.857 8h6.286M6.857 11.428h6.286"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.857 6.857 4.286 5.43 2.857 4M2.857 12l1.429-1.429-1.429-1.428M6.857 4.571h6.286M6.857 8h6.286M6.857 11.428h6.286"/></svg>
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-filter-icon lucide-list-filter"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5.333 3.333H2.667A.667.667 0 0 0 2 4v2.667c0 .368.298.666.667.666h2.666A.667.667 0 0 0 6 6.667V4a.667.667 0 0 0-.667-.667ZM2 11.333l1.333 1.334L6 10M8.667 4H14M8.667 8H14M8.667 12H14"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5.333 3.333H2.667A.667.667 0 0 0 2 4v2.667c0 .368.298.666.667.666h2.666A.667.667 0 0 0 6 6.667V4a.667.667 0 0 0-.667-.667ZM2 11.333l1.333 1.334L6 10M8.667 4H14M8.667 8H14M8.667 12H14"/></svg>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.5 8H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.5 4L6.5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.5 12H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 3.5V6.33333C3 7.25 3.72 8 4.6 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 6V10.5C3 11.325 3.72 12 4.6 12H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5 8H9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5 4L6.5 4" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.5 12H9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 3.5V6.33333C3 7.25 3.72 8 4.6 8H7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 6V10.5C3 11.325 3.72 12 4.6 12H7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.33333 8H3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.6667 4H3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11.6667 12H3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.6667 6.66663L11 9.33329" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11 6.66663L13.6667 9.33329" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.33333 8H3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.6667 4H3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.6667 12H3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.6667 6.66663L11 9.33329" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 6.66663L13.6667 9.33329" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 8a5 5 0 1 1-3.455-4.755"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13 8a5 5 0 1 1-3.455-4.755"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.502a4.904 4.904 0 0 0-1.686-3.28 5.059 5.059 0 0 0-3.522-1.22A5.045 5.045 0 0 0 3.39 3.52 4.889 4.889 0 0 0 2 6.93c0 2.89 3.06 5.893 4.397 7.068M13.655 11.013a1.18 1.18 0 0 0-1.67-1.67l-2.227 2.23a1.112 1.112 0 0 0-.282.475l-.465 1.594a.278.278 0 0 0 .345.345l1.594-.465a1.11 1.11 0 0 0 .475-.281l2.23-2.228Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 8.998a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12 6.502a4.904 4.904 0 0 0-1.686-3.28 5.059 5.059 0 0 0-3.522-1.22A5.045 5.045 0 0 0 3.39 3.52 4.889 4.889 0 0 0 2 6.93c0 2.89 3.06 5.893 4.397 7.068M13.655 11.013a1.18 1.18 0 0 0-1.67-1.67l-2.227 2.23a1.112 1.112 0 0 0-.282.475l-.465 1.594a.278.278 0 0 0 .345.345l1.594-.465a1.11 1.11 0 0 0 .475-.281l2.23-2.228Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M7 8.998a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/></svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.5"/>
-<path d="M8 9V11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M5 5C5 3.89543 5.89543 3 7 3H9C10.1046 3 11 3.89543 11 5V6H5V5Z" stroke="black" stroke-width="1.2"/>
+<path d="M8 9V11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
<circle cx="8" cy="9" r="1" fill="black"/>
-<rect x="3.75" y="5.75" width="8.5" height="7.5" rx="1.25" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
+<rect x="3.75" y="5.75" width="8.5" height="7.5" rx="1.25" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 13L10.4138 10.4138ZM3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" fill="black" fill-opacity="0.15"/>
-<path d="M13 13L10.4138 10.4138M3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 13L10.4138 10.4138M3 7.31034C3 4.92981 4.92981 3 7.31034 3C9.6909 3 11.6207 4.92981 11.6207 7.31034C11.6207 9.6909 9.6909 11.6207 7.31034 11.6207C4.92981 11.6207 3 9.6909 3 7.31034Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10 3H13V6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 13H3V10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 3L9.5 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 13L6.5 9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 3H13V6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 13H3V10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 3L9.5 6.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 13L6.5 9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.667 8h10.666M2.667 4h10.666M2.667 12h10.666"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.667 8h10.666M2.667 4h10.666M2.667 12h10.666"/></svg>
@@ -1 +1,3 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.667 8h8M2.667 4h10.666M2.667 12H8"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2.66699 8H10.667M2.66699 4H13.333M2.66699 12H7.99999" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13.333 10H8M13.333 6H2.66701" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 12.2028V14.3042" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.2027 6.94928V8.11672C12.2027 9.20041 11.7599 10.2397 10.9717 11.006C10.1836 11.7723 9.11457 12.2028 7.99992 12.2028C6.88527 12.2028 5.81627 11.7723 5.02809 11.006C4.23991 10.2397 3.79712 9.20041 3.79712 8.11672V6.94928" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.1015 3.63555C10.1015 2.56426 9.16065 1.6958 8.00008 1.6958C6.83951 1.6958 5.89868 2.56426 5.89868 3.63555V8.16165C5.89868 9.23294 6.83951 10.1014 8.00008 10.1014C9.16065 10.1014 10.1015 9.23294 10.1015 8.16165V3.63555Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 12.2028V14.3042" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.2027 6.94928V8.11672C12.2027 9.20041 11.7599 10.2397 10.9717 11.006C10.1836 11.7723 9.11457 12.2028 7.99992 12.2028C6.88527 12.2028 5.81627 11.7723 5.02809 11.006C4.23991 10.2397 3.79712 9.20041 3.79712 8.11672V6.94928" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.1015 3.63555C10.1015 2.56426 9.16065 1.6958 8.00008 1.6958C6.83951 1.6958 5.89868 2.56426 5.89868 3.63555V8.16165C5.89868 9.23294 6.83951 10.1014 8.00008 10.1014C9.16065 10.1014 10.1015 9.23294 10.1015 8.16165V3.63555Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,8 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 3L13 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 9C12 8.74858 12 8.49375 12 8.23839V7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4.00043 7V8.09869C3.98856 8.86731 4.22157 9.62164 4.66938 10.2643C5.11718 10.907 5.75924 11.4085 6.51267 11.7042C7.2661 11.9999 8.09632 12.0761 8.89619 11.923C9.47851 11.8115 10.0253 11.5823 10.5 11.2539" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 6V3.62904C9.99714 3.26103 9.8347 2.90448 9.53885 2.6168C9.24299 2.32913 8.83093 2.12707 8.36903 2.04316C7.90713 1.95926 7.42226 1.9984 6.99252 2.15427C6.56278 2.31015 6.21317 2.57369 6 2.90245" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 6V8.00088C6.00031 8.39636 6.10356 8.78287 6.29674 9.11159C6.48991 9.44031 6.76433 9.69649 7.08534 9.84779C7.40634 9.99909 7.75954 10.0387 8.10032 9.96165C8.4411 9.88459 8.75417 9.69431 9 9.41483" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 12V14" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 3L13 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 9C12 8.74858 12 8.49375 12 8.23839V7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.00043 7V8.09869C3.98856 8.86731 4.22157 9.62164 4.66938 10.2643C5.11718 10.907 5.75924 11.4085 6.51267 11.7042C7.2661 11.9999 8.09632 12.0761 8.89619 11.923C9.47851 11.8115 10.0253 11.5823 10.5 11.2539" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 6V3.62904C9.99714 3.26103 9.8347 2.90448 9.53885 2.6168C9.24299 2.32913 8.83093 2.12707 8.36903 2.04316C7.90713 1.95926 7.42226 1.9984 6.99252 2.15427C6.56278 2.31015 6.21317 2.57369 6 2.90245" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 6V8.00088C6.00031 8.39636 6.10356 8.78287 6.29674 9.11159C6.48991 9.44031 6.76433 9.69649 7.08534 9.84779C7.40634 9.99909 7.75954 10.0387 8.10032 9.96165C8.4411 9.88459 8.75417 9.69431 9 9.41483" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 12V14" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.5 9.5H6.5V12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.5 6.5H9.5V3.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.5 6.5L13 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 13L6.5 9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 9.5H6.5V12.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 6.5H9.5V3.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 6.5L13 3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 13L6.5 9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" clip-path="url(#a)"><path d="M6 8h4M6 10.5h4M3 3h10v9.565c0 .38-.158.746-.44 1.015-.28.269-.662.42-1.06.42h-7c-.398 0-.78-.151-1.06-.42A1.404 1.404 0 0 1 3 12.565V3ZM6.5 1v4M9.5 1v4"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="M6 8h4M6 10.5h4M3 3h10v9.565c0 .38-.158.746-.44 1.015-.28.269-.662.42-1.06.42h-7c-.398 0-.78-.151-1.06-.42A1.404 1.404 0 0 1 3 12.565V3ZM6.5 1v4M9.5 1v4"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 3H6.33333L9.66667 13H13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.11108 3H13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 3H6.33333L9.66667 13H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.11108 3H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.5871 5.40582C12.8514 5.14152 13 4.78304 13 4.40922C13 4.03541 12.8516 3.67688 12.5873 3.41252C12.323 3.14816 11.9645 2.99962 11.5907 2.99957C11.2169 2.99953 10.8584 3.14798 10.594 3.41227L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L12.5871 5.40582Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 5L11 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5871 5.40582C12.8514 5.14152 13 4.78304 13 4.40922C13 4.03541 12.8516 3.67688 12.5873 3.41252C12.323 3.14816 11.9645 2.99962 11.5907 2.99957C11.2169 2.99953 10.8584 3.14798 10.594 3.41227L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L12.5871 5.40582Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 5L11 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8.5 5.50621L10.5941 3.41227C10.8585 3.14798 11.217 2.99953 11.5908 2.99957C11.9646 2.99962 12.3231 3.14816 12.5874 3.41252C12.8517 3.67688 13.0001 4.03541 13.0001 4.40922C13.0001 4.78304 12.8515 5.14152 12.5872 5.40582L10.493 7.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.50789 8.5L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L7.49184 10.5019" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 5L11 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 3L13 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12.667 14v-1.333A2.667 2.667 0 0 0 10 10H6a2.667 2.667 0 0 0-2.667 2.667V14M8 7.333A2.667 2.667 0 1 0 8 2a2.667 2.667 0 0 0 0 5.333Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M12.667 14v-1.333A2.667 2.667 0 0 0 10 10H6a2.667 2.667 0 0 0-2.667 2.667V14M8 7.333A2.667 2.667 0 1 0 8 2a2.667 2.667 0 0 0 0 5.333Z"/></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 10V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 4L12 8L5 12V4Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 4L12 8L5 12V4Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 4L12 8L5 12V4Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 4L12 8L5 12V4Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.33325 8H12.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 3.33333V12.6667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.33325 8H12.6666" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 3.33333V12.6667" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 2v5M11.536 4a5.368 5.368 0 0 1 1.367 2.696 5.54 5.54 0 0 1-.28 3.042 5.223 5.223 0 0 1-1.836 2.367A4.82 4.82 0 0 1 8.016 13a4.817 4.817 0 0 1-2.777-.877 5.22 5.22 0 0 1-1.85-2.354 5.54 5.54 0 0 1-.298-3.041 5.371 5.371 0 0 1 1.35-2.705"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 2v5M11.536 4a5.368 5.368 0 0 1 1.367 2.696 5.54 5.54 0 0 1-.28 3.042 5.223 5.223 0 0 1-1.836 2.367A4.82 4.82 0 0 1 8.016 13a4.817 4.817 0 0 1-2.777-.877 5.22 5.22 0 0 1-1.85-2.354 5.54 5.54 0 0 1-.298-3.041 5.371 5.371 0 0 1 1.35-2.705"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6.375A5.625 5.625 0 0 1 9.625 12M4 10a2 2 0 0 1 2 2M4 3a9 9 0 0 1 9 9"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4 6.375A5.625 5.625 0 0 1 9.625 12M4 10a2 2 0 0 1 2 2M4 3a9 9 0 0 1 9 9"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM4 6v7M12 13a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM10 6.5l-2-2 2-2"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8.5 4.389h2.357c.303 0 .594.117.808.325.215.209.335.491.335.786V10"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M4 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM4 6v7M12 13a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM10 6.5l-2-2 2-2"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8.5 4.389h2.357c.303 0 .594.117.808.325.215.209.335.491.335.786V10"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.333 4H2M14 8H5.333M12 12H5M2 8v4"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M11.333 4H2M14 8H5.333M12 12H5M2 8v4"/></svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.8889 2H4.11111C3.49746 2 3 2.59695 3 3.33333V12.6667C3 13.403 3.49746 14 4.11111 14H11.8889C12.5025 14 13 13.403 13 12.6667V3.33333C13 2.59695 12.5025 2 11.8889 2Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 6H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 10H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11.8889 2H4.11111C3.49746 2 3 2.59695 3 3.33333V12.6667C3 13.403 3.49746 14 4.11111 14H11.8889C12.5025 14 13 13.403 13 12.6667V3.33333C13 2.59695 12.5025 2 11.8889 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 6H6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 10H6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-width="1.5" d="M4.667 14V8m0 0H2m2.667 0h2.666"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14 12.667h-3.333V9.333"/><path fill="#000" d="M4.03 3.5a.667.667 0 0 0 .883 1l-.882-1ZM8 3.333A4.667 4.667 0 0 1 12.666 8H14a6 6 0 0 0-6-6v1.333ZM12.666 8a4.656 4.656 0 0 1-1.75 3.643l.834 1.04A5.99 5.99 0 0 0 14 8h-1.334ZM5.667 3.957A4.642 4.642 0 0 1 8 3.333V2a5.976 5.976 0 0 0-3 .803l.667 1.154Zm-.754.543c.232-.205.485-.387.754-.543l-.668-1.154c-.346.2-.67.434-.968.697l.882 1Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-width="1.2" d="M4.667 14V8m0 0H2m2.667 0h2.666"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M14 12.667h-3.333V9.333"/><path fill="#000" d="M4.03 3.5a.667.667 0 0 0 .883 1l-.882-1ZM8 3.333A4.667 4.667 0 0 1 12.666 8H14a6 6 0 0 0-6-6v1.333ZM12.666 8a4.656 4.656 0 0 1-1.75 3.643l.834 1.04A5.99 5.99 0 0 0 14 8h-1.334ZM5.667 3.957A4.642 4.642 0 0 1 8 3.333V2a5.976 5.976 0 0 0-3 .803l.667 1.154Zm-.754.543c.232-.205.485-.387.754-.543l-.668-1.154c-.346.2-.67.434-.968.697l.882 1Z"/></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.57132 13.7143C5.20251 13.7143 5.71418 13.2026 5.71418 12.5714C5.71418 11.9403 5.20251 11.4286 4.57132 11.4286C3.94014 11.4286 3.42847 11.9403 3.42847 12.5714C3.42847 13.2026 3.94014 13.7143 4.57132 13.7143Z" fill="black"/>
-<path d="M10.2856 2.85712V5.71426M10.2856 5.71426V8.5714M10.2856 5.71426H13.1428M10.2856 5.71426H7.42847M10.2856 5.71426L12.1904 3.80949M10.2856 5.71426L8.38084 7.61906M10.2856 5.71426L12.1904 7.61906M10.2856 5.71426L8.38084 3.80949" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M10.2856 2.85712V5.71426M10.2856 5.71426V8.5714M10.2856 5.71426H13.1428M10.2856 5.71426H7.42847M10.2856 5.71426L12.1904 3.80949M10.2856 5.71426L8.38084 7.61906M10.2856 5.71426L12.1904 7.61906M10.2856 5.71426L8.38084 3.80949" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.00391 13.9293C8.16208 14.1122 9.35055 13.9659 10.4234 13.5085C11.4962 13.0511 12.4066 12.3024 13.0426 11.3545C13.6787 10.4066 14.0128 9.30075 14.0037 8.17293C13.9977 7.42342 13.8404 6.68587 13.5444 6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.00391 2.07233C7.83928 1.90288 6.64809 2.05936 5.57504 2.52278C4.502 2.9862 3.59331 3.73659 2.95939 4.68279C2.32547 5.62899 1.99362 6.73024 2.00415 7.85274C2.0111 8.59299 2.16675 9.32147 2.45883 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00391 13.9293C8.16208 14.1122 9.35055 13.9659 10.4234 13.5085C11.4962 13.0511 12.4066 12.3024 13.0426 11.3545C13.6787 10.4066 14.0128 9.30075 14.0037 8.17293C13.9977 7.42342 13.8404 6.68587 13.5444 6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.00391 2.07233C7.83928 1.90288 6.64809 2.05936 5.57504 2.52278C4.502 2.9862 3.59331 3.73659 2.95939 4.68279C2.32547 5.62899 1.99362 6.73024 2.00415 7.85274C2.0111 8.59299 2.16675 9.32147 2.45883 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,11 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M14.0039 8.01361C13.9981 7.31989 13.8495 6.63699 13.5698 6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.00391 2.01361C7.74152 2.01361 8.2084 2.01361 9.00391 2.01361" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.0039 11.0136C12.4719 11.8034 11.7928 12.4825 11.0039 13.0136" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.00391 14.0136C8.28937 14.0136 7.71844 14.0136 7.00391 14.0136" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.00391 5.01361C3.53595 4.22382 4.21507 3.5447 5.00391 3.01361" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.00391 9.01361C8.55621 9.01361 9.00391 8.56591 9.00391 8.01361C9.00391 7.46131 8.55621 7.01361 8.00391 7.01361C7.45161 7.01361 7.00391 7.46131 7.00391 8.01361C7.00391 8.56591 7.45161 9.01361 8.00391 9.01361Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M2.00391 8.01361C2.01055 8.69737 2.15535 9.37058 2.42723 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14.0039 8.01361C13.9981 7.31989 13.8495 6.63699 13.5698 6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00391 2.01361C7.74152 2.01361 8.2084 2.01361 9.00391 2.01361" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.0039 11.0136C12.4719 11.8034 11.7928 12.4825 11.0039 13.0136" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.00391 14.0136C8.28937 14.0136 7.71844 14.0136 7.00391 14.0136" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.00391 5.01361C3.53595 4.22382 4.21507 3.5447 5.00391 3.01361" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.00391 9.01361C8.55621 9.01361 9.00391 8.56591 9.00391 8.01361C9.00391 7.46131 8.55621 7.01361 8.00391 7.01361C7.45161 7.01361 7.00391 7.46131 7.00391 8.01361C7.00391 8.56591 7.45161 9.01361 8.00391 9.01361Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M2.00391 8.01361C2.01055 8.69737 2.15535 9.37058 2.42723 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,8 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.00391 13.9293C8.16208 14.1122 9.35055 13.9659 10.4234 13.5085C11.4962 13.0511 12.4066 12.3024 13.0426 11.3545C13.6787 10.4066 14.0128 9.30075 14.0037 8.17293C13.9977 7.42342 13.8404 6.68587 13.5444 6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.00391 2.07233C7.83928 1.90288 6.64809 2.05936 5.57504 2.52278C4.502 2.9862 3.59331 3.73659 2.95939 4.68279C2.32547 5.62899 1.99362 6.73024 2.00415 7.85274C2.0111 8.59299 2.16675 9.32147 2.45883 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.00391 10.0059V6.00592" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.00391 10.0059V6.00592" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00391 13.9293C8.16208 14.1122 9.35055 13.9659 10.4234 13.5085C11.4962 13.0511 12.4066 12.3024 13.0426 11.3545C13.6787 10.4066 14.0128 9.30075 14.0037 8.17293C13.9977 7.42342 13.8404 6.68587 13.5444 6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.00391 2.07233C7.83928 1.90288 6.64809 2.05936 5.57504 2.52278C4.502 2.9862 3.59331 3.73659 2.95939 4.68279C2.32547 5.62899 1.99362 6.73024 2.00415 7.85274C2.0111 8.59299 2.16675 9.32147 2.45883 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00391 10.0059V6.00592" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.00391 10.0059V6.00592" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.00366 6.16662C7.00366 6.03849 7.14274 5.96206 7.24661 6.03313L9.93408 7.87243C10.0269 7.93591 10.0269 8.07591 9.93408 8.13939L7.24661 9.97871C7.14274 10.0498 7.00366 9.97336 7.00366 9.84518V6.16662Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.00391 13.9293C8.16208 14.1122 9.35055 13.9659 10.4234 13.5085C11.4962 13.0511 12.4066 12.3024 13.0426 11.3545C13.6787 10.4066 14.0128 9.30075 14.0037 8.17293C13.9977 7.42342 13.8404 6.68587 13.5444 6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.00391 2.07233C7.83928 1.90288 6.64809 2.05936 5.57504 2.52278C4.502 2.9862 3.59331 3.73659 2.95939 4.68279C2.32547 5.62899 1.99362 6.73024 2.00415 7.85274C2.0111 8.59299 2.16675 9.32147 2.45883 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00366 6.16662C7.00366 6.03849 7.14274 5.96206 7.24661 6.03313L9.93408 7.87243C10.0269 7.93591 10.0269 8.07591 9.93408 8.13939L7.24661 9.97871C7.14274 10.0498 7.00366 9.97336 7.00366 9.84518V6.16662Z" fill="black" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5 6C13.3284 6 14 5.32843 14 4.5C14 3.67157 13.3284 3 12.5 3C11.6716 3 11 3.67157 11 4.5C11 5.32843 11.6716 6 12.5 6Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.5 13C4.32843 13 5 12.3284 5 11.5C5 10.6716 4.32843 10 3.5 10C2.67157 10 2 10.6716 2 11.5C2 12.3284 2.67157 13 3.5 13Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00391 13.9293C8.16208 14.1122 9.35055 13.9659 10.4234 13.5085C11.4962 13.0511 12.4066 12.3024 13.0426 11.3545C13.6787 10.4066 14.0128 9.30075 14.0037 8.17293C13.9977 7.42342 13.8404 6.68587 13.5444 6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.00391 2.07233C7.83928 1.90288 6.64809 2.05936 5.57504 2.52278C4.502 2.9862 3.59331 3.73659 2.95939 4.68279C2.32547 5.62899 1.99362 6.73024 2.00415 7.85274C2.0111 8.59299 2.16675 9.32147 2.45883 10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.99988 13C5.97267 13 4.22723 11.7936 3.44238 10.0595M7.99988 3C10.1122 3 11.9185 4.30981 12.6511 6.16152" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99988 13C5.97267 13 4.22723 11.7936 3.44238 10.0595M7.99988 3C10.1122 3 11.9185 4.30981 12.6511 6.16152" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.65625 3.29688C3.00143 3.29688 3.28125 3.01705 3.28125 2.67188C3.28125 2.3267 3.00143 2.04688 2.65625 2.04688C2.31107 2.04688 2.03125 2.3267 2.03125 2.67188C2.03125 3.01705 2.31107 3.29688 2.65625 3.29688Z" fill="black"/>
<path d="M4.71094 3.29688C5.05612 3.29688 5.33594 3.01705 5.33594 2.67188C5.33594 2.3267 5.05612 2.04688 4.71094 2.04688C4.36576 2.04688 4.08594 2.3267 4.08594 2.67188C4.08594 3.01705 4.36576 3.29688 4.71094 3.29688Z" fill="black"/>
<path d="M5.96094 4.99219C6.30612 4.99219 6.58594 4.71237 6.58594 4.36719C6.58594 4.02201 6.30612 3.74219 5.96094 3.74219C5.61576 3.74219 5.33594 4.02201 5.33594 4.36719C5.33594 4.71237 5.61576 4.99219 5.96094 4.99219Z" fill="black"/>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M8.5 6.88672C10.433 6.88672 12 8.45372 12 10.3867V13M12 13L14 11M12 13L10 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M8.5 6.88672C10.433 6.88672 12 8.45372 12 10.3867V13M12 13L14 11M12 13L10 11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2 8a6 6 0 0 1 6-6 6.5 6.5 0 0 1 4.493 1.827L14 5.333"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14 2v3.333h-3.333M14 8a6 6 0 0 1-6 6 6.5 6.5 0 0 1-4.493-1.827L2 10.667"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5.333 10.667H2V14"/><path fill="#000" d="M6.667 6.247c0-.257.279-.418.501-.288l3.005 1.753c.22.129.22.447 0 .576L7.168 10.04a.333.333 0 0 1-.501-.288V6.247Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2 8a6 6 0 0 1 6-6 6.5 6.5 0 0 1 4.493 1.827L14 5.333"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M14 2v3.333h-3.333M14 8a6 6 0 0 1-6 6 6.5 6.5 0 0 1-4.493-1.827L2 10.667"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5.333 10.667H2V14"/><path fill="#000" d="M6.667 6.247c0-.257.279-.418.501-.288l3.005 1.753c.22.129.22.447 0 .576L7.168 10.04a.333.333 0 0 1-.501-.288V6.247Z"/></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.00008 6.66669L2.66675 10L6.00008 13.3334" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13.3334 2.66669V7.33335C13.3334 8.0406 13.0525 8.71888 12.5524 9.21897C12.0523 9.71907 11.374 10 10.6667 10H2.66675" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.00008 6.66669L2.66675 10L6.00008 13.3334" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.3334 2.66669V7.33335C13.3334 8.0406 13.0525 8.71888 12.5524 9.21897C12.0523 9.71907 11.374 10 10.6667 10H2.66675" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m3 5.778 1.256-1.256A5.417 5.417 0 0 1 8 3a5 5 0 1 1-4.583 7"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 3v3h3"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m3 5.778 1.256-1.256A5.417 5.417 0 0 1 8 3a5 5 0 1 1-4.583 7"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3 3v3h3"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m13 5.778-1.256-1.256A5.416 5.416 0 0 0 8 3a5 5 0 1 0 4.583 7"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 3v3h-3"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m13 5.778-1.256-1.256A5.416 5.416 0 0 0 8 3a5 5 0 1 0 4.583 7"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13 3v3h-3"/></svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.03641 5.53641L8.33797 7.83797M13.0825 3.0934L6.03641 10.1395M9.99896 9.49896L13.0825 12.5825M4.77932 6.05864C5.25123 6.05864 5.7038 5.87118 6.03749 5.53749C6.37118 5.2038 6.55864 4.75123 6.55864 4.27932C6.55864 3.80742 6.37118 3.35484 6.03749 3.02115C5.7038 2.68746 5.25123 2.5 4.77932 2.5C4.30742 2.5 3.85484 2.68746 3.52115 3.02115C3.18746 3.35484 3 3.80742 3 4.27932C3 4.75123 3.18746 5.2038 3.52115 5.53749C3.85484 5.87118 4.30742 6.05864 4.77932 6.05864ZM4.77932 13.1759C5.25123 13.1759 5.7038 12.9885 6.03749 12.6548C6.37118 12.3211 6.55864 11.8685 6.55864 11.3966C6.55864 10.9247 6.37118 10.4721 6.03749 10.1384C5.7038 9.80475 5.25123 9.61729 4.77932 9.61729C4.30742 9.61729 3.85484 9.80475 3.52115 10.1384C3.18746 10.4721 3 10.9247 3 11.3966C3 11.8685 3.18746 12.3211 3.52115 12.6548C3.85484 12.9885 4.30742 13.1759 4.77932 13.1759Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.03641 5.53641L8.33797 7.83797M13.0825 3.0934L6.03641 10.1395M9.99896 9.49896L13.0825 12.5825M4.77932 6.05864C5.25123 6.05864 5.7038 5.87118 6.03749 5.53749C6.37118 5.2038 6.55864 4.75123 6.55864 4.27932C6.55864 3.80742 6.37118 3.35484 6.03749 3.02115C5.7038 2.68746 5.25123 2.5 4.77932 2.5C4.30742 2.5 3.85484 2.68746 3.52115 3.02115C3.18746 3.35484 3 3.80742 3 4.27932C3 4.75123 3.18746 5.2038 3.52115 5.53749C3.85484 5.87118 4.30742 6.05864 4.77932 6.05864ZM4.77932 13.1759C5.25123 13.1759 5.7038 12.9885 6.03749 12.6548C6.37118 12.3211 6.55864 11.8685 6.55864 11.3966C6.55864 10.9247 6.37118 10.4721 6.03749 10.1384C5.7038 9.80475 5.25123 9.61729 4.77932 9.61729C4.30742 9.61729 3.85484 9.80475 3.52115 10.1384C3.18746 10.4721 3 10.9247 3 11.3966C3 11.8685 3.18746 12.3211 3.52115 12.6548C3.85484 12.9885 4.30742 13.1759 4.77932 13.1759Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.8 3H3.2C2.53726 3 2 3.51167 2 4.14286V9.85714C2 10.4883 2.53726 11 3.2 11H12.8C13.4627 11 14 10.4883 14 9.85714V4.14286C14 3.51167 13.4627 3 12.8 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.33325 14H10.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 11.3333V14" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8 3H3.2C2.53726 3 2 3.51167 2 4.14286V9.85714C2 10.4883 2.53726 11 3.2 11H12.8C13.4627 11 14 10.4883 14 9.85714V4.14286C14 3.51167 13.4627 3 12.8 3Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.33325 14H10.6666" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 11.3333V14" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.377 3.028a.25.25 0 0 0-.292.045.291.291 0 0 0-.067.303l1.496 4.236c.088.25.088.526 0 .776l-1.496 4.236a.291.291 0 0 0 .067.303.25.25 0 0 0 .292.045l9.472-4.72a.267.267 0 0 0 .11-.103.288.288 0 0 0-.11-.4L3.377 3.028ZM5 8h8"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.377 3.028a.25.25 0 0 0-.292.045.291.291 0 0 0-.067.303l1.496 4.236c.088.25.088.526 0 .776l-1.496 4.236a.291.291 0 0 0 .067.303.25.25 0 0 0 .292.045l9.472-4.72a.267.267 0 0 0 .11-.103.288.288 0 0 0-.11-.4L3.377 3.028ZM5 8h8"/></svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.8 9H3.2C2.53726 9 2 9.44772 2 10V12C2 12.5523 2.53726 13 3.2 13H12.8C13.4627 13 14 12.5523 14 12V10C14 9.44772 13.4627 9 12.8 9Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.8 3H3.2C2.53726 3 2 3.44772 2 4V6C2 6.55228 2.53726 7 3.2 7H12.8C13.4627 7 14 6.55228 14 6V4C14 3.44772 13.4627 3 12.8 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4 11H4.00667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4 5H4.00667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8 9H3.2C2.53726 9 2 9.44772 2 10V12C2 12.5523 2.53726 13 3.2 13H12.8C13.4627 13 14 12.5523 14 12V10C14 9.44772 13.4627 9 12.8 9Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8 3H3.2C2.53726 3 2 3.44772 2 4V6C2 6.55228 2.53726 7 3.2 7H12.8C13.4627 7 14 6.55228 14 6V4C14 3.44772 13.4627 3 12.8 3Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 11H4.00667" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 5H4.00667" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.0001 8.62505C13.0001 11.75 10.8126 13.3125 8.21266 14.2187C8.07651 14.2648 7.92862 14.2626 7.79392 14.2125C5.18771 13.3125 3.00024 11.75 3.00024 8.62505V4.25012C3.00024 4.08436 3.06609 3.92539 3.1833 3.80818C3.30051 3.69098 3.45948 3.62513 3.62523 3.62513C4.87521 3.62513 6.43769 2.87514 7.52517 1.92516C7.65758 1.81203 7.82601 1.74988 8.00016 1.74988C8.17431 1.74988 8.34275 1.81203 8.47515 1.92516C9.56889 2.88139 11.1251 3.62513 12.3751 3.62513C12.5408 3.62513 12.6998 3.69098 12.817 3.80818C12.9342 3.92539 13.0001 4.08436 13.0001 4.25012V8.62505Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 8.00002L7.33333 9.33335L10 6.66669" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.0001 8.62505C13.0001 11.75 10.8126 13.3125 8.21266 14.2187C8.07651 14.2648 7.92862 14.2626 7.79392 14.2125C5.18771 13.3125 3.00024 11.75 3.00024 8.62505V4.25012C3.00024 4.08436 3.06609 3.92539 3.1833 3.80818C3.30051 3.69098 3.45948 3.62513 3.62523 3.62513C4.87521 3.62513 6.43769 2.87514 7.52517 1.92516C7.65758 1.81203 7.82601 1.74988 8.00016 1.74988C8.17431 1.74988 8.34275 1.81203 8.47515 1.92516C9.56889 2.88139 11.1251 3.62513 12.3751 3.62513C12.5408 3.62513 12.6998 3.69098 12.817 3.80818C12.9342 3.92539 13.0001 4.08436 13.0001 4.25012V8.62505Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 8.00002L7.33333 9.33335L10 6.66669" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3.07136 7.95724L7.86916 3.05405C7.93967 2.98199 8.06036 2.98198 8.13087 3.05405L12.9286 7.95724C13.0866 8.11865 12.9652 8.38015 12.7324 8.38015H10.226V12.748C10.226 12.8872 10.1065 13 9.95892 13H6.04111C5.89358 13 5.77399 12.8872 5.77399 12.748V8.38015H3.26765C3.03479 8.38015 2.91342 8.11865 3.07136 7.95724Z" stroke="black" stroke-width="1.5"/>
+<path d="M3.07136 7.95724L7.86916 3.05405C7.93967 2.98199 8.06036 2.98198 8.13087 3.05405L12.9286 7.95724C13.0866 8.11865 12.9652 8.38015 12.7324 8.38015H10.226V12.748C10.226 12.8872 10.1065 13 9.95892 13H6.04111C5.89358 13 5.77399 12.8872 5.77399 12.748V8.38015H3.26765C3.03479 8.38015 2.91342 8.11865 3.07136 7.95724Z" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.9999 2.99988L2.99976 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.9999 2.99988L2.99976 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,8 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M2 5H4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M8 5L14 5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M12 11L14 11" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<path d="M2 11H8" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<circle cx="6" cy="5" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
-<circle cx="10" cy="11" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M2 5H4" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M8 5L14 5" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M12 11L14 11" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<path d="M2 11H8" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<circle cx="6" cy="5" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
+<circle cx="10" cy="11" r="2" fill="black" fill-opacity="0.1" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.9999 11.4V12C13.9999 12.3 13.6999 12.6 13.3999 12.6H2.59976C2.29976 12.6 1.99976 12.3 1.99976 12V11.4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.9999 11.4V12C13.9999 12.3 13.6999 12.6 13.3999 12.6H2.59976C2.29976 12.6 1.99976 12.3 1.99976 12V11.4" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.762 10.1a1.2 1.2 0 0 0-.862-.862l-3.68-.95a.3.3 0 0 1 0-.577l3.68-.95a1.2 1.2 0 0 0 .862-.86l.95-3.682a.3.3 0 0 1 .577 0L9.238 5.9a1.2 1.2 0 0 0 .862.862l3.68.949a.3.3 0 0 1 0 .578l-3.68.949a1.2 1.2 0 0 0-.862.862l-.95 3.68a.3.3 0 0 1-.577 0l-.949-3.68Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.762 10.1a1.2 1.2 0 0 0-.862-.862l-3.68-.95a.3.3 0 0 1 0-.577l3.68-.95a1.2 1.2 0 0 0 .862-.86l.95-3.682a.3.3 0 0 1 .577 0L9.238 5.9a1.2 1.2 0 0 0 .862.862l3.68.949a.3.3 0 0 1 0 .578l-3.68.949a1.2 1.2 0 0 0-.862.862l-.95 3.68a.3.3 0 0 1-.577 0l-.949-3.68Z"/></svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="8" y="2" width="6" height="12" fill="black" fill-opacity="0.25"/>
-<path d="M13.4 2H2.6C2.26863 2 2 2.26863 2 2.6V13.4C2 13.7314 2.26863 14 2.6 14H13.4C13.7314 14 14 13.7314 14 13.4V2.6C14 2.26863 13.7314 2 13.4 2Z" stroke="black" stroke-width="1.5"/>
-<path d="M8 2L8 14" stroke="black" stroke-width="1.5"/>
+<path d="M13.4 2H2.6C2.26863 2 2 2.26863 2 2.6V13.4C2 13.7314 2.26863 14 2.6 14H13.4C13.7314 14 14 13.7314 14 13.4V2.6C14 2.26863 13.7314 2 13.4 2Z" stroke="black" stroke-width="1.2"/>
+<path d="M8 2L8 14" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10 2.833h3v3M6 2.833H3v3"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 13.833V9.028a2.401 2.401 0 0 0-.165-.9 2.325 2.325 0 0 0-.486-.763L3 2.833M10 5.833l3-3"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M10 2.833h3v3M6 2.833H3v3"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 13.833V9.028a2.401 2.401 0 0 0-.165-.9 2.325 2.325 0 0 0-.486-.763L3 2.833M10 5.833l3-3"/></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="8" cy="8" r="1.25" fill="black" stroke="black" stroke-width="0.5"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 8H10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 8H10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6 8H10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 6V10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V12.6667C2 13.403 2.59695 14 3.33333 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6 8H10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 6V10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5 10.8V5.2C5 5.08954 5.08954 5 5.2 5H10.8C10.9105 5 11 5.08954 11 5.2V10.8C11 10.9105 10.9105 11 10.8 11H5.2C5.08954 11 5 10.9105 5 10.8Z" fill="black" stroke="black" stroke-width="1.5" stroke-linejoin="round"/>
+<path d="M5 10.8V5.2C5 5.08954 5.08954 5 5.2 5H10.8C10.9105 5 11 5.08954 11 5.2V10.8C11 10.9105 10.9105 11 10.8 11H5.2C5.08954 11 5 10.9105 5 10.8Z" fill="black" stroke="black" stroke-width="1.2" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7.333 11.333a2.667 2.667 0 1 1-5.333 0v-8A1.333 1.333 0 0 1 3.333 2H6a1.333 1.333 0 0 1 1.333 1.333v8Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M11.133 8.667h1.534A1.333 1.333 0 0 1 14 10v2.667A1.334 1.334 0 0 1 12.667 14h-8M4.667 11.333h.006"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7.333 5.333 8.867 3.8a1.6 1.6 0 0 1 2.269.003L12.4 5.067a1.6 1.6 0 0 1 .017 2.289L6.6 13.2"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M7.333 11.333a2.667 2.667 0 1 1-5.333 0v-8A1.333 1.333 0 0 1 3.333 2H6a1.333 1.333 0 0 1 1.333 1.333v8Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M11.133 8.667h1.534A1.333 1.333 0 0 1 14 10v2.667A1.334 1.334 0 0 1 12.667 14h-8M4.667 11.333h.006"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M7.333 5.333 8.867 3.8a1.6 1.6 0 0 1 2.269.003L12.4 5.067a1.6 1.6 0 0 1 .017 2.289L6.6 13.2"/></svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10.3333 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.33325 12L10.3333 8L6.33325 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 3.33331V12.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.3333 8H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.33325 12L10.3333 8L6.33325 4" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 3.33331V12.6666" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.2782 2.49951H3.72184C3.04677 2.49951 2.49951 3.04677 2.49951 3.72184V12.2782C2.49951 12.9532 3.04677 13.5005 3.72184 13.5005H12.2782C12.9532 13.5005 13.5005 12.9532 13.5005 12.2782V3.72184C13.5005 3.04677 12.9532 2.49951 12.2782 2.49951Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 10.7502H10.7502" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.24976 9.21777L7.08325 7.38428L5.24976 5.55078" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.2782 2.49951H3.72184C3.04677 2.49951 2.49951 3.04677 2.49951 3.72184V12.2782C2.49951 12.9532 3.04677 13.5005 3.72184 13.5005H12.2782C12.9532 13.5005 13.5005 12.9532 13.5005 12.2782V3.72184C13.5005 3.04677 12.9532 2.49951 12.2782 2.49951Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 10.7502H10.7502" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.24976 9.21777L7.08325 7.38428L5.24976 5.55078" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 12.375H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 11.125L6.75003 7.375L3 3.62497" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3.333 2A1.333 1.333 0 0 0 2 3.333M12.667 2A1.334 1.334 0 0 1 14 3.333M14 12.667A1.334 1.334 0 0 1 12.667 14M3.333 14A1.334 1.334 0 0 1 2 12.667M6 2h.667M6 14h.667M9.333 2H10M9.333 14H10M2 6v.667M14 6v.667M2 9.333V10M14 9.333V10M4.667 5.333H10M4.667 8h6.666M4.667 10.667h4"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 2A1.333 1.333 0 0 0 2 3.333M12.667 2A1.334 1.334 0 0 1 14 3.333M14 12.667A1.334 1.334 0 0 1 12.667 14M3.333 14A1.334 1.334 0 0 1 2 12.667M6 2h.667M6 14h.667M9.333 2H10M9.333 14H10M2 6v.667M14 6v.667M2 9.333V10M14 9.333V10M4.667 5.333H10M4.667 8h6.666M4.667 10.667h4"/></svg>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.33333 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.6667 5H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 11H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 7V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M14 9H10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.33333 8H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.6667 5H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 11H2" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 7V11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14 9H10" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.31254 12.549C7.3841 13.0987 8.61676 13.2476 9.78839 12.9688C10.96 12.6901 11.9936 12.0021 12.7028 11.0287C13.412 10.0554 13.7503 8.8607 13.6566 7.66002C13.5629 6.45934 13.0435 5.33159 12.1919 4.48C11.3403 3.62841 10.2126 3.10898 9.01188 3.01531C7.8112 2.92164 6.61655 3.2599 5.64319 3.96912C4.66984 4.67834 3.9818 5.71188 3.70306 6.88351C3.42432 8.05514 3.5732 9.2878 4.12289 10.3594L3 13.6719L6.31254 12.549Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.31254 12.549C7.3841 13.0987 8.61676 13.2476 9.78839 12.9688C10.96 12.6901 11.9936 12.0021 12.7028 11.0287C13.412 10.0554 13.7503 8.8607 13.6566 7.66002C13.5629 6.45934 13.0435 5.33159 12.1919 4.48C11.3403 3.62841 10.2126 3.10898 9.01188 3.01531C7.8112 2.92164 6.61655 3.2599 5.64319 3.96912C4.66984 4.67834 3.9818 5.71188 3.70306 6.88351C3.42432 8.05514 3.5732 9.2878 4.12289 10.3594L3 13.6719L6.31254 12.549Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.98795 10.4323C9.40771 10.9919 9.99294 11.4054 10.6607 11.614C11.3285 11.8226 12.045 11.8158 12.7087 11.5945C13.3724 11.3733 13.9497 10.9488 14.3588 10.3813C14.7678 9.81373 14.9879 9.13186 14.9879 8.43225C14.9879 7.6366 14.6719 6.87354 14.1093 6.31093C13.5467 5.74832 12.7836 5.43225 11.9879 5.43225C11.6685 5.43225 11.3595 5.47897 11.0677 5.56586C10.0571 5.86681 9.46945 6.84992 8.98796 7.78806V7.78806" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.01318 5.93652V8.60319H10.6799" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.01318 5.93652V8.60319H10.6799" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.00558 12.4263C6.16246 12.4494 5.3211 12.2612 4.56083 11.8712L1.24829 12.994L2.37119 9.68151C1.8215 8.60995 1.67261 7.37729 1.95135 6.20566C2.23009 5.03403 2.91813 4.00048 3.89148 3.29126C4.86484 2.58204 6.05949 2.24379 7.26018 2.33746C7.86645 2.38475 8.45413 2.54061 8.99705 2.79296" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.98795 10.4323C9.40771 10.9919 9.99294 11.4054 10.6607 11.614C11.3285 11.8226 12.045 11.8158 12.7087 11.5945C13.3724 11.3733 13.9497 10.9488 14.3588 10.3813C14.7678 9.81373 14.9879 9.13186 14.9879 8.43225C14.9879 7.6366 14.6719 6.87354 14.1093 6.31093C13.5467 5.74832 12.7836 5.43225 11.9879 5.43225C11.6685 5.43225 11.3595 5.47897 11.0677 5.56586C10.0571 5.86681 9.46945 6.84992 8.98796 7.78806V7.78806" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.01318 5.93652V8.60319H10.6799" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.01318 5.93652V8.60319H10.6799" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.00558 12.4263C6.16246 12.4494 5.3211 12.2612 4.56083 11.8712L1.24829 12.994L2.37119 9.68151C1.8215 8.60995 1.67261 7.37729 1.95135 6.20566C2.23009 5.03403 2.91813 4.00048 3.89148 3.29126C4.86484 2.58204 6.05949 2.24379 7.26018 2.33746C7.86645 2.38475 8.45413 2.54061 8.99705 2.79296" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" clip-path="url(#a)"><path d="M3.013 8.57h2.478V3.2H3.013a.413.413 0 0 0-.413.413v4.544a.413.413 0 0 0 .413.413ZM5.491 8.57l2.066 4.13a1.652 1.652 0 0 0 1.652-1.652v-1.24h3.304a.826.826 0 0 0 .82-.929l-.62-4.956a.827.827 0 0 0-.82-.723H5.492"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="M3.013 8.57h2.478V3.2H3.013a.413.413 0 0 0-.413.413v4.544a.413.413 0 0 0 .413.413ZM5.491 8.57l2.066 4.13a1.652 1.652 0 0 0 1.652-1.652v-1.24h3.304a.826.826 0 0 0 .82-.929l-.62-4.956a.827.827 0 0 0-.82-.723H5.492"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" clip-path="url(#a)"><path d="M3.013 7.33h2.478v5.37H3.013a.413.413 0 0 1-.413-.413V7.743a.413.413 0 0 1 .413-.413ZM5.491 7.33 7.557 3.2a1.652 1.652 0 0 1 1.652 1.652v1.24h3.304a.826.826 0 0 1 .82.929l-.62 4.956a.827.827 0 0 1-.82.723H5.492"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" clip-path="url(#a)"><path d="M3.013 7.33h2.478v5.37H3.013a.413.413 0 0 1-.413-.413V7.743a.413.413 0 0 1 .413-.413ZM5.491 7.33 7.557 3.2a1.652 1.652 0 0 1 1.652 1.652v1.24h3.304a.826.826 0 0 1 .82.929l-.62 4.956a.827.827 0 0 1-.82.723H5.492"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 13A5 5 0 1 0 8 3a5 5 0 0 0 0 10Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m5.949 9.026 1.538 1.025 2.564-3.59"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 13A5 5 0 1 0 8 3a5 5 0 0 0 0 10Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="m5.949 9.026 1.538 1.025 2.564-3.59"/></svg>
@@ -1,10 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,11 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8.00016 8.66665C8.36835 8.66665 8.66683 8.36817 8.66683 7.99998C8.66683 7.63179 8.36835 7.33331 8.00016 7.33331C7.63197 7.33331 7.3335 7.63179 7.3335 7.99998C7.3335 8.36817 7.63197 8.66665 8.00016 8.66665Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.00016 8.66665C8.36835 8.66665 8.66683 8.36817 8.66683 7.99998C8.66683 7.63179 8.36835 7.33331 8.00016 7.33331C7.63197 7.33331 7.3335 7.63179 7.3335 7.99998C7.3335 8.36817 7.63197 8.66665 8.00016 8.66665Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.5 2.5H6.5C6.22386 2.5 6 2.83579 6 3.25V4.75C6 5.16421 6.22386 5.5 6.5 5.5H9.5C9.77614 5.5 10 5.16421 10 4.75V3.25C10 2.83579 9.77614 2.5 9.5 2.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 3.5H11C11.2652 3.5 11.5196 3.61706 11.7071 3.82544C11.8946 4.03381 12 4.31643 12 4.61111V12.3889C12 12.6836 11.8946 12.9662 11.7071 13.1746C11.5196 13.3829 11.2652 13.5 11 13.5H5C4.73478 13.5 4.48043 13.3829 4.29289 13.1746C4.10536 12.9662 4 12.6836 4 12.3889V4.61111C4 4.31643 4.10536 4.03381 4.29289 3.82544C4.48043 3.61706 4.73478 3.5 5 3.5H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.5 2.5H6.5C6.22386 2.5 6 2.83579 6 3.25V4.75C6 5.16421 6.22386 5.5 6.5 5.5H9.5C9.77614 5.5 10 5.16421 10 4.75V3.25C10 2.83579 9.77614 2.5 9.5 2.5Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 3.5H11C11.2652 3.5 11.5196 3.61706 11.7071 3.82544C11.8946 4.03381 12 4.31643 12 4.61111V12.3889C12 12.6836 11.8946 12.9662 11.7071 13.1746C11.5196 13.3829 11.2652 13.5 11 13.5H5C4.73478 13.5 4.48043 13.3829 4.29289 13.1746C4.10536 12.9662 4 12.6836 4 12.3889V4.61111C4 4.31643 4.10536 4.03381 4.29289 3.82544C4.48043 3.61706 4.73478 3.5 5 3.5H6" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.50002 2.5H5C4.73478 2.5 4.48043 2.6159 4.29289 2.82219C4.10535 3.02848 4 3.30826 4 3.6V12.3999C4 12.6917 4.10535 12.9715 4.29289 13.1778C4.48043 13.3841 4.73478 13.5 5 13.5H11C11.2652 13.5 11.5195 13.3841 11.7071 13.1778C11.8946 12.9715 12 12.6917 12 12.3999V5.25L9.50002 2.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9.3427 6.82379L6.65698 9.5095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.65698 6.82379L9.3427 9.5095" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.50002 2.5H5C4.73478 2.5 4.48043 2.6159 4.29289 2.82219C4.10535 3.02848 4 3.30826 4 3.6V12.3999C4 12.6917 4.10535 12.9715 4.29289 13.1778C4.48043 13.3841 4.73478 13.5 5 13.5H11C11.2652 13.5 11.5195 13.3841 11.7071 13.1778C11.8946 12.9715 12 12.6917 12 12.3999V5.25L9.50002 2.5Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.3427 6.82379L6.65698 9.5095" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.65698 6.82379L9.3427 9.5095" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13.7244 11.5299L9.01711 3.2922C8.91447 3.11109 8.76562 2.96045 8.58576 2.85564C8.4059 2.75084 8.20145 2.69562 7.99328 2.69562C7.7851 2.69562 7.58066 2.75084 7.40079 2.85564C7.22093 2.96045 7.07209 3.11109 6.96945 3.2922L2.26218 11.5299C2.15844 11.7096 2.10404 11.9135 2.1045 12.121C2.10495 12.3285 2.16026 12.5321 2.2648 12.7113C2.36934 12.8905 2.5194 13.0389 2.69978 13.1415C2.88015 13.244 3.08443 13.297 3.2919 13.2951H12.7064C12.9129 13.2949 13.1157 13.2404 13.2944 13.137C13.4731 13.0336 13.6215 12.8851 13.7247 12.7062C13.8278 12.5273 13.8821 12.3245 13.882 12.118C13.882 11.9115 13.8276 11.7087 13.7244 11.5299Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.99927 6.23425V8.58788" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.99927 10.9415H8.00492" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13.7244 11.5299L9.01711 3.2922C8.91447 3.11109 8.76562 2.96045 8.58576 2.85564C8.4059 2.75084 8.20145 2.69562 7.99328 2.69562C7.7851 2.69562 7.58066 2.75084 7.40079 2.85564C7.22093 2.96045 7.07209 3.11109 6.96945 3.2922L2.26218 11.5299C2.15844 11.7096 2.10404 11.9135 2.1045 12.121C2.10495 12.3285 2.16026 12.5321 2.2648 12.7113C2.36934 12.8905 2.5194 13.0389 2.69978 13.1415C2.88015 13.244 3.08443 13.297 3.2919 13.2951H12.7064C12.9129 13.2949 13.1157 13.2404 13.2944 13.137C13.4731 13.0336 13.6215 12.8851 13.7247 12.7062C13.8278 12.5273 13.8821 12.3245 13.882 12.118C13.882 11.9115 13.8276 11.7087 13.7244 11.5299Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99927 6.23425V8.58788" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99927 10.9415H8.00492" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.8 13C13.1183 13 13.4235 12.8761 13.6486 12.6554C13.8735 12.4349 14 12.1356 14 11.8236V5.94118C14 5.62916 13.8735 5.32992 13.6486 5.10929C13.4235 4.88866 13.1183 4.76471 12.8 4.76471H8.06C7.8593 4.76664 7.66133 4.71919 7.48418 4.6267C7.30703 4.53421 7.15637 4.39964 7.046 4.2353L6.56 3.52941C6.45073 3.36675 6.30199 3.23322 6.1271 3.14082C5.95221 3.04842 5.75666 3.00004 5.558 3H3.2C2.88174 3 2.57651 3.12395 2.35148 3.34458C2.12643 3.56521 2 3.86445 2 4.17647V11.8236C2 12.1356 2.12643 12.4349 2.35148 12.6554C2.57651 12.8761 2.88174 13 3.2 13H12.8Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8 13C13.1183 13 13.4235 12.8761 13.6486 12.6554C13.8735 12.4349 14 12.1356 14 11.8236V5.94118C14 5.62916 13.8735 5.32992 13.6486 5.10929C13.4235 4.88866 13.1183 4.76471 12.8 4.76471H8.06C7.8593 4.76664 7.66133 4.71919 7.48418 4.6267C7.30703 4.53421 7.15637 4.39964 7.046 4.2353L6.56 3.52941C6.45073 3.36675 6.30199 3.23322 6.1271 3.14082C5.95221 3.04842 5.75666 3.00004 5.558 3H3.2C2.88174 3 2.57651 3.12395 2.35148 3.34458C2.12643 3.56521 2 3.86445 2 4.17647V11.8236C2 12.1356 2.12643 12.4349 2.35148 12.6554C2.57651 12.8761 2.88174 13 3.2 13H12.8Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9 8.5L4.94864 12.6222C4.71647 12.8544 4.40157 12.9848 4.07323 12.9848C3.74488 12.9848 3.42999 12.8544 3.19781 12.6222C2.96564 12.39 2.83521 12.0751 2.83521 11.7468C2.83521 11.4185 2.96564 11.1036 3.19781 10.8714L7.5 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.8352 9.98474L13.8352 6.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.8352 7.42495L11.7634 6.4298C11.5533 6.23484 11.4353 5.97039 11.4352 5.69462V5.08526L10.1696 3.91022C9.54495 3.33059 8.69961 3.00261 7.81649 2.99722L5.83521 2.98474L6.35041 3.41108C6.71634 3.71233 7.00935 4.08216 7.21013 4.4962C7.4109 4.91024 7.51488 5.35909 7.51521 5.81316L7.5 6.5L9 8.5L9.5 8C9.5 8 9.87337 7.79457 10.0834 7.98959L11.1552 8.98474" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 8.5L4.94864 12.6222C4.71647 12.8544 4.40157 12.9848 4.07323 12.9848C3.74488 12.9848 3.42999 12.8544 3.19781 12.6222C2.96564 12.39 2.83521 12.0751 2.83521 11.7468C2.83521 11.4185 2.96564 11.1036 3.19781 10.8714L7.5 6.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.8352 9.98474L13.8352 6.98474" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.8352 7.42495L11.7634 6.4298C11.5533 6.23484 11.4353 5.97039 11.4352 5.69462V5.08526L10.1696 3.91022C9.54495 3.33059 8.69961 3.00261 7.81649 2.99722L5.83521 2.98474L6.35041 3.41108C6.71634 3.71233 7.00935 4.08216 7.21013 4.4962C7.4109 4.91024 7.51488 5.35909 7.51521 5.81316L7.5 6.5L9 8.5L9.5 8C9.5 8 9.87337 7.79457 10.0834 7.98959L11.1552 8.98474" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.5 12C6.65203 12.304 6.87068 12.5565 7.13399 12.7321C7.39729 12.9076 7.69597 13 8 13C8.30403 13 8.60271 12.9076 8.86601 12.7321C9.12932 12.5565 9.34797 12.304 9.5 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.63088 9.21874C3.56556 9.28556 3.52246 9.36865 3.50681 9.45791C3.49116 9.54718 3.50364 9.63876 3.54273 9.72152C3.58183 9.80429 3.64585 9.87467 3.72701 9.92409C3.80817 9.97352 3.90298 9.99987 3.99989 9.99994H12.0001C12.097 9.99997 12.1918 9.97372 12.273 9.92439C12.3542 9.87505 12.4183 9.80476 12.4575 9.72205C12.4967 9.63934 12.5093 9.54778 12.4938 9.45851C12.4783 9.36924 12.4353 9.2861 12.3701 9.21921C11.705 8.57941 11 7.89947 11 5.79994C11 5.05733 10.684 4.34514 10.1213 3.82004C9.55872 3.29494 8.79564 2.99994 7.99997 2.99994C7.20431 2.99994 6.44123 3.29494 5.87861 3.82004C5.31599 4.34514 4.99991 5.05733 4.99991 5.79994C4.99991 7.89947 4.2944 8.57941 3.63088 9.21874Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.5 12C6.65203 12.304 6.87068 12.5565 7.13399 12.7321C7.39729 12.9076 7.69597 13 8 13C8.30403 13 8.60271 12.9076 8.86601 12.7321C9.12932 12.5565 9.34797 12.304 9.5 12" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.63088 9.21874C3.56556 9.28556 3.52246 9.36865 3.50681 9.45791C3.49116 9.54718 3.50364 9.63876 3.54273 9.72152C3.58183 9.80429 3.64585 9.87467 3.72701 9.92409C3.80817 9.97352 3.90298 9.99987 3.99989 9.99994H12.0001C12.097 9.99997 12.1918 9.97372 12.273 9.92439C12.3542 9.87505 12.4183 9.80476 12.4575 9.72205C12.4967 9.63934 12.5093 9.54778 12.4938 9.45851C12.4783 9.36924 12.4353 9.2861 12.3701 9.21921C11.705 8.57941 11 7.89947 11 5.79994C11 5.05733 10.684 4.34514 10.1213 3.82004C9.55872 3.29494 8.79564 2.99994 7.99997 2.99994C7.20431 2.99994 6.44123 3.29494 5.87861 3.82004C5.31599 4.34514 4.99991 5.05733 4.99991 5.79994C4.99991 7.89947 4.2944 8.57941 3.63088 9.21874Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.5871 5.40582C12.8514 5.14152 13 4.78304 13 4.40922C13 4.03541 12.8516 3.67688 12.5873 3.41252C12.323 3.14816 11.9645 2.99962 11.5907 2.99957C11.2169 2.99953 10.8584 3.14798 10.594 3.41227L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L12.5871 5.40582Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M9 5L11 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.5871 5.40582C12.8514 5.14152 13 4.78304 13 4.40922C13 4.03541 12.8516 3.67688 12.5873 3.41252C12.323 3.14816 11.9645 2.99962 11.5907 2.99957C11.2169 2.99953 10.8584 3.14798 10.594 3.41227L3.92098 10.0869C3.80488 10.2027 3.71903 10.3452 3.67097 10.5019L3.01047 12.678C2.99754 12.7212 2.99657 12.7672 3.00764 12.8109C3.01872 12.8547 3.04143 12.8946 3.07337 12.9265C3.1053 12.9584 3.14528 12.981 3.18905 12.992C3.23282 13.003 3.27875 13.002 3.32197 12.989L5.49849 12.329C5.65508 12.2813 5.79758 12.196 5.91349 12.0805L12.5871 5.40582Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 5L11 7" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,7 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 5.66667V4.33333C3 3.97971 3.14048 3.64057 3.39052 3.39052C3.64057 3.14048 3.97971 3 4.33333 3H5.66667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10.3333 3H11.6666C12.0202 3 12.3593 3.14048 12.6094 3.39052C12.8594 3.64057 12.9999 3.97971 12.9999 4.33333V5.66667" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.9999 10.3333V11.6666C12.9999 12.0203 12.8594 12.3594 12.6094 12.6095C12.3593 12.8595 12.0202 13 11.6666 13H10.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.66667 13H4.33333C3.97971 13 3.64057 12.8595 3.39052 12.6095C3.14048 12.3594 3 12.0203 3 11.6666V10.3333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.5 8H10.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 5.66667V4.33333C3 3.97971 3.14048 3.64057 3.39052 3.39052C3.64057 3.14048 3.97971 3 4.33333 3H5.66667" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.3333 3H11.6666C12.0202 3 12.3593 3.14048 12.6094 3.39052C12.8594 3.64057 12.9999 3.97971 12.9999 4.33333V5.66667" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.9999 10.3333V11.6666C12.9999 12.0203 12.8594 12.3594 12.6094 12.6095C12.3593 12.8595 12.0202 13 11.6666 13H10.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.66667 13H4.33333C3.97971 13 3.64057 12.8595 3.39052 12.6095C3.14048 12.3594 3 12.0203 3 11.6666V10.3333" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.5 8H10.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.57132 13.7143C5.20251 13.7143 5.71418 13.2026 5.71418 12.5714C5.71418 11.9403 5.20251 11.4286 4.57132 11.4286C3.94014 11.4286 3.42847 11.9403 3.42847 12.5714C3.42847 13.2026 3.94014 13.7143 4.57132 13.7143Z" fill="black"/>
-<path d="M10.2856 2.85712V5.71426M10.2856 5.71426V8.5714M10.2856 5.71426H13.1428M10.2856 5.71426H7.42847M10.2856 5.71426L12.1904 3.80949M10.2856 5.71426L8.38084 7.61906M10.2856 5.71426L12.1904 7.61906M10.2856 5.71426L8.38084 3.80949" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
+<path d="M10.2856 2.85712V5.71426M10.2856 5.71426V8.5714M10.2856 5.71426H13.1428M10.2856 5.71426H7.42847M10.2856 5.71426L12.1904 3.80949M10.2856 5.71426L8.38084 7.61906M10.2856 5.71426L12.1904 7.61906M10.2856 5.71426L8.38084 3.80949" stroke="black" stroke-width="1.2" stroke-linecap="round"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13 13L11 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M7.5 12C9.98528 12 12 9.98528 12 7.5C12 5.01472 9.98528 3 7.5 3C5.01472 3 3 5.01472 3 7.5C3 9.98528 5.01472 12 7.5 12Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 13L11 11" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.5 12C9.98528 12 12 9.98528 12 7.5C12 5.01472 9.98528 3 7.5 3C5.01472 3 3 5.01472 3 7.5C3 9.98528 5.01472 12 7.5 12Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12.2782 2.49951H3.72184C3.04677 2.49951 2.49951 3.04677 2.49951 3.72184V12.2782C2.49951 12.9532 3.04677 13.5005 3.72184 13.5005H12.2782C12.9532 13.5005 13.5005 12.9532 13.5005 12.2782V3.72184C13.5005 3.04677 12.9532 2.49951 12.2782 2.49951Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 10.7502H10.7502" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M5.24976 9.21777L7.08325 7.38428L5.24976 5.55078" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.2782 2.49951H3.72184C3.04677 2.49951 2.49951 3.04677 2.49951 3.72184V12.2782C2.49951 12.9532 3.04677 13.5005 3.72184 13.5005H12.2782C12.9532 13.5005 13.5005 12.9532 13.5005 12.2782V3.72184C13.5005 3.04677 12.9532 2.49951 12.2782 2.49951Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 10.7502H10.7502" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.24976 9.21777L7.08325 7.38428L5.24976 5.55078" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M9.95231 10.2159C10.0803 9.58974 9.95231 9.57261 10.9111 8.46959C11.4686 7.82822 11.8699 7.09214 11.8699 6.27818C11.8699 5.28184 11.4658 4.32631 10.7467 3.62179C10.0275 2.91728 9.05201 2.52148 8.03492 2.52148C7.01782 2.52148 6.04239 2.91728 5.32319 3.62179C4.604 4.32631 4.19995 5.28184 4.19995 6.27818C4.19995 6.9043 4.32779 7.65565 5.1587 8.46959C6.11744 9.59098 5.98965 9.58974 6.11748 10.2159M9.95231 10.2159V12.2989C9.95231 12.9504 9.41327 13.4786 8.7482 13.4786H7.32165C6.65658 13.4786 6.11744 12.9504 6.11744 12.2989L6.11748 10.2159M9.95231 10.2159H8.03492H6.11748" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9.9526 10.2625C10.0833 9.62316 9.9526 9.60566 10.9315 8.47946C11.5008 7.82461 11.9105 7.07306 11.9105 6.242C11.9105 5.22472 11.4979 4.2491 10.7637 3.52978C10.0294 2.81046 9.03338 2.40634 7.99491 2.40634C6.95644 2.40634 5.96051 2.81046 5.22619 3.52978C4.49189 4.2491 4.07935 5.22472 4.07935 6.242C4.07935 6.88128 4.20987 7.64842 5.05825 8.47946C6.03714 9.62442 5.90666 9.62316 6.03718 10.2625M9.9526 10.2625V12.3893C9.9526 13.0544 9.40223 13.5937 8.72319 13.5937H7.26665C6.58761 13.5937 6.03714 13.0544 6.03714 12.3893L6.03718 10.2625M9.9526 10.2625H7.99491H6.03718" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.99993 13.4804C11.0267 13.4804 13.4803 11.0267 13.4803 7.99999C13.4803 4.97325 11.0267 2.51959 7.99993 2.51959C4.97319 2.51959 2.51953 4.97325 2.51953 7.99999C2.51953 11.0267 4.97319 13.4804 7.99993 13.4804Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M8 3C6.71611 4.34807 6 6.13836 6 7.99999C6 9.86163 6.71611 11.6519 8 13C9.28387 11.6519 10 9.86163 10 7.99999C10 6.13836 9.28387 4.34807 8 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.24121 7.04827C4.52425 8.27022 6.22817 8.95178 7.99999 8.95178C9.77182 8.95178 11.4757 8.27022 12.7588 7.04827" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.99993 13.4804C11.0267 13.4804 13.4803 11.0267 13.4803 7.99999C13.4803 4.97325 11.0267 2.51959 7.99993 2.51959C4.97319 2.51959 2.51953 4.97325 2.51953 7.99999C2.51953 11.0267 4.97319 13.4804 7.99993 13.4804Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 3C6.71611 4.34807 6 6.13836 6 7.99999C6 9.86163 6.71611 11.6519 8 13C9.28387 11.6519 10 9.86163 10 7.99999C10 6.13836 9.28387 4.34807 8 3Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.24121 7.04827C4.52425 8.27022 6.22817 8.95178 7.99999 8.95178C9.77182 8.95178 11.4757 8.27022 12.7588 7.04827" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M3 5L13 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12 5V12.875C12 13.4375 11.4286 14 10.8571 14H5.14286C4.57143 14 4 13.4375 4 12.875V5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M10 5V3C10 2.44772 9.55228 2 9 2H7C6.44772 2 6 2.44772 6 3V5" stroke="black" stroke-width="1.5"/>
+<path d="M3 5L13 5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 5V12.875C12 13.4375 11.4286 14 10.8571 14H5.14286C4.57143 14 4 13.4375 4 12.875V5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10 5V3C10 2.44772 9.55228 2 9 2H7C6.44772 2 6 2.44772 6 3V5" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.6 5v3.6h3.6"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.4 11A5.4 5.4 0 0 0 8 5.6a5.4 5.4 0 0 0-3.6 1.38L2.6 8.6"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.6 5v3.6h3.6"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13.4 11A5.4 5.4 0 0 0 8 5.6a5.4 5.4 0 0 0-3.6 1.38L2.6 8.6"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2 13.4a4.8 4.8 0 0 1 7.975-3.6"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.8 8.6a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM10.4 12.2l1.2 1.2L14 11"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2 13.4a4.8 4.8 0 0 1 7.975-3.6"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.8 8.6a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM10.4 12.2l1.2 1.2L14 11"/></svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6.79118 8.27005C8.27568 8.27005 9.4791 7.06663 9.4791 5.58214C9.4791 4.09765 8.27568 2.89423 6.79118 2.89423C5.30669 2.89423 4.10327 4.09765 4.10327 5.58214C4.10327 7.06663 5.30669 8.27005 6.79118 8.27005Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M6.79112 8.60443C4.19441 8.60443 2.08936 10.7095 2.08936 13.3062H11.4929C11.4929 10.7095 9.38784 8.60443 6.79112 8.60443Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M14.6984 12.9263C14.6984 10.8893 13.4895 8.99736 12.2806 8.09067C12.6779 7.79254 12.9957 7.40104 13.2057 6.95083C13.4157 6.50062 13.5115 6.00558 13.4846 5.50952C13.4577 5.01346 13.309 4.53168 13.0515 4.10681C12.7941 3.68194 12.4358 3.3271 12.0085 3.07367" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.79118 8.27005C8.27568 8.27005 9.4791 7.06663 9.4791 5.58214C9.4791 4.09765 8.27568 2.89423 6.79118 2.89423C5.30669 2.89423 4.10327 4.09765 4.10327 5.58214C4.10327 7.06663 5.30669 8.27005 6.79118 8.27005Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.79112 8.60443C4.19441 8.60443 2.08936 10.7095 2.08936 13.3062H11.4929C11.4929 10.7095 9.38784 8.60443 6.79112 8.60443Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14.6984 12.9263C14.6984 10.8893 13.4895 8.99736 12.2806 8.09067C12.6779 7.79254 12.9957 7.40104 13.2057 6.95083C13.4157 6.50062 13.5115 6.00558 13.4846 5.50952C13.4577 5.01346 13.309 4.53168 13.0515 4.10681C12.7941 3.68194 12.4358 3.3271 12.0085 3.07367" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2 13.433a4.8 4.8 0 0 1 6.493-4.492M13.627 10.809a1.275 1.275 0 0 0-1.803-1.803l-2.406 2.408a1.2 1.2 0 0 0-.303.512l-.502 1.722a.3.3 0 0 0 .372.372l1.722-.502c.193-.057.37-.161.512-.304l2.408-2.405Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M6.8 8.633a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2 13.433a4.8 4.8 0 0 1 6.493-4.492M13.627 10.809a1.275 1.275 0 0 0-1.803-1.803l-2.406 2.408a1.2 1.2 0 0 0-.303.512l-.502 1.722a.3.3 0 0 0 .372.372l1.722-.502c.193-.057.37-.161.512-.304l2.408-2.405Z"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M6.8 8.633a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/></svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.84 11.6 9.037 3.199a1.2 1.2 0 0 0-2.089 0l-4.802 8.403a1.2 1.2 0 0 0 1.05 1.8h9.604a1.201 1.201 0 0 0 1.038-1.8ZM8 6v2.667M8 11.333h.007"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13.84 11.6 9.037 3.199a1.2 1.2 0 0 0-2.089 0l-4.802 8.403a1.2 1.2 0 0 0 1.05 1.8h9.604a1.201 1.201 0 0 0 1.038-1.8ZM8 6v2.667M8 11.333h.007"/></svg>
@@ -1 +1 @@
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path fill="#000" fill-opacity=".157" stroke="#000" stroke-linejoin="round" stroke-width="1.5" d="M9.864 3a.5.5 0 0 1 .353.146l2.636 2.636a.5.5 0 0 1 .147.354v3.728a.5.5 0 0 1-.146.353l-2.637 2.637a.5.5 0 0 1-.353.146H6.136a.5.5 0 0 1-.354-.146l-2.636-2.636A.5.5 0 0 1 3 9.864V6.136a.5.5 0 0 1 .146-.354l2.636-2.636A.5.5 0 0 1 6.136 3h3.728Z"/><path stroke="#000" stroke-linecap="round" stroke-width="1.5" d="m9.5 6.5-3 3m3 0-3-3"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path fill="#000" fill-opacity=".157" stroke="#000" stroke-linejoin="round" stroke-width="1.2" d="M9.864 3a.5.5 0 0 1 .353.146l2.636 2.636a.5.5 0 0 1 .147.354v3.728a.5.5 0 0 1-.146.353l-2.637 2.637a.5.5 0 0 1-.353.146H6.136a.5.5 0 0 1-.354-.146l-2.636-2.636A.5.5 0 0 1 3 9.864V6.136a.5.5 0 0 1 .146-.354l2.636-2.636A.5.5 0 0 1 6.136 3h3.728Z"/><path stroke="#000" stroke-linecap="round" stroke-width="1.2" d="m9.5 6.5-3 3m3 0-3-3"/></svg>
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8 14C4.68629 14 2 11.3137 2 8C2 4.68629 4.68629 2 8 2ZM10.4238 5.57617C10.1895 5.34187 9.81049 5.3419 9.57617 5.57617L8 7.15234L6.42383 5.57617C6.18953 5.34187 5.81049 5.3419 5.57617 5.57617C5.34186 5.81049 5.34186 6.18951 5.57617 6.42383L7.15234 8L5.57617 9.57617C5.34186 9.81049 5.34186 10.1895 5.57617 10.4238C5.81049 10.6581 6.18954 10.6581 6.42383 10.4238L8 8.84766L9.57617 10.4238C9.81049 10.6581 10.1895 10.6581 10.4238 10.4238C10.6581 10.1895 10.658 9.81048 10.4238 9.57617L8.84766 8L10.4238 6.42383C10.6581 6.18954 10.658 5.81048 10.4238 5.57617Z" fill="black"/>
+</svg>
@@ -0,0 +1,27 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11 8.75V10.5C8.93097 10.5 8.06903 10.5 6 10.5V10L11 6V5.5H6V7.25" stroke="black" stroke-width="1.5"/>
+<path d="M2 8.5C2.27614 8.5 2.5 8.27614 2.5 8C2.5 7.72386 2.27614 7.5 2 7.5C1.72386 7.5 1.5 7.72386 1.5 8C1.5 8.27614 1.72386 8.5 2 8.5Z" fill="black"/>
+<path opacity="0.6" d="M2.99976 6.33002C3.2759 6.33002 3.49976 6.10616 3.49976 5.83002C3.49976 5.55387 3.2759 5.33002 2.99976 5.33002C2.72361 5.33002 2.49976 5.55387 2.49976 5.83002C2.49976 6.10616 2.72361 6.33002 2.99976 6.33002Z" fill="black"/>
+<path opacity="0.6" d="M2.99976 10.66C3.2759 10.66 3.49976 10.4361 3.49976 10.16C3.49976 9.88383 3.2759 9.65997 2.99976 9.65997C2.72361 9.65997 2.49976 9.88383 2.49976 10.16C2.49976 10.4361 2.72361 10.66 2.99976 10.66Z" fill="black"/>
+<path d="M15 8.5C15.2761 8.5 15.5 8.27614 15.5 8C15.5 7.72386 15.2761 7.5 15 7.5C14.7239 7.5 14.5 7.72386 14.5 8C14.5 8.27614 14.7239 8.5 15 8.5Z" fill="black"/>
+<path opacity="0.6" d="M14 6.33002C14.2761 6.33002 14.5 6.10616 14.5 5.83002C14.5 5.55387 14.2761 5.33002 14 5.33002C13.7239 5.33002 13.5 5.55387 13.5 5.83002C13.5 6.10616 13.7239 6.33002 14 6.33002Z" fill="black"/>
+<path opacity="0.6" d="M14 10.66C14.2761 10.66 14.5 10.4361 14.5 10.16C14.5 9.88383 14.2761 9.65997 14 9.65997C13.7239 9.65997 13.5 9.88383 13.5 10.16C13.5 10.4361 13.7239 10.66 14 10.66Z" fill="black"/>
+<path d="M8.49219 2C8.76833 2 8.99219 1.77614 8.99219 1.5C8.99219 1.22386 8.76833 1 8.49219 1C8.21605 1 7.99219 1.22386 7.99219 1.5C7.99219 1.77614 8.21605 2 8.49219 2Z" fill="black"/>
+<path opacity="0.6" d="M6 3C6.27614 3 6.5 2.77614 6.5 2.5C6.5 2.22386 6.27614 2 6 2C5.72386 2 5.5 2.22386 5.5 2.5C5.5 2.77614 5.72386 3 6 3Z" fill="black"/>
+<path d="M4 4C4.27614 4 4.5 3.77614 4.5 3.5C4.5 3.22386 4.27614 3 4 3C3.72386 3 3.5 3.22386 3.5 3.5C3.5 3.77614 3.72386 4 4 4Z" fill="black"/>
+<path d="M3.99976 13C4.2759 13 4.49976 12.7761 4.49976 12.5C4.49976 12.2239 4.2759 12 3.99976 12C3.72361 12 3.49976 12.2239 3.49976 12.5C3.49976 12.7761 3.72361 13 3.99976 13Z" fill="black"/>
+<path opacity="0.2" d="M2 12.5C2.27614 12.5 2.5 12.2761 2.5 12C2.5 11.7239 2.27614 11.5 2 11.5C1.72386 11.5 1.5 11.7239 1.5 12C1.5 12.2761 1.72386 12.5 2 12.5Z" fill="black"/>
+<path opacity="0.2" d="M2 4.5C2.27614 4.5 2.5 4.27614 2.5 4C2.5 3.72386 2.27614 3.5 2 3.5C1.72386 3.5 1.5 3.72386 1.5 4C1.5 4.27614 1.72386 4.5 2 4.5Z" fill="black"/>
+<path opacity="0.2" d="M15 12.5C15.2761 12.5 15.5 12.2761 15.5 12C15.5 11.7239 15.2761 11.5 15 11.5C14.7239 11.5 14.5 11.7239 14.5 12C14.5 12.2761 14.7239 12.5 15 12.5Z" fill="black"/>
+<path opacity="0.2" d="M15 4.5C15.2761 4.5 15.5 4.27614 15.5 4C15.5 3.72386 15.2761 3.5 15 3.5C14.7239 3.5 14.5 3.72386 14.5 4C14.5 4.27614 14.7239 4.5 15 4.5Z" fill="black"/>
+<path opacity="0.5" d="M3.99976 15C4.2759 15 4.49976 14.7761 4.49976 14.5C4.49976 14.2239 4.2759 14 3.99976 14C3.72361 14 3.49976 14.2239 3.49976 14.5C3.49976 14.7761 3.72361 15 3.99976 15Z" fill="black"/>
+<path opacity="0.5" d="M4 2C4.27614 2 4.5 1.77614 4.5 1.5C4.5 1.22386 4.27614 1 4 1C3.72386 1 3.5 1.22386 3.5 1.5C3.5 1.77614 3.72386 2 4 2Z" fill="black"/>
+<path opacity="0.5" d="M13 15C13.2761 15 13.5 14.7761 13.5 14.5C13.5 14.2239 13.2761 14 13 14C12.7239 14 12.5 14.2239 12.5 14.5C12.5 14.7761 12.7239 15 13 15Z" fill="black"/>
+<path opacity="0.5" d="M13 2C13.2761 2 13.5 1.77614 13.5 1.5C13.5 1.22386 13.2761 1 13 1C12.7239 1 12.5 1.22386 12.5 1.5C12.5 1.77614 12.7239 2 13 2Z" fill="black"/>
+<path d="M13 4C13.2761 4 13.5 3.77614 13.5 3.5C13.5 3.22386 13.2761 3 13 3C12.7239 3 12.5 3.22386 12.5 3.5C12.5 3.77614 12.7239 4 13 4Z" fill="black"/>
+<path d="M13 13C13.2761 13 13.5 12.7761 13.5 12.5C13.5 12.2239 13.2761 12 13 12C12.7239 12 12.5 12.2239 12.5 12.5C12.5 12.7761 12.7239 13 13 13Z" fill="black"/>
+<path opacity="0.6" d="M11 3C11.2761 3 11.5 2.77614 11.5 2.5C11.5 2.22386 11.2761 2 11 2C10.7239 2 10.5 2.22386 10.5 2.5C10.5 2.77614 10.7239 3 11 3Z" fill="black"/>
+<path d="M8.5 15C8.77614 15 9 14.7761 9 14.5C9 14.2239 8.77614 14 8.5 14C8.22386 14 8 14.2239 8 14.5C8 14.7761 8.22386 15 8.5 15Z" fill="black"/>
+<path opacity="0.6" d="M6 14C6.27614 14 6.5 13.7761 6.5 13.5C6.5 13.2239 6.27614 13 6 13C5.72386 13 5.5 13.2239 5.5 13.5C5.5 13.7761 5.72386 14 6 14Z" fill="black"/>
+<path opacity="0.6" d="M11 14C11.2761 14 11.5 13.7761 11.5 13.5C11.5 13.2239 11.2761 13 11 13C10.7239 13 10.5 13.2239 10.5 13.5C10.5 13.7761 10.7239 14 11 14Z" fill="black"/>
+</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8 2.93652L6.9243 6.20697C6.86924 6.37435 6.77565 6.52646 6.65105 6.65105C6.52646 6.77565 6.37435 6.86924 6.20697 6.9243L2.93652 8L6.20697 9.0757C6.37435 9.13076 6.52646 9.22435 6.65105 9.34895C6.77565 9.47354 6.86924 9.62565 6.9243 9.79306L8 13.0635L9.0757 9.79306C9.13076 9.62565 9.22435 9.47354 9.34895 9.34895C9.47354 9.22435 9.62565 9.13076 9.79306 9.0757L13.0635 8L9.79306 6.9243C9.62565 6.86924 9.47354 6.77565 9.34895 6.65105C9.22435 6.52646 9.13076 6.37435 9.0757 6.20697L8 2.93652Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
-<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 2.93652L6.9243 6.20697C6.86924 6.37435 6.77565 6.52646 6.65105 6.65105C6.52646 6.77565 6.37435 6.86924 6.20697 6.9243L2.93652 8L6.20697 9.0757C6.37435 9.13076 6.52646 9.22435 6.65105 9.34895C6.77565 9.47354 6.86924 9.62565 6.9243 9.79306L8 13.0635L9.0757 9.79306C9.13076 9.62565 9.22435 9.47354 9.34895 9.34895C9.47354 9.22435 9.62565 9.13076 9.79306 9.0757L13.0635 8L9.79306 6.9243C9.62565 6.86924 9.47354 6.77565 9.34895 6.65105C9.22435 6.52646 9.13076 6.37435 9.0757 6.20697L8 2.93652Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3.33334 2V4.66666M2 3.33334H4.66666" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12.6665 11.3333V14M11.3333 12.6666H13.9999" stroke="black" stroke-opacity="0.75" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M5.70519 9.31137C6.13992 9.31137 6.55683 9.13868 6.86423 8.83128C7.17163 8.52389 7.34432 8.10696 7.34432 7.67224C7.34432 6.76744 7.01649 6.36094 6.68868 5.70528C5.98581 4.30022 6.54181 3.04726 7.99998 1.77136C8.32781 3.41049 9.31128 4.98406 10.6226 6.03311C11.9339 7.08215 12.5896 8.3279 12.5896 9.6392C12.5896 10.2419 12.4708 10.8387 12.2402 11.3956C12.0096 11.9524 11.6715 12.4583 11.2453 12.8845C10.8191 13.3107 10.3132 13.6487 9.75633 13.8794C9.1995 14.1101 8.60269 14.2287 7.99998 14.2287C7.39727 14.2287 6.80046 14.1101 6.24362 13.8794C5.68679 13.6487 5.18083 13.3107 4.75465 12.8845C4.32848 12.4583 3.99041 11.9524 3.75976 11.3956C3.52911 10.8387 3.4104 10.2419 3.4104 9.6392C3.4104 8.88324 3.6943 8.13513 4.06606 7.67224C4.06606 8.10696 4.23875 8.52389 4.54615 8.83128C4.85354 9.13868 5.27047 9.31137 5.70519 9.31137Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5.70519 9.31137C6.13992 9.31137 6.55683 9.13868 6.86423 8.83128C7.17163 8.52389 7.34432 8.10696 7.34432 7.67224C7.34432 6.76744 7.01649 6.36094 6.68868 5.70528C5.98581 4.30022 6.54181 3.04726 7.99998 1.77136C8.32781 3.41049 9.31128 4.98406 10.6226 6.03311C11.9339 7.08215 12.5896 8.3279 12.5896 9.6392C12.5896 10.2419 12.4708 10.8387 12.2402 11.3956C12.0096 11.9524 11.6715 12.4583 11.2453 12.8845C10.8191 13.3107 10.3132 13.6487 9.75633 13.8794C9.1995 14.1101 8.60269 14.2287 7.99998 14.2287C7.39727 14.2287 6.80046 14.1101 6.24362 13.8794C5.68679 13.6487 5.18083 13.3107 4.75465 12.8845C4.32848 12.4583 3.99041 11.9524 3.75976 11.3956C3.52911 10.8387 3.4104 10.2419 3.4104 9.6392C3.4104 8.88324 3.6943 8.13513 4.06606 7.67224C4.06606 8.10696 4.23875 8.52389 4.54615 8.83128C4.85354 9.13868 5.27047 9.31137 5.70519 9.31137Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g fill="#000" clip-path="url(#a)"><path fill-opacity=".5" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M5.705 9.311a1.64 1.64 0 0 0 1.64-1.639c0-.905-.328-1.311-.656-1.967C5.986 4.3 6.542 3.047 8 1.771c.328 1.64 1.312 3.213 2.623 4.262 1.311 1.05 1.967 2.295 1.967 3.606a4.59 4.59 0 1 1-9.18 0c0-.756.285-1.504.656-1.967a1.64 1.64 0 0 0 1.64 1.64Z"/><path d="M2.286 4.571a1.143 1.143 0 1 0 0-2.285 1.143 1.143 0 0 0 0 2.285ZM11.429 2.286a1.143 1.143 0 1 0 0-2.286 1.143 1.143 0 0 0 0 2.286ZM14.857 5.714a1.143 1.143 0 1 0 0-2.286 1.143 1.143 0 0 0 0 2.286Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g fill="#000" clip-path="url(#a)"><path fill-opacity=".5" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M5.705 9.311a1.64 1.64 0 0 0 1.64-1.639c0-.905-.328-1.311-.656-1.967C5.986 4.3 6.542 3.047 8 1.771c.328 1.64 1.312 3.213 2.623 4.262 1.311 1.05 1.967 2.295 1.967 3.606a4.59 4.59 0 1 1-9.18 0c0-.756.285-1.504.656-1.967a1.64 1.64 0 0 0 1.64 1.64Z"/><path d="M2.286 4.571a1.143 1.143 0 1 0 0-2.285 1.143 1.143 0 0 0 0 2.285ZM11.429 2.286a1.143 1.143 0 1 0 0-2.286 1.143 1.143 0 0 0 0 2.286ZM14.857 5.714a1.143 1.143 0 1 0 0-2.286 1.143 1.143 0 0 0 0 2.286Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.5"/>
+<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.2"/>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.5"/>
+<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.2"/>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.5"/>
-<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.5"/>
-<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
+<path d="M12 5L14 8L12 11" stroke="black" stroke-width="1.2"/>
+<path d="M10 6.5L11 8L10 9.5" stroke="black" stroke-width="1.2"/>
+<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M15 9.33333L14.5 9.66667L12.5 11L10.5 9.66667L10 9.33333" stroke="black" stroke-width="1.5"/>
-<path d="M12.5 11V4.5" stroke="black" stroke-width="1.5"/>
-<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
+<path d="M15 9.33333L14.5 9.66667L12.5 11L10.5 9.66667L10 9.33333" stroke="black" stroke-width="1.2"/>
+<path d="M12.5 11V4.5" stroke="black" stroke-width="1.2"/>
+<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path opacity="0.6" d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
-<path d="M14 8L10 12M14 12L10 8" stroke="black" stroke-width="1.5"/>
+<path opacity="0.6" d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.2"/>
+<path d="M14 8L10 12M14 12L10 8" stroke="black" stroke-width="1.2"/>
</svg>
@@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M10 6.66667L10.5 6.33333L12.5 5L14.5 6.33333L15 6.66667" stroke="black" stroke-width="1.5"/>
-<path d="M12.5 11V5" stroke="black" stroke-width="1.5"/>
-<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
+<path d="M10 6.66667L10.5 6.33333L12.5 5L14.5 6.33333L15 6.66667" stroke="black" stroke-width="1.2"/>
+<path d="M12.5 11V5" stroke="black" stroke-width="1.2"/>
+<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.2"/>
</svg>
@@ -0,0 +1,1257 @@
+<svg width="515" height="126" viewBox="0 0 515 126" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_2906_6463)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 0.390625H0.390625V12.1094H12.1094V0.390625ZM0 0V12.5H12.5V0H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 0.390625H12.8906V12.1094H24.6094V0.390625ZM12.5 0V12.5H25V0H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 0.390625H25.3906V12.1094H37.1094V0.390625ZM25 0V12.5H37.5V0H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 0.390625H37.8906V12.1094H49.6094V0.390625ZM37.5 0V12.5H50V0H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 0.390625H50.3906V12.1094H62.1094V0.390625ZM50 0V12.5H62.5V0H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 0.390625H62.8906V12.1094H74.6094V0.390625ZM62.5 0V12.5H75V0H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 0.390625H75.3906V12.1094H87.1094V0.390625ZM75 0V12.5H87.5V0H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 0.390625H87.8906V12.1094H99.6094V0.390625ZM87.5 0V12.5H100V0H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 0.390625H100.391V12.1094H112.109V0.390625ZM100 0V12.5H112.5V0H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 0.390625H112.891V12.1094H124.609V0.390625ZM112.5 0V12.5H125V0H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 0.390625H125.391V12.1094H137.109V0.390625ZM125 0V12.5H137.5V0H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 0.390625H137.891V12.1094H149.609V0.390625ZM137.5 0V12.5H150V0H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 0.390625H150.391V12.1094H162.109V0.390625ZM150 0V12.5H162.5V0H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 0.390625H162.891V12.1094H174.609V0.390625ZM162.5 0V12.5H175V0H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 0.390625H175.391V12.1094H187.109V0.390625ZM175 0V12.5H187.5V0H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 0.390625H187.891V12.1094H199.609V0.390625ZM187.5 0V12.5H200V0H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 0.390625H200.391V12.1094H212.109V0.390625ZM200 0V12.5H212.5V0H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 0.390625H212.891V12.1094H224.609V0.390625ZM212.5 0V12.5H225V0H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 0.390625H225.391V12.1094H237.109V0.390625ZM225 0V12.5H237.5V0H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 0.390625H237.891V12.1094H249.609V0.390625ZM237.5 0V12.5H250V0H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 0.390625H250.391V12.1094H262.109V0.390625ZM250 0V12.5H262.5V0H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 0.390625H262.891V12.1094H274.609V0.390625ZM262.5 0V12.5H275V0H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 0.390625H275.391V12.1094H287.109V0.390625ZM275 0V12.5H287.5V0H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 0.390625H287.891V12.1094H299.609V0.390625ZM287.5 0V12.5H300V0H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 0.390625H300.391V12.1094H312.109V0.390625ZM300 0V12.5H312.5V0H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 0.390625H312.891V12.1094H324.609V0.390625ZM312.5 0V12.5H325V0H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 0.390625H325.391V12.1094H337.109V0.390625ZM325 0V12.5H337.5V0H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 0.390625H337.891V12.1094H349.609V0.390625ZM337.5 0V12.5H350V0H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 0.390625H350.391V12.1094H362.109V0.390625ZM350 0V12.5H362.5V0H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 0.390625H362.891V12.1094H374.609V0.390625ZM362.5 0V12.5H375V0H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 0.390625H375.391V12.1094H387.109V0.390625ZM375 0V12.5H387.5V0H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 0.390625H387.891V12.1094H399.609V0.390625ZM387.5 0V12.5H400V0H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 0.390625H400.391V12.1094H412.109V0.390625ZM400 0V12.5H412.5V0H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 0.390625H412.891V12.1094H424.609V0.390625ZM412.5 0V12.5H425V0H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 0.390625H425.391V12.1094H437.109V0.390625ZM425 0V12.5H437.5V0H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 0.390625H437.891V12.1094H449.609V0.390625ZM437.5 0V12.5H450V0H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 0.390625H450.391V12.1094H462.109V0.390625ZM450 0V12.5H462.5V0H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 0.390625H462.891V12.1094H474.609V0.390625ZM462.5 0V12.5H475V0H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 0.390625H475.391V12.1094H487.109V0.390625ZM475 0V12.5H487.5V0H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 0.390625H487.891V12.1094H499.609V0.390625ZM487.5 0V12.5H500V0H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 0.390625H500.391V12.1094H512.109V0.390625ZM500 0V12.5H512.5V0H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 0.390625H512.891V12.1094H524.609V0.390625ZM512.5 0V12.5H525V0H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 0.390625H525.391V12.1094H537.109V0.390625ZM525 0V12.5H537.5V0H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 0.390625H537.891V12.1094H549.609V0.390625ZM537.5 0V12.5H550V0H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 0.390625H550.391V12.1094H562.109V0.390625ZM550 0V12.5H562.5V0H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 0.390625H562.891V12.1094H574.609V0.390625ZM562.5 0V12.5H575V0H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 0.390625H575.391V12.1094H587.109V0.390625ZM575 0V12.5H587.5V0H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 0.390625H587.891V12.1094H599.609V0.390625ZM587.5 0V12.5H600V0H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 12.8906H0.390625V24.6094H12.1094V12.8906ZM0 12.5V25H12.5V12.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 12.8906H12.8906V24.6094H24.6094V12.8906ZM12.5 12.5V25H25V12.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 12.8906H25.3906V24.6094H37.1094V12.8906ZM25 12.5V25H37.5V12.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 12.8906H37.8906V24.6094H49.6094V12.8906ZM37.5 12.5V25H50V12.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 12.8906H50.3906V24.6094H62.1094V12.8906ZM50 12.5V25H62.5V12.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 12.8906H62.8906V24.6094H74.6094V12.8906ZM62.5 12.5V25H75V12.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 12.8906H75.3906V24.6094H87.1094V12.8906ZM75 12.5V25H87.5V12.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 12.8906H87.8906V24.6094H99.6094V12.8906ZM87.5 12.5V25H100V12.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 12.8906H100.391V24.6094H112.109V12.8906ZM100 12.5V25H112.5V12.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 12.8906H112.891V24.6094H124.609V12.8906ZM112.5 12.5V25H125V12.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 12.8906H125.391V24.6094H137.109V12.8906ZM125 12.5V25H137.5V12.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 12.8906H137.891V24.6094H149.609V12.8906ZM137.5 12.5V25H150V12.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 12.8906H150.391V24.6094H162.109V12.8906ZM150 12.5V25H162.5V12.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 12.8906H162.891V24.6094H174.609V12.8906ZM162.5 12.5V25H175V12.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 12.8906H175.391V24.6094H187.109V12.8906ZM175 12.5V25H187.5V12.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 12.8906H187.891V24.6094H199.609V12.8906ZM187.5 12.5V25H200V12.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 12.8906H200.391V24.6094H212.109V12.8906ZM200 12.5V25H212.5V12.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 12.8906H212.891V24.6094H224.609V12.8906ZM212.5 12.5V25H225V12.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 12.8906H225.391V24.6094H237.109V12.8906ZM225 12.5V25H237.5V12.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 12.8906H237.891V24.6094H249.609V12.8906ZM237.5 12.5V25H250V12.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 12.8906H250.391V24.6094H262.109V12.8906ZM250 12.5V25H262.5V12.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 12.8906H262.891V24.6094H274.609V12.8906ZM262.5 12.5V25H275V12.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 12.8906H275.391V24.6094H287.109V12.8906ZM275 12.5V25H287.5V12.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 12.8906H287.891V24.6094H299.609V12.8906ZM287.5 12.5V25H300V12.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 12.8906H300.391V24.6094H312.109V12.8906ZM300 12.5V25H312.5V12.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 12.8906H312.891V24.6094H324.609V12.8906ZM312.5 12.5V25H325V12.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 12.8906H325.391V24.6094H337.109V12.8906ZM325 12.5V25H337.5V12.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 12.8906H337.891V24.6094H349.609V12.8906ZM337.5 12.5V25H350V12.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 12.8906H350.391V24.6094H362.109V12.8906ZM350 12.5V25H362.5V12.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 12.8906H362.891V24.6094H374.609V12.8906ZM362.5 12.5V25H375V12.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 12.8906H375.391V24.6094H387.109V12.8906ZM375 12.5V25H387.5V12.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 12.8906H387.891V24.6094H399.609V12.8906ZM387.5 12.5V25H400V12.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 12.8906H400.391V24.6094H412.109V12.8906ZM400 12.5V25H412.5V12.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 12.8906H412.891V24.6094H424.609V12.8906ZM412.5 12.5V25H425V12.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 12.8906H425.391V24.6094H437.109V12.8906ZM425 12.5V25H437.5V12.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 12.8906H437.891V24.6094H449.609V12.8906ZM437.5 12.5V25H450V12.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 12.8906H450.391V24.6094H462.109V12.8906ZM450 12.5V25H462.5V12.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 12.8906H462.891V24.6094H474.609V12.8906ZM462.5 12.5V25H475V12.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 12.8906H475.391V24.6094H487.109V12.8906ZM475 12.5V25H487.5V12.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 12.8906H487.891V24.6094H499.609V12.8906ZM487.5 12.5V25H500V12.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 12.8906H500.391V24.6094H512.109V12.8906ZM500 12.5V25H512.5V12.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 12.8906H512.891V24.6094H524.609V12.8906ZM512.5 12.5V25H525V12.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 12.8906H525.391V24.6094H537.109V12.8906ZM525 12.5V25H537.5V12.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 12.8906H537.891V24.6094H549.609V12.8906ZM537.5 12.5V25H550V12.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 12.8906H550.391V24.6094H562.109V12.8906ZM550 12.5V25H562.5V12.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 12.8906H562.891V24.6094H574.609V12.8906ZM562.5 12.5V25H575V12.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 12.8906H575.391V24.6094H587.109V12.8906ZM575 12.5V25H587.5V12.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 12.8906H587.891V24.6094H599.609V12.8906ZM587.5 12.5V25H600V12.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 25.3906H0.390625V37.1094H12.1094V25.3906ZM0 25V37.5H12.5V25H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 25.3906H12.8906V37.1094H24.6094V25.3906ZM12.5 25V37.5H25V25H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 25.3906H25.3906V37.1094H37.1094V25.3906ZM25 25V37.5H37.5V25H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 25.3906H37.8906V37.1094H49.6094V25.3906ZM37.5 25V37.5H50V25H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 25.3906H50.3906V37.1094H62.1094V25.3906ZM50 25V37.5H62.5V25H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 25.3906H62.8906V37.1094H74.6094V25.3906ZM62.5 25V37.5H75V25H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 25.3906H75.3906V37.1094H87.1094V25.3906ZM75 25V37.5H87.5V25H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 25.3906H87.8906V37.1094H99.6094V25.3906ZM87.5 25V37.5H100V25H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 25.3906H100.391V37.1094H112.109V25.3906ZM100 25V37.5H112.5V25H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 25.3906H112.891V37.1094H124.609V25.3906ZM112.5 25V37.5H125V25H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 25.3906H125.391V37.1094H137.109V25.3906ZM125 25V37.5H137.5V25H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 25.3906H137.891V37.1094H149.609V25.3906ZM137.5 25V37.5H150V25H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 25.3906H150.391V37.1094H162.109V25.3906ZM150 25V37.5H162.5V25H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 25.3906H162.891V37.1094H174.609V25.3906ZM162.5 25V37.5H175V25H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 25.3906H175.391V37.1094H187.109V25.3906ZM175 25V37.5H187.5V25H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 25.3906H187.891V37.1094H199.609V25.3906ZM187.5 25V37.5H200V25H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 25.3906H200.391V37.1094H212.109V25.3906ZM200 25V37.5H212.5V25H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 25.3906H212.891V37.1094H224.609V25.3906ZM212.5 25V37.5H225V25H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 25.3906H225.391V37.1094H237.109V25.3906ZM225 25V37.5H237.5V25H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 25.3906H237.891V37.1094H249.609V25.3906ZM237.5 25V37.5H250V25H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 25.3906H250.391V37.1094H262.109V25.3906ZM250 25V37.5H262.5V25H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 25.3906H262.891V37.1094H274.609V25.3906ZM262.5 25V37.5H275V25H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 25.3906H275.391V37.1094H287.109V25.3906ZM275 25V37.5H287.5V25H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 25.3906H287.891V37.1094H299.609V25.3906ZM287.5 25V37.5H300V25H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 25.3906H300.391V37.1094H312.109V25.3906ZM300 25V37.5H312.5V25H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 25.3906H312.891V37.1094H324.609V25.3906ZM312.5 25V37.5H325V25H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 25.3906H325.391V37.1094H337.109V25.3906ZM325 25V37.5H337.5V25H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 25.3906H337.891V37.1094H349.609V25.3906ZM337.5 25V37.5H350V25H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 25.3906H350.391V37.1094H362.109V25.3906ZM350 25V37.5H362.5V25H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 25.3906H362.891V37.1094H374.609V25.3906ZM362.5 25V37.5H375V25H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 25.3906H375.391V37.1094H387.109V25.3906ZM375 25V37.5H387.5V25H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 25.3906H387.891V37.1094H399.609V25.3906ZM387.5 25V37.5H400V25H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 25.3906H400.391V37.1094H412.109V25.3906ZM400 25V37.5H412.5V25H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 25.3906H412.891V37.1094H424.609V25.3906ZM412.5 25V37.5H425V25H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 25.3906H425.391V37.1094H437.109V25.3906ZM425 25V37.5H437.5V25H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 25.3906H437.891V37.1094H449.609V25.3906ZM437.5 25V37.5H450V25H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 25.3906H450.391V37.1094H462.109V25.3906ZM450 25V37.5H462.5V25H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 25.3906H462.891V37.1094H474.609V25.3906ZM462.5 25V37.5H475V25H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 25.3906H475.391V37.1094H487.109V25.3906ZM475 25V37.5H487.5V25H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 25.3906H487.891V37.1094H499.609V25.3906ZM487.5 25V37.5H500V25H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 25.3906H500.391V37.1094H512.109V25.3906ZM500 25V37.5H512.5V25H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 25.3906H512.891V37.1094H524.609V25.3906ZM512.5 25V37.5H525V25H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 25.3906H525.391V37.1094H537.109V25.3906ZM525 25V37.5H537.5V25H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 25.3906H537.891V37.1094H549.609V25.3906ZM537.5 25V37.5H550V25H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 25.3906H550.391V37.1094H562.109V25.3906ZM550 25V37.5H562.5V25H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 25.3906H562.891V37.1094H574.609V25.3906ZM562.5 25V37.5H575V25H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 25.3906H575.391V37.1094H587.109V25.3906ZM575 25V37.5H587.5V25H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 25.3906H587.891V37.1094H599.609V25.3906ZM587.5 25V37.5H600V25H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 37.8906H0.390625V49.6094H12.1094V37.8906ZM0 37.5V50H12.5V37.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 37.8906H12.8906V49.6094H24.6094V37.8906ZM12.5 37.5V50H25V37.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 37.8906H25.3906V49.6094H37.1094V37.8906ZM25 37.5V50H37.5V37.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 37.8906H37.8906V49.6094H49.6094V37.8906ZM37.5 37.5V50H50V37.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 37.8906H50.3906V49.6094H62.1094V37.8906ZM50 37.5V50H62.5V37.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 37.8906H62.8906V49.6094H74.6094V37.8906ZM62.5 37.5V50H75V37.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 37.8906H75.3906V49.6094H87.1094V37.8906ZM75 37.5V50H87.5V37.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 37.8906H87.8906V49.6094H99.6094V37.8906ZM87.5 37.5V50H100V37.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 37.8906H100.391V49.6094H112.109V37.8906ZM100 37.5V50H112.5V37.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 37.8906H112.891V49.6094H124.609V37.8906ZM112.5 37.5V50H125V37.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 37.8906H125.391V49.6094H137.109V37.8906ZM125 37.5V50H137.5V37.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 37.8906H137.891V49.6094H149.609V37.8906ZM137.5 37.5V50H150V37.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 37.8906H150.391V49.6094H162.109V37.8906ZM150 37.5V50H162.5V37.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 37.8906H162.891V49.6094H174.609V37.8906ZM162.5 37.5V50H175V37.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 37.8906H175.391V49.6094H187.109V37.8906ZM175 37.5V50H187.5V37.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 37.8906H187.891V49.6094H199.609V37.8906ZM187.5 37.5V50H200V37.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 37.8906H200.391V49.6094H212.109V37.8906ZM200 37.5V50H212.5V37.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 37.8906H212.891V49.6094H224.609V37.8906ZM212.5 37.5V50H225V37.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 37.8906H225.391V49.6094H237.109V37.8906ZM225 37.5V50H237.5V37.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 37.8906H237.891V49.6094H249.609V37.8906ZM237.5 37.5V50H250V37.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 37.8906H250.391V49.6094H262.109V37.8906ZM250 37.5V50H262.5V37.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 37.8906H262.891V49.6094H274.609V37.8906ZM262.5 37.5V50H275V37.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 37.8906H275.391V49.6094H287.109V37.8906ZM275 37.5V50H287.5V37.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 37.8906H287.891V49.6094H299.609V37.8906ZM287.5 37.5V50H300V37.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 37.8906H300.391V49.6094H312.109V37.8906ZM300 37.5V50H312.5V37.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 37.8906H312.891V49.6094H324.609V37.8906ZM312.5 37.5V50H325V37.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 37.8906H325.391V49.6094H337.109V37.8906ZM325 37.5V50H337.5V37.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 37.8906H337.891V49.6094H349.609V37.8906ZM337.5 37.5V50H350V37.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 37.8906H350.391V49.6094H362.109V37.8906ZM350 37.5V50H362.5V37.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 37.8906H362.891V49.6094H374.609V37.8906ZM362.5 37.5V50H375V37.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 37.8906H375.391V49.6094H387.109V37.8906ZM375 37.5V50H387.5V37.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 37.8906H387.891V49.6094H399.609V37.8906ZM387.5 37.5V50H400V37.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 37.8906H400.391V49.6094H412.109V37.8906ZM400 37.5V50H412.5V37.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 37.8906H412.891V49.6094H424.609V37.8906ZM412.5 37.5V50H425V37.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 37.8906H425.391V49.6094H437.109V37.8906ZM425 37.5V50H437.5V37.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 37.8906H437.891V49.6094H449.609V37.8906ZM437.5 37.5V50H450V37.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 37.8906H450.391V49.6094H462.109V37.8906ZM450 37.5V50H462.5V37.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 37.8906H462.891V49.6094H474.609V37.8906ZM462.5 37.5V50H475V37.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 37.8906H475.391V49.6094H487.109V37.8906ZM475 37.5V50H487.5V37.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 37.8906H487.891V49.6094H499.609V37.8906ZM487.5 37.5V50H500V37.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 37.8906H500.391V49.6094H512.109V37.8906ZM500 37.5V50H512.5V37.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 37.8906H512.891V49.6094H524.609V37.8906ZM512.5 37.5V50H525V37.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 37.8906H525.391V49.6094H537.109V37.8906ZM525 37.5V50H537.5V37.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 37.8906H537.891V49.6094H549.609V37.8906ZM537.5 37.5V50H550V37.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 37.8906H550.391V49.6094H562.109V37.8906ZM550 37.5V50H562.5V37.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 37.8906H562.891V49.6094H574.609V37.8906ZM562.5 37.5V50H575V37.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 37.8906H575.391V49.6094H587.109V37.8906ZM575 37.5V50H587.5V37.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 37.8906H587.891V49.6094H599.609V37.8906ZM587.5 37.5V50H600V37.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 50.3906H0.390625V62.1094H12.1094V50.3906ZM0 50V62.5H12.5V50H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 50.3906H12.8906V62.1094H24.6094V50.3906ZM12.5 50V62.5H25V50H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 50.3906H25.3906V62.1094H37.1094V50.3906ZM25 50V62.5H37.5V50H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 50.3906H37.8906V62.1094H49.6094V50.3906ZM37.5 50V62.5H50V50H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 50.3906H50.3906V62.1094H62.1094V50.3906ZM50 50V62.5H62.5V50H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 50.3906H62.8906V62.1094H74.6094V50.3906ZM62.5 50V62.5H75V50H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 50.3906H75.3906V62.1094H87.1094V50.3906ZM75 50V62.5H87.5V50H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 50.3906H87.8906V62.1094H99.6094V50.3906ZM87.5 50V62.5H100V50H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 50.3906H100.391V62.1094H112.109V50.3906ZM100 50V62.5H112.5V50H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 50.3906H112.891V62.1094H124.609V50.3906ZM112.5 50V62.5H125V50H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 50.3906H125.391V62.1094H137.109V50.3906ZM125 50V62.5H137.5V50H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 50.3906H137.891V62.1094H149.609V50.3906ZM137.5 50V62.5H150V50H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 50.3906H150.391V62.1094H162.109V50.3906ZM150 50V62.5H162.5V50H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 50.3906H162.891V62.1094H174.609V50.3906ZM162.5 50V62.5H175V50H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 50.3906H175.391V62.1094H187.109V50.3906ZM175 50V62.5H187.5V50H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 50.3906H187.891V62.1094H199.609V50.3906ZM187.5 50V62.5H200V50H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 50.3906H200.391V62.1094H212.109V50.3906ZM200 50V62.5H212.5V50H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 50.3906H212.891V62.1094H224.609V50.3906ZM212.5 50V62.5H225V50H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 50.3906H225.391V62.1094H237.109V50.3906ZM225 50V62.5H237.5V50H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 50.3906H237.891V62.1094H249.609V50.3906ZM237.5 50V62.5H250V50H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 50.3906H250.391V62.1094H262.109V50.3906ZM250 50V62.5H262.5V50H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 50.3906H262.891V62.1094H274.609V50.3906ZM262.5 50V62.5H275V50H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 50.3906H275.391V62.1094H287.109V50.3906ZM275 50V62.5H287.5V50H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 50.3906H287.891V62.1094H299.609V50.3906ZM287.5 50V62.5H300V50H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 50.3906H300.391V62.1094H312.109V50.3906ZM300 50V62.5H312.5V50H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 50.3906H312.891V62.1094H324.609V50.3906ZM312.5 50V62.5H325V50H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 50.3906H325.391V62.1094H337.109V50.3906ZM325 50V62.5H337.5V50H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 50.3906H337.891V62.1094H349.609V50.3906ZM337.5 50V62.5H350V50H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 50.3906H350.391V62.1094H362.109V50.3906ZM350 50V62.5H362.5V50H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 50.3906H362.891V62.1094H374.609V50.3906ZM362.5 50V62.5H375V50H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 50.3906H375.391V62.1094H387.109V50.3906ZM375 50V62.5H387.5V50H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 50.3906H387.891V62.1094H399.609V50.3906ZM387.5 50V62.5H400V50H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 50.3906H400.391V62.1094H412.109V50.3906ZM400 50V62.5H412.5V50H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 50.3906H412.891V62.1094H424.609V50.3906ZM412.5 50V62.5H425V50H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 50.3906H425.391V62.1094H437.109V50.3906ZM425 50V62.5H437.5V50H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 50.3906H437.891V62.1094H449.609V50.3906ZM437.5 50V62.5H450V50H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 50.3906H450.391V62.1094H462.109V50.3906ZM450 50V62.5H462.5V50H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 50.3906H462.891V62.1094H474.609V50.3906ZM462.5 50V62.5H475V50H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 50.3906H475.391V62.1094H487.109V50.3906ZM475 50V62.5H487.5V50H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 50.3906H487.891V62.1094H499.609V50.3906ZM487.5 50V62.5H500V50H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 50.3906H500.391V62.1094H512.109V50.3906ZM500 50V62.5H512.5V50H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 50.3906H512.891V62.1094H524.609V50.3906ZM512.5 50V62.5H525V50H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 50.3906H525.391V62.1094H537.109V50.3906ZM525 50V62.5H537.5V50H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 50.3906H537.891V62.1094H549.609V50.3906ZM537.5 50V62.5H550V50H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 50.3906H550.391V62.1094H562.109V50.3906ZM550 50V62.5H562.5V50H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 50.3906H562.891V62.1094H574.609V50.3906ZM562.5 50V62.5H575V50H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 50.3906H575.391V62.1094H587.109V50.3906ZM575 50V62.5H587.5V50H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 50.3906H587.891V62.1094H599.609V50.3906ZM587.5 50V62.5H600V50H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 62.8906H0.390625V74.6094H12.1094V62.8906ZM0 62.5V75H12.5V62.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 62.8906H12.8906V74.6094H24.6094V62.8906ZM12.5 62.5V75H25V62.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 62.8906H25.3906V74.6094H37.1094V62.8906ZM25 62.5V75H37.5V62.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 62.8906H37.8906V74.6094H49.6094V62.8906ZM37.5 62.5V75H50V62.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 62.8906H50.3906V74.6094H62.1094V62.8906ZM50 62.5V75H62.5V62.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 62.8906H62.8906V74.6094H74.6094V62.8906ZM62.5 62.5V75H75V62.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 62.8906H75.3906V74.6094H87.1094V62.8906ZM75 62.5V75H87.5V62.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 62.8906H87.8906V74.6094H99.6094V62.8906ZM87.5 62.5V75H100V62.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 62.8906H100.391V74.6094H112.109V62.8906ZM100 62.5V75H112.5V62.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 62.8906H112.891V74.6094H124.609V62.8906ZM112.5 62.5V75H125V62.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 62.8906H125.391V74.6094H137.109V62.8906ZM125 62.5V75H137.5V62.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 62.8906H137.891V74.6094H149.609V62.8906ZM137.5 62.5V75H150V62.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 62.8906H150.391V74.6094H162.109V62.8906ZM150 62.5V75H162.5V62.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 62.8906H162.891V74.6094H174.609V62.8906ZM162.5 62.5V75H175V62.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 62.8906H175.391V74.6094H187.109V62.8906ZM175 62.5V75H187.5V62.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 62.8906H187.891V74.6094H199.609V62.8906ZM187.5 62.5V75H200V62.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 62.8906H200.391V74.6094H212.109V62.8906ZM200 62.5V75H212.5V62.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 62.8906H212.891V74.6094H224.609V62.8906ZM212.5 62.5V75H225V62.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 62.8906H225.391V74.6094H237.109V62.8906ZM225 62.5V75H237.5V62.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 62.8906H237.891V74.6094H249.609V62.8906ZM237.5 62.5V75H250V62.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 62.8906H250.391V74.6094H262.109V62.8906ZM250 62.5V75H262.5V62.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 62.8906H262.891V74.6094H274.609V62.8906ZM262.5 62.5V75H275V62.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 62.8906H275.391V74.6094H287.109V62.8906ZM275 62.5V75H287.5V62.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 62.8906H287.891V74.6094H299.609V62.8906ZM287.5 62.5V75H300V62.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 62.8906H300.391V74.6094H312.109V62.8906ZM300 62.5V75H312.5V62.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 62.8906H312.891V74.6094H324.609V62.8906ZM312.5 62.5V75H325V62.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 62.8906H325.391V74.6094H337.109V62.8906ZM325 62.5V75H337.5V62.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 62.8906H337.891V74.6094H349.609V62.8906ZM337.5 62.5V75H350V62.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 62.8906H350.391V74.6094H362.109V62.8906ZM350 62.5V75H362.5V62.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 62.8906H362.891V74.6094H374.609V62.8906ZM362.5 62.5V75H375V62.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 62.8906H375.391V74.6094H387.109V62.8906ZM375 62.5V75H387.5V62.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 62.8906H387.891V74.6094H399.609V62.8906ZM387.5 62.5V75H400V62.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 62.8906H400.391V74.6094H412.109V62.8906ZM400 62.5V75H412.5V62.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 62.8906H412.891V74.6094H424.609V62.8906ZM412.5 62.5V75H425V62.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 62.8906H425.391V74.6094H437.109V62.8906ZM425 62.5V75H437.5V62.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 62.8906H437.891V74.6094H449.609V62.8906ZM437.5 62.5V75H450V62.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 62.8906H450.391V74.6094H462.109V62.8906ZM450 62.5V75H462.5V62.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 62.8906H462.891V74.6094H474.609V62.8906ZM462.5 62.5V75H475V62.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 62.8906H475.391V74.6094H487.109V62.8906ZM475 62.5V75H487.5V62.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 62.8906H487.891V74.6094H499.609V62.8906ZM487.5 62.5V75H500V62.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 62.8906H500.391V74.6094H512.109V62.8906ZM500 62.5V75H512.5V62.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 62.8906H512.891V74.6094H524.609V62.8906ZM512.5 62.5V75H525V62.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 62.8906H525.391V74.6094H537.109V62.8906ZM525 62.5V75H537.5V62.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 62.8906H537.891V74.6094H549.609V62.8906ZM537.5 62.5V75H550V62.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 62.8906H550.391V74.6094H562.109V62.8906ZM550 62.5V75H562.5V62.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 62.8906H562.891V74.6094H574.609V62.8906ZM562.5 62.5V75H575V62.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 62.8906H575.391V74.6094H587.109V62.8906ZM575 62.5V75H587.5V62.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 62.8906H587.891V74.6094H599.609V62.8906ZM587.5 62.5V75H600V62.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 75.3906H0.390625V87.1094H12.1094V75.3906ZM0 75V87.5H12.5V75H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 75.3906H12.8906V87.1094H24.6094V75.3906ZM12.5 75V87.5H25V75H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 75.3906H25.3906V87.1094H37.1094V75.3906ZM25 75V87.5H37.5V75H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 75.3906H37.8906V87.1094H49.6094V75.3906ZM37.5 75V87.5H50V75H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 75.3906H50.3906V87.1094H62.1094V75.3906ZM50 75V87.5H62.5V75H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 75.3906H62.8906V87.1094H74.6094V75.3906ZM62.5 75V87.5H75V75H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 75.3906H75.3906V87.1094H87.1094V75.3906ZM75 75V87.5H87.5V75H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 75.3906H87.8906V87.1094H99.6094V75.3906ZM87.5 75V87.5H100V75H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 75.3906H100.391V87.1094H112.109V75.3906ZM100 75V87.5H112.5V75H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 75.3906H112.891V87.1094H124.609V75.3906ZM112.5 75V87.5H125V75H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 75.3906H125.391V87.1094H137.109V75.3906ZM125 75V87.5H137.5V75H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 75.3906H137.891V87.1094H149.609V75.3906ZM137.5 75V87.5H150V75H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 75.3906H150.391V87.1094H162.109V75.3906ZM150 75V87.5H162.5V75H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 75.3906H162.891V87.1094H174.609V75.3906ZM162.5 75V87.5H175V75H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 75.3906H175.391V87.1094H187.109V75.3906ZM175 75V87.5H187.5V75H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 75.3906H187.891V87.1094H199.609V75.3906ZM187.5 75V87.5H200V75H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 75.3906H200.391V87.1094H212.109V75.3906ZM200 75V87.5H212.5V75H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 75.3906H212.891V87.1094H224.609V75.3906ZM212.5 75V87.5H225V75H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 75.3906H225.391V87.1094H237.109V75.3906ZM225 75V87.5H237.5V75H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 75.3906H237.891V87.1094H249.609V75.3906ZM237.5 75V87.5H250V75H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 75.3906H250.391V87.1094H262.109V75.3906ZM250 75V87.5H262.5V75H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 75.3906H262.891V87.1094H274.609V75.3906ZM262.5 75V87.5H275V75H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 75.3906H275.391V87.1094H287.109V75.3906ZM275 75V87.5H287.5V75H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 75.3906H287.891V87.1094H299.609V75.3906ZM287.5 75V87.5H300V75H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 75.3906H300.391V87.1094H312.109V75.3906ZM300 75V87.5H312.5V75H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 75.3906H312.891V87.1094H324.609V75.3906ZM312.5 75V87.5H325V75H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 75.3906H325.391V87.1094H337.109V75.3906ZM325 75V87.5H337.5V75H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 75.3906H337.891V87.1094H349.609V75.3906ZM337.5 75V87.5H350V75H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 75.3906H350.391V87.1094H362.109V75.3906ZM350 75V87.5H362.5V75H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 75.3906H362.891V87.1094H374.609V75.3906ZM362.5 75V87.5H375V75H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 75.3906H375.391V87.1094H387.109V75.3906ZM375 75V87.5H387.5V75H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 75.3906H387.891V87.1094H399.609V75.3906ZM387.5 75V87.5H400V75H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 75.3906H400.391V87.1094H412.109V75.3906ZM400 75V87.5H412.5V75H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 75.3906H412.891V87.1094H424.609V75.3906ZM412.5 75V87.5H425V75H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 75.3906H425.391V87.1094H437.109V75.3906ZM425 75V87.5H437.5V75H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 75.3906H437.891V87.1094H449.609V75.3906ZM437.5 75V87.5H450V75H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 75.3906H450.391V87.1094H462.109V75.3906ZM450 75V87.5H462.5V75H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 75.3906H462.891V87.1094H474.609V75.3906ZM462.5 75V87.5H475V75H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 75.3906H475.391V87.1094H487.109V75.3906ZM475 75V87.5H487.5V75H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 75.3906H487.891V87.1094H499.609V75.3906ZM487.5 75V87.5H500V75H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 75.3906H500.391V87.1094H512.109V75.3906ZM500 75V87.5H512.5V75H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 75.3906H512.891V87.1094H524.609V75.3906ZM512.5 75V87.5H525V75H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 75.3906H525.391V87.1094H537.109V75.3906ZM525 75V87.5H537.5V75H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 75.3906H537.891V87.1094H549.609V75.3906ZM537.5 75V87.5H550V75H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 75.3906H550.391V87.1094H562.109V75.3906ZM550 75V87.5H562.5V75H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 75.3906H562.891V87.1094H574.609V75.3906ZM562.5 75V87.5H575V75H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 75.3906H575.391V87.1094H587.109V75.3906ZM575 75V87.5H587.5V75H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 75.3906H587.891V87.1094H599.609V75.3906ZM587.5 75V87.5H600V75H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 87.8906H0.390625V99.6094H12.1094V87.8906ZM0 87.5V100H12.5V87.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 87.8906H12.8906V99.6094H24.6094V87.8906ZM12.5 87.5V100H25V87.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 87.8906H25.3906V99.6094H37.1094V87.8906ZM25 87.5V100H37.5V87.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 87.8906H37.8906V99.6094H49.6094V87.8906ZM37.5 87.5V100H50V87.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 87.8906H50.3906V99.6094H62.1094V87.8906ZM50 87.5V100H62.5V87.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 87.8906H62.8906V99.6094H74.6094V87.8906ZM62.5 87.5V100H75V87.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 87.8906H75.3906V99.6094H87.1094V87.8906ZM75 87.5V100H87.5V87.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 87.8906H87.8906V99.6094H99.6094V87.8906ZM87.5 87.5V100H100V87.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 87.8906H100.391V99.6094H112.109V87.8906ZM100 87.5V100H112.5V87.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 87.8906H112.891V99.6094H124.609V87.8906ZM112.5 87.5V100H125V87.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 87.8906H125.391V99.6094H137.109V87.8906ZM125 87.5V100H137.5V87.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 87.8906H137.891V99.6094H149.609V87.8906ZM137.5 87.5V100H150V87.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 87.8906H150.391V99.6094H162.109V87.8906ZM150 87.5V100H162.5V87.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 87.8906H162.891V99.6094H174.609V87.8906ZM162.5 87.5V100H175V87.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 87.8906H175.391V99.6094H187.109V87.8906ZM175 87.5V100H187.5V87.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 87.8906H187.891V99.6094H199.609V87.8906ZM187.5 87.5V100H200V87.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 87.8906H200.391V99.6094H212.109V87.8906ZM200 87.5V100H212.5V87.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 87.8906H212.891V99.6094H224.609V87.8906ZM212.5 87.5V100H225V87.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 87.8906H225.391V99.6094H237.109V87.8906ZM225 87.5V100H237.5V87.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 87.8906H237.891V99.6094H249.609V87.8906ZM237.5 87.5V100H250V87.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 87.8906H250.391V99.6094H262.109V87.8906ZM250 87.5V100H262.5V87.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 87.8906H262.891V99.6094H274.609V87.8906ZM262.5 87.5V100H275V87.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 87.8906H275.391V99.6094H287.109V87.8906ZM275 87.5V100H287.5V87.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 87.8906H287.891V99.6094H299.609V87.8906ZM287.5 87.5V100H300V87.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 87.8906H300.391V99.6094H312.109V87.8906ZM300 87.5V100H312.5V87.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 87.8906H312.891V99.6094H324.609V87.8906ZM312.5 87.5V100H325V87.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 87.8906H325.391V99.6094H337.109V87.8906ZM325 87.5V100H337.5V87.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 87.8906H337.891V99.6094H349.609V87.8906ZM337.5 87.5V100H350V87.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 87.8906H350.391V99.6094H362.109V87.8906ZM350 87.5V100H362.5V87.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 87.8906H362.891V99.6094H374.609V87.8906ZM362.5 87.5V100H375V87.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 87.8906H375.391V99.6094H387.109V87.8906ZM375 87.5V100H387.5V87.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 87.8906H387.891V99.6094H399.609V87.8906ZM387.5 87.5V100H400V87.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 87.8906H400.391V99.6094H412.109V87.8906ZM400 87.5V100H412.5V87.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 87.8906H412.891V99.6094H424.609V87.8906ZM412.5 87.5V100H425V87.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 87.8906H425.391V99.6094H437.109V87.8906ZM425 87.5V100H437.5V87.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 87.8906H437.891V99.6094H449.609V87.8906ZM437.5 87.5V100H450V87.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 87.8906H450.391V99.6094H462.109V87.8906ZM450 87.5V100H462.5V87.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 87.8906H462.891V99.6094H474.609V87.8906ZM462.5 87.5V100H475V87.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 87.8906H475.391V99.6094H487.109V87.8906ZM475 87.5V100H487.5V87.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 87.8906H487.891V99.6094H499.609V87.8906ZM487.5 87.5V100H500V87.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 87.8906H500.391V99.6094H512.109V87.8906ZM500 87.5V100H512.5V87.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 87.8906H512.891V99.6094H524.609V87.8906ZM512.5 87.5V100H525V87.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 87.8906H525.391V99.6094H537.109V87.8906ZM525 87.5V100H537.5V87.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 87.8906H537.891V99.6094H549.609V87.8906ZM537.5 87.5V100H550V87.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 87.8906H550.391V99.6094H562.109V87.8906ZM550 87.5V100H562.5V87.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 87.8906H562.891V99.6094H574.609V87.8906ZM562.5 87.5V100H575V87.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 87.8906H575.391V99.6094H587.109V87.8906ZM575 87.5V100H587.5V87.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 87.8906H587.891V99.6094H599.609V87.8906ZM587.5 87.5V100H600V87.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 100.391H0.390625V112.109H12.1094V100.391ZM0 100V112.5H12.5V100H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 100.391H12.8906V112.109H24.6094V100.391ZM12.5 100V112.5H25V100H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 100.391H25.3906V112.109H37.1094V100.391ZM25 100V112.5H37.5V100H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 100.391H37.8906V112.109H49.6094V100.391ZM37.5 100V112.5H50V100H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 100.391H50.3906V112.109H62.1094V100.391ZM50 100V112.5H62.5V100H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 100.391H62.8906V112.109H74.6094V100.391ZM62.5 100V112.5H75V100H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 100.391H75.3906V112.109H87.1094V100.391ZM75 100V112.5H87.5V100H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 100.391H87.8906V112.109H99.6094V100.391ZM87.5 100V112.5H100V100H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 100.391H100.391V112.109H112.109V100.391ZM100 100V112.5H112.5V100H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 100.391H112.891V112.109H124.609V100.391ZM112.5 100V112.5H125V100H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 100.391H125.391V112.109H137.109V100.391ZM125 100V112.5H137.5V100H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 100.391H137.891V112.109H149.609V100.391ZM137.5 100V112.5H150V100H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 100.391H150.391V112.109H162.109V100.391ZM150 100V112.5H162.5V100H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 100.391H162.891V112.109H174.609V100.391ZM162.5 100V112.5H175V100H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 100.391H175.391V112.109H187.109V100.391ZM175 100V112.5H187.5V100H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 100.391H187.891V112.109H199.609V100.391ZM187.5 100V112.5H200V100H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 100.391H200.391V112.109H212.109V100.391ZM200 100V112.5H212.5V100H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 100.391H212.891V112.109H224.609V100.391ZM212.5 100V112.5H225V100H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 100.391H225.391V112.109H237.109V100.391ZM225 100V112.5H237.5V100H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 100.391H237.891V112.109H249.609V100.391ZM237.5 100V112.5H250V100H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 100.391H250.391V112.109H262.109V100.391ZM250 100V112.5H262.5V100H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 100.391H262.891V112.109H274.609V100.391ZM262.5 100V112.5H275V100H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 100.391H275.391V112.109H287.109V100.391ZM275 100V112.5H287.5V100H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 100.391H287.891V112.109H299.609V100.391ZM287.5 100V112.5H300V100H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 100.391H300.391V112.109H312.109V100.391ZM300 100V112.5H312.5V100H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 100.391H312.891V112.109H324.609V100.391ZM312.5 100V112.5H325V100H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 100.391H325.391V112.109H337.109V100.391ZM325 100V112.5H337.5V100H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 100.391H337.891V112.109H349.609V100.391ZM337.5 100V112.5H350V100H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 100.391H350.391V112.109H362.109V100.391ZM350 100V112.5H362.5V100H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 100.391H362.891V112.109H374.609V100.391ZM362.5 100V112.5H375V100H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 100.391H375.391V112.109H387.109V100.391ZM375 100V112.5H387.5V100H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 100.391H387.891V112.109H399.609V100.391ZM387.5 100V112.5H400V100H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 100.391H400.391V112.109H412.109V100.391ZM400 100V112.5H412.5V100H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 100.391H412.891V112.109H424.609V100.391ZM412.5 100V112.5H425V100H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 100.391H425.391V112.109H437.109V100.391ZM425 100V112.5H437.5V100H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 100.391H437.891V112.109H449.609V100.391ZM437.5 100V112.5H450V100H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 100.391H450.391V112.109H462.109V100.391ZM450 100V112.5H462.5V100H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 100.391H462.891V112.109H474.609V100.391ZM462.5 100V112.5H475V100H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 100.391H475.391V112.109H487.109V100.391ZM475 100V112.5H487.5V100H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 100.391H487.891V112.109H499.609V100.391ZM487.5 100V112.5H500V100H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 100.391H500.391V112.109H512.109V100.391ZM500 100V112.5H512.5V100H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 100.391H512.891V112.109H524.609V100.391ZM512.5 100V112.5H525V100H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 100.391H525.391V112.109H537.109V100.391ZM525 100V112.5H537.5V100H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 100.391H537.891V112.109H549.609V100.391ZM537.5 100V112.5H550V100H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 100.391H550.391V112.109H562.109V100.391ZM550 100V112.5H562.5V100H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 100.391H562.891V112.109H574.609V100.391ZM562.5 100V112.5H575V100H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 100.391H575.391V112.109H587.109V100.391ZM575 100V112.5H587.5V100H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 100.391H587.891V112.109H599.609V100.391ZM587.5 100V112.5H600V100H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 112.891H0.390625V124.609H12.1094V112.891ZM0 112.5V125H12.5V112.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 112.891H12.8906V124.609H24.6094V112.891ZM12.5 112.5V125H25V112.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 112.891H25.3906V124.609H37.1094V112.891ZM25 112.5V125H37.5V112.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 112.891H37.8906V124.609H49.6094V112.891ZM37.5 112.5V125H50V112.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 112.891H50.3906V124.609H62.1094V112.891ZM50 112.5V125H62.5V112.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 112.891H62.8906V124.609H74.6094V112.891ZM62.5 112.5V125H75V112.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 112.891H75.3906V124.609H87.1094V112.891ZM75 112.5V125H87.5V112.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 112.891H87.8906V124.609H99.6094V112.891ZM87.5 112.5V125H100V112.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 112.891H100.391V124.609H112.109V112.891ZM100 112.5V125H112.5V112.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 112.891H112.891V124.609H124.609V112.891ZM112.5 112.5V125H125V112.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 112.891H125.391V124.609H137.109V112.891ZM125 112.5V125H137.5V112.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 112.891H137.891V124.609H149.609V112.891ZM137.5 112.5V125H150V112.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 112.891H150.391V124.609H162.109V112.891ZM150 112.5V125H162.5V112.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 112.891H162.891V124.609H174.609V112.891ZM162.5 112.5V125H175V112.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 112.891H175.391V124.609H187.109V112.891ZM175 112.5V125H187.5V112.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 112.891H187.891V124.609H199.609V112.891ZM187.5 112.5V125H200V112.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 112.891H200.391V124.609H212.109V112.891ZM200 112.5V125H212.5V112.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 112.891H212.891V124.609H224.609V112.891ZM212.5 112.5V125H225V112.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 112.891H225.391V124.609H237.109V112.891ZM225 112.5V125H237.5V112.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 112.891H237.891V124.609H249.609V112.891ZM237.5 112.5V125H250V112.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 112.891H250.391V124.609H262.109V112.891ZM250 112.5V125H262.5V112.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 112.891H262.891V124.609H274.609V112.891ZM262.5 112.5V125H275V112.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 112.891H275.391V124.609H287.109V112.891ZM275 112.5V125H287.5V112.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 112.891H287.891V124.609H299.609V112.891ZM287.5 112.5V125H300V112.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 112.891H300.391V124.609H312.109V112.891ZM300 112.5V125H312.5V112.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 112.891H312.891V124.609H324.609V112.891ZM312.5 112.5V125H325V112.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 112.891H325.391V124.609H337.109V112.891ZM325 112.5V125H337.5V112.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 112.891H337.891V124.609H349.609V112.891ZM337.5 112.5V125H350V112.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 112.891H350.391V124.609H362.109V112.891ZM350 112.5V125H362.5V112.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 112.891H362.891V124.609H374.609V112.891ZM362.5 112.5V125H375V112.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 112.891H375.391V124.609H387.109V112.891ZM375 112.5V125H387.5V112.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 112.891H387.891V124.609H399.609V112.891ZM387.5 112.5V125H400V112.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 112.891H400.391V124.609H412.109V112.891ZM400 112.5V125H412.5V112.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 112.891H412.891V124.609H424.609V112.891ZM412.5 112.5V125H425V112.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 112.891H425.391V124.609H437.109V112.891ZM425 112.5V125H437.5V112.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 112.891H437.891V124.609H449.609V112.891ZM437.5 112.5V125H450V112.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 112.891H450.391V124.609H462.109V112.891ZM450 112.5V125H462.5V112.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 112.891H462.891V124.609H474.609V112.891ZM462.5 112.5V125H475V112.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 112.891H475.391V124.609H487.109V112.891ZM475 112.5V125H487.5V112.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 112.891H487.891V124.609H499.609V112.891ZM487.5 112.5V125H500V112.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 112.891H500.391V124.609H512.109V112.891ZM500 112.5V125H512.5V112.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 112.891H512.891V124.609H524.609V112.891ZM512.5 112.5V125H525V112.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 112.891H525.391V124.609H537.109V112.891ZM525 112.5V125H537.5V112.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 112.891H537.891V124.609H549.609V112.891ZM537.5 112.5V125H550V112.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 112.891H550.391V124.609H562.109V112.891ZM550 112.5V125H562.5V112.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 112.891H562.891V124.609H574.609V112.891ZM562.5 112.5V125H575V112.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 112.891H575.391V124.609H587.109V112.891ZM575 112.5V125H587.5V112.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 112.891H587.891V124.609H599.609V112.891ZM587.5 112.5V125H600V112.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 125.391H0.390625V137.109H12.1094V125.391ZM0 125V137.5H12.5V125H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 125.391H12.8906V137.109H24.6094V125.391ZM12.5 125V137.5H25V125H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 125.391H25.3906V137.109H37.1094V125.391ZM25 125V137.5H37.5V125H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 125.391H37.8906V137.109H49.6094V125.391ZM37.5 125V137.5H50V125H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 125.391H50.3906V137.109H62.1094V125.391ZM50 125V137.5H62.5V125H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 125.391H62.8906V137.109H74.6094V125.391ZM62.5 125V137.5H75V125H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 125.391H75.3906V137.109H87.1094V125.391ZM75 125V137.5H87.5V125H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 125.391H87.8906V137.109H99.6094V125.391ZM87.5 125V137.5H100V125H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 125.391H100.391V137.109H112.109V125.391ZM100 125V137.5H112.5V125H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 125.391H112.891V137.109H124.609V125.391ZM112.5 125V137.5H125V125H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 125.391H125.391V137.109H137.109V125.391ZM125 125V137.5H137.5V125H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 125.391H137.891V137.109H149.609V125.391ZM137.5 125V137.5H150V125H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 125.391H150.391V137.109H162.109V125.391ZM150 125V137.5H162.5V125H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 125.391H162.891V137.109H174.609V125.391ZM162.5 125V137.5H175V125H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 125.391H175.391V137.109H187.109V125.391ZM175 125V137.5H187.5V125H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 125.391H187.891V137.109H199.609V125.391ZM187.5 125V137.5H200V125H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 125.391H200.391V137.109H212.109V125.391ZM200 125V137.5H212.5V125H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 125.391H212.891V137.109H224.609V125.391ZM212.5 125V137.5H225V125H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 125.391H225.391V137.109H237.109V125.391ZM225 125V137.5H237.5V125H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 125.391H237.891V137.109H249.609V125.391ZM237.5 125V137.5H250V125H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 125.391H250.391V137.109H262.109V125.391ZM250 125V137.5H262.5V125H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 125.391H262.891V137.109H274.609V125.391ZM262.5 125V137.5H275V125H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 125.391H275.391V137.109H287.109V125.391ZM275 125V137.5H287.5V125H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 125.391H287.891V137.109H299.609V125.391ZM287.5 125V137.5H300V125H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 125.391H300.391V137.109H312.109V125.391ZM300 125V137.5H312.5V125H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 125.391H312.891V137.109H324.609V125.391ZM312.5 125V137.5H325V125H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 125.391H325.391V137.109H337.109V125.391ZM325 125V137.5H337.5V125H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 125.391H337.891V137.109H349.609V125.391ZM337.5 125V137.5H350V125H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 125.391H350.391V137.109H362.109V125.391ZM350 125V137.5H362.5V125H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 125.391H362.891V137.109H374.609V125.391ZM362.5 125V137.5H375V125H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 125.391H375.391V137.109H387.109V125.391ZM375 125V137.5H387.5V125H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 125.391H387.891V137.109H399.609V125.391ZM387.5 125V137.5H400V125H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 125.391H400.391V137.109H412.109V125.391ZM400 125V137.5H412.5V125H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 125.391H412.891V137.109H424.609V125.391ZM412.5 125V137.5H425V125H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 125.391H425.391V137.109H437.109V125.391ZM425 125V137.5H437.5V125H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 125.391H437.891V137.109H449.609V125.391ZM437.5 125V137.5H450V125H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 125.391H450.391V137.109H462.109V125.391ZM450 125V137.5H462.5V125H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 125.391H462.891V137.109H474.609V125.391ZM462.5 125V137.5H475V125H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 125.391H475.391V137.109H487.109V125.391ZM475 125V137.5H487.5V125H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 125.391H487.891V137.109H499.609V125.391ZM487.5 125V137.5H500V125H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 125.391H500.391V137.109H512.109V125.391ZM500 125V137.5H512.5V125H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 125.391H512.891V137.109H524.609V125.391ZM512.5 125V137.5H525V125H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 125.391H525.391V137.109H537.109V125.391ZM525 125V137.5H537.5V125H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 125.391H537.891V137.109H549.609V125.391ZM537.5 125V137.5H550V125H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 125.391H550.391V137.109H562.109V125.391ZM550 125V137.5H562.5V125H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 125.391H562.891V137.109H574.609V125.391ZM562.5 125V137.5H575V125H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 125.391H575.391V137.109H587.109V125.391ZM575 125V137.5H587.5V125H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 125.391H587.891V137.109H599.609V125.391ZM587.5 125V137.5H600V125H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 137.891H0.390625V149.609H12.1094V137.891ZM0 137.5V150H12.5V137.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 137.891H12.8906V149.609H24.6094V137.891ZM12.5 137.5V150H25V137.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 137.891H25.3906V149.609H37.1094V137.891ZM25 137.5V150H37.5V137.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 137.891H37.8906V149.609H49.6094V137.891ZM37.5 137.5V150H50V137.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 137.891H50.3906V149.609H62.1094V137.891ZM50 137.5V150H62.5V137.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 137.891H62.8906V149.609H74.6094V137.891ZM62.5 137.5V150H75V137.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 137.891H75.3906V149.609H87.1094V137.891ZM75 137.5V150H87.5V137.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 137.891H87.8906V149.609H99.6094V137.891ZM87.5 137.5V150H100V137.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 137.891H100.391V149.609H112.109V137.891ZM100 137.5V150H112.5V137.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 137.891H112.891V149.609H124.609V137.891ZM112.5 137.5V150H125V137.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 137.891H125.391V149.609H137.109V137.891ZM125 137.5V150H137.5V137.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 137.891H137.891V149.609H149.609V137.891ZM137.5 137.5V150H150V137.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 137.891H150.391V149.609H162.109V137.891ZM150 137.5V150H162.5V137.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 137.891H162.891V149.609H174.609V137.891ZM162.5 137.5V150H175V137.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 137.891H175.391V149.609H187.109V137.891ZM175 137.5V150H187.5V137.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 137.891H187.891V149.609H199.609V137.891ZM187.5 137.5V150H200V137.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 137.891H200.391V149.609H212.109V137.891ZM200 137.5V150H212.5V137.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 137.891H212.891V149.609H224.609V137.891ZM212.5 137.5V150H225V137.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 137.891H225.391V149.609H237.109V137.891ZM225 137.5V150H237.5V137.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 137.891H237.891V149.609H249.609V137.891ZM237.5 137.5V150H250V137.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 137.891H250.391V149.609H262.109V137.891ZM250 137.5V150H262.5V137.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 137.891H262.891V149.609H274.609V137.891ZM262.5 137.5V150H275V137.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 137.891H275.391V149.609H287.109V137.891ZM275 137.5V150H287.5V137.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 137.891H287.891V149.609H299.609V137.891ZM287.5 137.5V150H300V137.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 137.891H300.391V149.609H312.109V137.891ZM300 137.5V150H312.5V137.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 137.891H312.891V149.609H324.609V137.891ZM312.5 137.5V150H325V137.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 137.891H325.391V149.609H337.109V137.891ZM325 137.5V150H337.5V137.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 137.891H337.891V149.609H349.609V137.891ZM337.5 137.5V150H350V137.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 137.891H350.391V149.609H362.109V137.891ZM350 137.5V150H362.5V137.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 137.891H362.891V149.609H374.609V137.891ZM362.5 137.5V150H375V137.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 137.891H375.391V149.609H387.109V137.891ZM375 137.5V150H387.5V137.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 137.891H387.891V149.609H399.609V137.891ZM387.5 137.5V150H400V137.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 137.891H400.391V149.609H412.109V137.891ZM400 137.5V150H412.5V137.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 137.891H412.891V149.609H424.609V137.891ZM412.5 137.5V150H425V137.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 137.891H425.391V149.609H437.109V137.891ZM425 137.5V150H437.5V137.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 137.891H437.891V149.609H449.609V137.891ZM437.5 137.5V150H450V137.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 137.891H450.391V149.609H462.109V137.891ZM450 137.5V150H462.5V137.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 137.891H462.891V149.609H474.609V137.891ZM462.5 137.5V150H475V137.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 137.891H475.391V149.609H487.109V137.891ZM475 137.5V150H487.5V137.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 137.891H487.891V149.609H499.609V137.891ZM487.5 137.5V150H500V137.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 137.891H500.391V149.609H512.109V137.891ZM500 137.5V150H512.5V137.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 137.891H512.891V149.609H524.609V137.891ZM512.5 137.5V150H525V137.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 137.891H525.391V149.609H537.109V137.891ZM525 137.5V150H537.5V137.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 137.891H537.891V149.609H549.609V137.891ZM537.5 137.5V150H550V137.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 137.891H550.391V149.609H562.109V137.891ZM550 137.5V150H562.5V137.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 137.891H562.891V149.609H574.609V137.891ZM562.5 137.5V150H575V137.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 137.891H575.391V149.609H587.109V137.891ZM575 137.5V150H587.5V137.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 137.891H587.891V149.609H599.609V137.891ZM587.5 137.5V150H600V137.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 150.391H0.390625V162.109H12.1094V150.391ZM0 150V162.5H12.5V150H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 150.391H12.8906V162.109H24.6094V150.391ZM12.5 150V162.5H25V150H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 150.391H25.3906V162.109H37.1094V150.391ZM25 150V162.5H37.5V150H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 150.391H37.8906V162.109H49.6094V150.391ZM37.5 150V162.5H50V150H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 150.391H50.3906V162.109H62.1094V150.391ZM50 150V162.5H62.5V150H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 150.391H62.8906V162.109H74.6094V150.391ZM62.5 150V162.5H75V150H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 150.391H75.3906V162.109H87.1094V150.391ZM75 150V162.5H87.5V150H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 150.391H87.8906V162.109H99.6094V150.391ZM87.5 150V162.5H100V150H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 150.391H100.391V162.109H112.109V150.391ZM100 150V162.5H112.5V150H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 150.391H112.891V162.109H124.609V150.391ZM112.5 150V162.5H125V150H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 150.391H125.391V162.109H137.109V150.391ZM125 150V162.5H137.5V150H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 150.391H137.891V162.109H149.609V150.391ZM137.5 150V162.5H150V150H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 150.391H150.391V162.109H162.109V150.391ZM150 150V162.5H162.5V150H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 150.391H162.891V162.109H174.609V150.391ZM162.5 150V162.5H175V150H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 150.391H175.391V162.109H187.109V150.391ZM175 150V162.5H187.5V150H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 150.391H187.891V162.109H199.609V150.391ZM187.5 150V162.5H200V150H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 150.391H200.391V162.109H212.109V150.391ZM200 150V162.5H212.5V150H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 150.391H212.891V162.109H224.609V150.391ZM212.5 150V162.5H225V150H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 150.391H225.391V162.109H237.109V150.391ZM225 150V162.5H237.5V150H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 150.391H237.891V162.109H249.609V150.391ZM237.5 150V162.5H250V150H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 150.391H250.391V162.109H262.109V150.391ZM250 150V162.5H262.5V150H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 150.391H262.891V162.109H274.609V150.391ZM262.5 150V162.5H275V150H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 150.391H275.391V162.109H287.109V150.391ZM275 150V162.5H287.5V150H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 150.391H287.891V162.109H299.609V150.391ZM287.5 150V162.5H300V150H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 150.391H300.391V162.109H312.109V150.391ZM300 150V162.5H312.5V150H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 150.391H312.891V162.109H324.609V150.391ZM312.5 150V162.5H325V150H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 150.391H325.391V162.109H337.109V150.391ZM325 150V162.5H337.5V150H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 150.391H337.891V162.109H349.609V150.391ZM337.5 150V162.5H350V150H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 150.391H350.391V162.109H362.109V150.391ZM350 150V162.5H362.5V150H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 150.391H362.891V162.109H374.609V150.391ZM362.5 150V162.5H375V150H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 150.391H375.391V162.109H387.109V150.391ZM375 150V162.5H387.5V150H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 150.391H387.891V162.109H399.609V150.391ZM387.5 150V162.5H400V150H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 150.391H400.391V162.109H412.109V150.391ZM400 150V162.5H412.5V150H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 150.391H412.891V162.109H424.609V150.391ZM412.5 150V162.5H425V150H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 150.391H425.391V162.109H437.109V150.391ZM425 150V162.5H437.5V150H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 150.391H437.891V162.109H449.609V150.391ZM437.5 150V162.5H450V150H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 150.391H450.391V162.109H462.109V150.391ZM450 150V162.5H462.5V150H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 150.391H462.891V162.109H474.609V150.391ZM462.5 150V162.5H475V150H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 150.391H475.391V162.109H487.109V150.391ZM475 150V162.5H487.5V150H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 150.391H487.891V162.109H499.609V150.391ZM487.5 150V162.5H500V150H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 150.391H500.391V162.109H512.109V150.391ZM500 150V162.5H512.5V150H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 150.391H512.891V162.109H524.609V150.391ZM512.5 150V162.5H525V150H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 150.391H525.391V162.109H537.109V150.391ZM525 150V162.5H537.5V150H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 150.391H537.891V162.109H549.609V150.391ZM537.5 150V162.5H550V150H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 150.391H550.391V162.109H562.109V150.391ZM550 150V162.5H562.5V150H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 150.391H562.891V162.109H574.609V150.391ZM562.5 150V162.5H575V150H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 150.391H575.391V162.109H587.109V150.391ZM575 150V162.5H587.5V150H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 150.391H587.891V162.109H599.609V150.391ZM587.5 150V162.5H600V150H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 162.891H0.390625V174.609H12.1094V162.891ZM0 162.5V175H12.5V162.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 162.891H12.8906V174.609H24.6094V162.891ZM12.5 162.5V175H25V162.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 162.891H25.3906V174.609H37.1094V162.891ZM25 162.5V175H37.5V162.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 162.891H37.8906V174.609H49.6094V162.891ZM37.5 162.5V175H50V162.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 162.891H50.3906V174.609H62.1094V162.891ZM50 162.5V175H62.5V162.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 162.891H62.8906V174.609H74.6094V162.891ZM62.5 162.5V175H75V162.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 162.891H75.3906V174.609H87.1094V162.891ZM75 162.5V175H87.5V162.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 162.891H87.8906V174.609H99.6094V162.891ZM87.5 162.5V175H100V162.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 162.891H100.391V174.609H112.109V162.891ZM100 162.5V175H112.5V162.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 162.891H112.891V174.609H124.609V162.891ZM112.5 162.5V175H125V162.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 162.891H125.391V174.609H137.109V162.891ZM125 162.5V175H137.5V162.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 162.891H137.891V174.609H149.609V162.891ZM137.5 162.5V175H150V162.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 162.891H150.391V174.609H162.109V162.891ZM150 162.5V175H162.5V162.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 162.891H162.891V174.609H174.609V162.891ZM162.5 162.5V175H175V162.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 162.891H175.391V174.609H187.109V162.891ZM175 162.5V175H187.5V162.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 162.891H187.891V174.609H199.609V162.891ZM187.5 162.5V175H200V162.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 162.891H200.391V174.609H212.109V162.891ZM200 162.5V175H212.5V162.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 162.891H212.891V174.609H224.609V162.891ZM212.5 162.5V175H225V162.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 162.891H225.391V174.609H237.109V162.891ZM225 162.5V175H237.5V162.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 162.891H237.891V174.609H249.609V162.891ZM237.5 162.5V175H250V162.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 162.891H250.391V174.609H262.109V162.891ZM250 162.5V175H262.5V162.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 162.891H262.891V174.609H274.609V162.891ZM262.5 162.5V175H275V162.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 162.891H275.391V174.609H287.109V162.891ZM275 162.5V175H287.5V162.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 162.891H287.891V174.609H299.609V162.891ZM287.5 162.5V175H300V162.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 162.891H300.391V174.609H312.109V162.891ZM300 162.5V175H312.5V162.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 162.891H312.891V174.609H324.609V162.891ZM312.5 162.5V175H325V162.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 162.891H325.391V174.609H337.109V162.891ZM325 162.5V175H337.5V162.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 162.891H337.891V174.609H349.609V162.891ZM337.5 162.5V175H350V162.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 162.891H350.391V174.609H362.109V162.891ZM350 162.5V175H362.5V162.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 162.891H362.891V174.609H374.609V162.891ZM362.5 162.5V175H375V162.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 162.891H375.391V174.609H387.109V162.891ZM375 162.5V175H387.5V162.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 162.891H387.891V174.609H399.609V162.891ZM387.5 162.5V175H400V162.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 162.891H400.391V174.609H412.109V162.891ZM400 162.5V175H412.5V162.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 162.891H412.891V174.609H424.609V162.891ZM412.5 162.5V175H425V162.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 162.891H425.391V174.609H437.109V162.891ZM425 162.5V175H437.5V162.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 162.891H437.891V174.609H449.609V162.891ZM437.5 162.5V175H450V162.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 162.891H450.391V174.609H462.109V162.891ZM450 162.5V175H462.5V162.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 162.891H462.891V174.609H474.609V162.891ZM462.5 162.5V175H475V162.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 162.891H475.391V174.609H487.109V162.891ZM475 162.5V175H487.5V162.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 162.891H487.891V174.609H499.609V162.891ZM487.5 162.5V175H500V162.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 162.891H500.391V174.609H512.109V162.891ZM500 162.5V175H512.5V162.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 162.891H512.891V174.609H524.609V162.891ZM512.5 162.5V175H525V162.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 162.891H525.391V174.609H537.109V162.891ZM525 162.5V175H537.5V162.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 162.891H537.891V174.609H549.609V162.891ZM537.5 162.5V175H550V162.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 162.891H550.391V174.609H562.109V162.891ZM550 162.5V175H562.5V162.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 162.891H562.891V174.609H574.609V162.891ZM562.5 162.5V175H575V162.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 162.891H575.391V174.609H587.109V162.891ZM575 162.5V175H587.5V162.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 162.891H587.891V174.609H599.609V162.891ZM587.5 162.5V175H600V162.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 175.391H0.390625V187.109H12.1094V175.391ZM0 175V187.5H12.5V175H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 175.391H12.8906V187.109H24.6094V175.391ZM12.5 175V187.5H25V175H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 175.391H25.3906V187.109H37.1094V175.391ZM25 175V187.5H37.5V175H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 175.391H37.8906V187.109H49.6094V175.391ZM37.5 175V187.5H50V175H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 175.391H50.3906V187.109H62.1094V175.391ZM50 175V187.5H62.5V175H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 175.391H62.8906V187.109H74.6094V175.391ZM62.5 175V187.5H75V175H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 175.391H75.3906V187.109H87.1094V175.391ZM75 175V187.5H87.5V175H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 175.391H87.8906V187.109H99.6094V175.391ZM87.5 175V187.5H100V175H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 175.391H100.391V187.109H112.109V175.391ZM100 175V187.5H112.5V175H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 175.391H112.891V187.109H124.609V175.391ZM112.5 175V187.5H125V175H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 175.391H125.391V187.109H137.109V175.391ZM125 175V187.5H137.5V175H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 175.391H137.891V187.109H149.609V175.391ZM137.5 175V187.5H150V175H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 175.391H150.391V187.109H162.109V175.391ZM150 175V187.5H162.5V175H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 175.391H162.891V187.109H174.609V175.391ZM162.5 175V187.5H175V175H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 175.391H175.391V187.109H187.109V175.391ZM175 175V187.5H187.5V175H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 175.391H187.891V187.109H199.609V175.391ZM187.5 175V187.5H200V175H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 175.391H200.391V187.109H212.109V175.391ZM200 175V187.5H212.5V175H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 175.391H212.891V187.109H224.609V175.391ZM212.5 175V187.5H225V175H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 175.391H225.391V187.109H237.109V175.391ZM225 175V187.5H237.5V175H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 175.391H237.891V187.109H249.609V175.391ZM237.5 175V187.5H250V175H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 175.391H250.391V187.109H262.109V175.391ZM250 175V187.5H262.5V175H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 175.391H262.891V187.109H274.609V175.391ZM262.5 175V187.5H275V175H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 175.391H275.391V187.109H287.109V175.391ZM275 175V187.5H287.5V175H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 175.391H287.891V187.109H299.609V175.391ZM287.5 175V187.5H300V175H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 175.391H300.391V187.109H312.109V175.391ZM300 175V187.5H312.5V175H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 175.391H312.891V187.109H324.609V175.391ZM312.5 175V187.5H325V175H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 175.391H325.391V187.109H337.109V175.391ZM325 175V187.5H337.5V175H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 175.391H337.891V187.109H349.609V175.391ZM337.5 175V187.5H350V175H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 175.391H350.391V187.109H362.109V175.391ZM350 175V187.5H362.5V175H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 175.391H362.891V187.109H374.609V175.391ZM362.5 175V187.5H375V175H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 175.391H375.391V187.109H387.109V175.391ZM375 175V187.5H387.5V175H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 175.391H387.891V187.109H399.609V175.391ZM387.5 175V187.5H400V175H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 175.391H400.391V187.109H412.109V175.391ZM400 175V187.5H412.5V175H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 175.391H412.891V187.109H424.609V175.391ZM412.5 175V187.5H425V175H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 175.391H425.391V187.109H437.109V175.391ZM425 175V187.5H437.5V175H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 175.391H437.891V187.109H449.609V175.391ZM437.5 175V187.5H450V175H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 175.391H450.391V187.109H462.109V175.391ZM450 175V187.5H462.5V175H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 175.391H462.891V187.109H474.609V175.391ZM462.5 175V187.5H475V175H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 175.391H475.391V187.109H487.109V175.391ZM475 175V187.5H487.5V175H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 175.391H487.891V187.109H499.609V175.391ZM487.5 175V187.5H500V175H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 175.391H500.391V187.109H512.109V175.391ZM500 175V187.5H512.5V175H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 175.391H512.891V187.109H524.609V175.391ZM512.5 175V187.5H525V175H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 175.391H525.391V187.109H537.109V175.391ZM525 175V187.5H537.5V175H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 175.391H537.891V187.109H549.609V175.391ZM537.5 175V187.5H550V175H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 175.391H550.391V187.109H562.109V175.391ZM550 175V187.5H562.5V175H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 175.391H562.891V187.109H574.609V175.391ZM562.5 175V187.5H575V175H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 175.391H575.391V187.109H587.109V175.391ZM575 175V187.5H587.5V175H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 175.391H587.891V187.109H599.609V175.391ZM587.5 175V187.5H600V175H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 187.891H0.390625V199.609H12.1094V187.891ZM0 187.5V200H12.5V187.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 187.891H12.8906V199.609H24.6094V187.891ZM12.5 187.5V200H25V187.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 187.891H25.3906V199.609H37.1094V187.891ZM25 187.5V200H37.5V187.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 187.891H37.8906V199.609H49.6094V187.891ZM37.5 187.5V200H50V187.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 187.891H50.3906V199.609H62.1094V187.891ZM50 187.5V200H62.5V187.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 187.891H62.8906V199.609H74.6094V187.891ZM62.5 187.5V200H75V187.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 187.891H75.3906V199.609H87.1094V187.891ZM75 187.5V200H87.5V187.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 187.891H87.8906V199.609H99.6094V187.891ZM87.5 187.5V200H100V187.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 187.891H100.391V199.609H112.109V187.891ZM100 187.5V200H112.5V187.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 187.891H112.891V199.609H124.609V187.891ZM112.5 187.5V200H125V187.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 187.891H125.391V199.609H137.109V187.891ZM125 187.5V200H137.5V187.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 187.891H137.891V199.609H149.609V187.891ZM137.5 187.5V200H150V187.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 187.891H150.391V199.609H162.109V187.891ZM150 187.5V200H162.5V187.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 187.891H162.891V199.609H174.609V187.891ZM162.5 187.5V200H175V187.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 187.891H175.391V199.609H187.109V187.891ZM175 187.5V200H187.5V187.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 187.891H187.891V199.609H199.609V187.891ZM187.5 187.5V200H200V187.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 187.891H200.391V199.609H212.109V187.891ZM200 187.5V200H212.5V187.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 187.891H212.891V199.609H224.609V187.891ZM212.5 187.5V200H225V187.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 187.891H225.391V199.609H237.109V187.891ZM225 187.5V200H237.5V187.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 187.891H237.891V199.609H249.609V187.891ZM237.5 187.5V200H250V187.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 187.891H250.391V199.609H262.109V187.891ZM250 187.5V200H262.5V187.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 187.891H262.891V199.609H274.609V187.891ZM262.5 187.5V200H275V187.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 187.891H275.391V199.609H287.109V187.891ZM275 187.5V200H287.5V187.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 187.891H287.891V199.609H299.609V187.891ZM287.5 187.5V200H300V187.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 187.891H300.391V199.609H312.109V187.891ZM300 187.5V200H312.5V187.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 187.891H312.891V199.609H324.609V187.891ZM312.5 187.5V200H325V187.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 187.891H325.391V199.609H337.109V187.891ZM325 187.5V200H337.5V187.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 187.891H337.891V199.609H349.609V187.891ZM337.5 187.5V200H350V187.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 187.891H350.391V199.609H362.109V187.891ZM350 187.5V200H362.5V187.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 187.891H362.891V199.609H374.609V187.891ZM362.5 187.5V200H375V187.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 187.891H375.391V199.609H387.109V187.891ZM375 187.5V200H387.5V187.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 187.891H387.891V199.609H399.609V187.891ZM387.5 187.5V200H400V187.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 187.891H400.391V199.609H412.109V187.891ZM400 187.5V200H412.5V187.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 187.891H412.891V199.609H424.609V187.891ZM412.5 187.5V200H425V187.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 187.891H425.391V199.609H437.109V187.891ZM425 187.5V200H437.5V187.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 187.891H437.891V199.609H449.609V187.891ZM437.5 187.5V200H450V187.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 187.891H450.391V199.609H462.109V187.891ZM450 187.5V200H462.5V187.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 187.891H462.891V199.609H474.609V187.891ZM462.5 187.5V200H475V187.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 187.891H475.391V199.609H487.109V187.891ZM475 187.5V200H487.5V187.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 187.891H487.891V199.609H499.609V187.891ZM487.5 187.5V200H500V187.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 187.891H500.391V199.609H512.109V187.891ZM500 187.5V200H512.5V187.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 187.891H512.891V199.609H524.609V187.891ZM512.5 187.5V200H525V187.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 187.891H525.391V199.609H537.109V187.891ZM525 187.5V200H537.5V187.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 187.891H537.891V199.609H549.609V187.891ZM537.5 187.5V200H550V187.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 187.891H550.391V199.609H562.109V187.891ZM550 187.5V200H562.5V187.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 187.891H562.891V199.609H574.609V187.891ZM562.5 187.5V200H575V187.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 187.891H575.391V199.609H587.109V187.891ZM575 187.5V200H587.5V187.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 187.891H587.891V199.609H599.609V187.891ZM587.5 187.5V200H600V187.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 200.391H0.390625V212.109H12.1094V200.391ZM0 200V212.5H12.5V200H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 200.391H12.8906V212.109H24.6094V200.391ZM12.5 200V212.5H25V200H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 200.391H25.3906V212.109H37.1094V200.391ZM25 200V212.5H37.5V200H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 200.391H37.8906V212.109H49.6094V200.391ZM37.5 200V212.5H50V200H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 200.391H50.3906V212.109H62.1094V200.391ZM50 200V212.5H62.5V200H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 200.391H62.8906V212.109H74.6094V200.391ZM62.5 200V212.5H75V200H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 200.391H75.3906V212.109H87.1094V200.391ZM75 200V212.5H87.5V200H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 200.391H87.8906V212.109H99.6094V200.391ZM87.5 200V212.5H100V200H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 200.391H100.391V212.109H112.109V200.391ZM100 200V212.5H112.5V200H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 200.391H112.891V212.109H124.609V200.391ZM112.5 200V212.5H125V200H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 200.391H125.391V212.109H137.109V200.391ZM125 200V212.5H137.5V200H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 200.391H137.891V212.109H149.609V200.391ZM137.5 200V212.5H150V200H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 200.391H150.391V212.109H162.109V200.391ZM150 200V212.5H162.5V200H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 200.391H162.891V212.109H174.609V200.391ZM162.5 200V212.5H175V200H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 200.391H175.391V212.109H187.109V200.391ZM175 200V212.5H187.5V200H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 200.391H187.891V212.109H199.609V200.391ZM187.5 200V212.5H200V200H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 200.391H200.391V212.109H212.109V200.391ZM200 200V212.5H212.5V200H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 200.391H212.891V212.109H224.609V200.391ZM212.5 200V212.5H225V200H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 200.391H225.391V212.109H237.109V200.391ZM225 200V212.5H237.5V200H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 200.391H237.891V212.109H249.609V200.391ZM237.5 200V212.5H250V200H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 200.391H250.391V212.109H262.109V200.391ZM250 200V212.5H262.5V200H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 200.391H262.891V212.109H274.609V200.391ZM262.5 200V212.5H275V200H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 200.391H275.391V212.109H287.109V200.391ZM275 200V212.5H287.5V200H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 200.391H287.891V212.109H299.609V200.391ZM287.5 200V212.5H300V200H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 200.391H300.391V212.109H312.109V200.391ZM300 200V212.5H312.5V200H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 200.391H312.891V212.109H324.609V200.391ZM312.5 200V212.5H325V200H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 200.391H325.391V212.109H337.109V200.391ZM325 200V212.5H337.5V200H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 200.391H337.891V212.109H349.609V200.391ZM337.5 200V212.5H350V200H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 200.391H350.391V212.109H362.109V200.391ZM350 200V212.5H362.5V200H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 200.391H362.891V212.109H374.609V200.391ZM362.5 200V212.5H375V200H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 200.391H375.391V212.109H387.109V200.391ZM375 200V212.5H387.5V200H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 200.391H387.891V212.109H399.609V200.391ZM387.5 200V212.5H400V200H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 200.391H400.391V212.109H412.109V200.391ZM400 200V212.5H412.5V200H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 200.391H412.891V212.109H424.609V200.391ZM412.5 200V212.5H425V200H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 200.391H425.391V212.109H437.109V200.391ZM425 200V212.5H437.5V200H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 200.391H437.891V212.109H449.609V200.391ZM437.5 200V212.5H450V200H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 200.391H450.391V212.109H462.109V200.391ZM450 200V212.5H462.5V200H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 200.391H462.891V212.109H474.609V200.391ZM462.5 200V212.5H475V200H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 200.391H475.391V212.109H487.109V200.391ZM475 200V212.5H487.5V200H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 200.391H487.891V212.109H499.609V200.391ZM487.5 200V212.5H500V200H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 200.391H500.391V212.109H512.109V200.391ZM500 200V212.5H512.5V200H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 200.391H512.891V212.109H524.609V200.391ZM512.5 200V212.5H525V200H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 200.391H525.391V212.109H537.109V200.391ZM525 200V212.5H537.5V200H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 200.391H537.891V212.109H549.609V200.391ZM537.5 200V212.5H550V200H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 200.391H550.391V212.109H562.109V200.391ZM550 200V212.5H562.5V200H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 200.391H562.891V212.109H574.609V200.391ZM562.5 200V212.5H575V200H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 200.391H575.391V212.109H587.109V200.391ZM575 200V212.5H587.5V200H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 200.391H587.891V212.109H599.609V200.391ZM587.5 200V212.5H600V200H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 212.891H0.390625V224.609H12.1094V212.891ZM0 212.5V225H12.5V212.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 212.891H12.8906V224.609H24.6094V212.891ZM12.5 212.5V225H25V212.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 212.891H25.3906V224.609H37.1094V212.891ZM25 212.5V225H37.5V212.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 212.891H37.8906V224.609H49.6094V212.891ZM37.5 212.5V225H50V212.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 212.891H50.3906V224.609H62.1094V212.891ZM50 212.5V225H62.5V212.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 212.891H62.8906V224.609H74.6094V212.891ZM62.5 212.5V225H75V212.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 212.891H75.3906V224.609H87.1094V212.891ZM75 212.5V225H87.5V212.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 212.891H87.8906V224.609H99.6094V212.891ZM87.5 212.5V225H100V212.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 212.891H100.391V224.609H112.109V212.891ZM100 212.5V225H112.5V212.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 212.891H112.891V224.609H124.609V212.891ZM112.5 212.5V225H125V212.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 212.891H125.391V224.609H137.109V212.891ZM125 212.5V225H137.5V212.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 212.891H137.891V224.609H149.609V212.891ZM137.5 212.5V225H150V212.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 212.891H150.391V224.609H162.109V212.891ZM150 212.5V225H162.5V212.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 212.891H162.891V224.609H174.609V212.891ZM162.5 212.5V225H175V212.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 212.891H175.391V224.609H187.109V212.891ZM175 212.5V225H187.5V212.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 212.891H187.891V224.609H199.609V212.891ZM187.5 212.5V225H200V212.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 212.891H200.391V224.609H212.109V212.891ZM200 212.5V225H212.5V212.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 212.891H212.891V224.609H224.609V212.891ZM212.5 212.5V225H225V212.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 212.891H225.391V224.609H237.109V212.891ZM225 212.5V225H237.5V212.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 212.891H237.891V224.609H249.609V212.891ZM237.5 212.5V225H250V212.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 212.891H250.391V224.609H262.109V212.891ZM250 212.5V225H262.5V212.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 212.891H262.891V224.609H274.609V212.891ZM262.5 212.5V225H275V212.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 212.891H275.391V224.609H287.109V212.891ZM275 212.5V225H287.5V212.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 212.891H287.891V224.609H299.609V212.891ZM287.5 212.5V225H300V212.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 212.891H300.391V224.609H312.109V212.891ZM300 212.5V225H312.5V212.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 212.891H312.891V224.609H324.609V212.891ZM312.5 212.5V225H325V212.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 212.891H325.391V224.609H337.109V212.891ZM325 212.5V225H337.5V212.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 212.891H337.891V224.609H349.609V212.891ZM337.5 212.5V225H350V212.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 212.891H350.391V224.609H362.109V212.891ZM350 212.5V225H362.5V212.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 212.891H362.891V224.609H374.609V212.891ZM362.5 212.5V225H375V212.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 212.891H375.391V224.609H387.109V212.891ZM375 212.5V225H387.5V212.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 212.891H387.891V224.609H399.609V212.891ZM387.5 212.5V225H400V212.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 212.891H400.391V224.609H412.109V212.891ZM400 212.5V225H412.5V212.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 212.891H412.891V224.609H424.609V212.891ZM412.5 212.5V225H425V212.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 212.891H425.391V224.609H437.109V212.891ZM425 212.5V225H437.5V212.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 212.891H437.891V224.609H449.609V212.891ZM437.5 212.5V225H450V212.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 212.891H450.391V224.609H462.109V212.891ZM450 212.5V225H462.5V212.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 212.891H462.891V224.609H474.609V212.891ZM462.5 212.5V225H475V212.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 212.891H475.391V224.609H487.109V212.891ZM475 212.5V225H487.5V212.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 212.891H487.891V224.609H499.609V212.891ZM487.5 212.5V225H500V212.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 212.891H500.391V224.609H512.109V212.891ZM500 212.5V225H512.5V212.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 212.891H512.891V224.609H524.609V212.891ZM512.5 212.5V225H525V212.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 212.891H525.391V224.609H537.109V212.891ZM525 212.5V225H537.5V212.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 212.891H537.891V224.609H549.609V212.891ZM537.5 212.5V225H550V212.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 212.891H550.391V224.609H562.109V212.891ZM550 212.5V225H562.5V212.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 212.891H562.891V224.609H574.609V212.891ZM562.5 212.5V225H575V212.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 212.891H575.391V224.609H587.109V212.891ZM575 212.5V225H587.5V212.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 212.891H587.891V224.609H599.609V212.891ZM587.5 212.5V225H600V212.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 225.391H0.390625V237.109H12.1094V225.391ZM0 225V237.5H12.5V225H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 225.391H12.8906V237.109H24.6094V225.391ZM12.5 225V237.5H25V225H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 225.391H25.3906V237.109H37.1094V225.391ZM25 225V237.5H37.5V225H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 225.391H37.8906V237.109H49.6094V225.391ZM37.5 225V237.5H50V225H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 225.391H50.3906V237.109H62.1094V225.391ZM50 225V237.5H62.5V225H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 225.391H62.8906V237.109H74.6094V225.391ZM62.5 225V237.5H75V225H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 225.391H75.3906V237.109H87.1094V225.391ZM75 225V237.5H87.5V225H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 225.391H87.8906V237.109H99.6094V225.391ZM87.5 225V237.5H100V225H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 225.391H100.391V237.109H112.109V225.391ZM100 225V237.5H112.5V225H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 225.391H112.891V237.109H124.609V225.391ZM112.5 225V237.5H125V225H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 225.391H125.391V237.109H137.109V225.391ZM125 225V237.5H137.5V225H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 225.391H137.891V237.109H149.609V225.391ZM137.5 225V237.5H150V225H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 225.391H150.391V237.109H162.109V225.391ZM150 225V237.5H162.5V225H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 225.391H162.891V237.109H174.609V225.391ZM162.5 225V237.5H175V225H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 225.391H175.391V237.109H187.109V225.391ZM175 225V237.5H187.5V225H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 225.391H187.891V237.109H199.609V225.391ZM187.5 225V237.5H200V225H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 225.391H200.391V237.109H212.109V225.391ZM200 225V237.5H212.5V225H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 225.391H212.891V237.109H224.609V225.391ZM212.5 225V237.5H225V225H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 225.391H225.391V237.109H237.109V225.391ZM225 225V237.5H237.5V225H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 225.391H237.891V237.109H249.609V225.391ZM237.5 225V237.5H250V225H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 225.391H250.391V237.109H262.109V225.391ZM250 225V237.5H262.5V225H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 225.391H262.891V237.109H274.609V225.391ZM262.5 225V237.5H275V225H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 225.391H275.391V237.109H287.109V225.391ZM275 225V237.5H287.5V225H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 225.391H287.891V237.109H299.609V225.391ZM287.5 225V237.5H300V225H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 225.391H300.391V237.109H312.109V225.391ZM300 225V237.5H312.5V225H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 225.391H312.891V237.109H324.609V225.391ZM312.5 225V237.5H325V225H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 225.391H325.391V237.109H337.109V225.391ZM325 225V237.5H337.5V225H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 225.391H337.891V237.109H349.609V225.391ZM337.5 225V237.5H350V225H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 225.391H350.391V237.109H362.109V225.391ZM350 225V237.5H362.5V225H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 225.391H362.891V237.109H374.609V225.391ZM362.5 225V237.5H375V225H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 225.391H375.391V237.109H387.109V225.391ZM375 225V237.5H387.5V225H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 225.391H387.891V237.109H399.609V225.391ZM387.5 225V237.5H400V225H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 225.391H400.391V237.109H412.109V225.391ZM400 225V237.5H412.5V225H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 225.391H412.891V237.109H424.609V225.391ZM412.5 225V237.5H425V225H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 225.391H425.391V237.109H437.109V225.391ZM425 225V237.5H437.5V225H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 225.391H437.891V237.109H449.609V225.391ZM437.5 225V237.5H450V225H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 225.391H450.391V237.109H462.109V225.391ZM450 225V237.5H462.5V225H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 225.391H462.891V237.109H474.609V225.391ZM462.5 225V237.5H475V225H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 225.391H475.391V237.109H487.109V225.391ZM475 225V237.5H487.5V225H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 225.391H487.891V237.109H499.609V225.391ZM487.5 225V237.5H500V225H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 225.391H500.391V237.109H512.109V225.391ZM500 225V237.5H512.5V225H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 225.391H512.891V237.109H524.609V225.391ZM512.5 225V237.5H525V225H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 225.391H525.391V237.109H537.109V225.391ZM525 225V237.5H537.5V225H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 225.391H537.891V237.109H549.609V225.391ZM537.5 225V237.5H550V225H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 225.391H550.391V237.109H562.109V225.391ZM550 225V237.5H562.5V225H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 225.391H562.891V237.109H574.609V225.391ZM562.5 225V237.5H575V225H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 225.391H575.391V237.109H587.109V225.391ZM575 225V237.5H587.5V225H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 225.391H587.891V237.109H599.609V225.391ZM587.5 225V237.5H600V225H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 237.891H0.390625V249.609H12.1094V237.891ZM0 237.5V250H12.5V237.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 237.891H12.8906V249.609H24.6094V237.891ZM12.5 237.5V250H25V237.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 237.891H25.3906V249.609H37.1094V237.891ZM25 237.5V250H37.5V237.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 237.891H37.8906V249.609H49.6094V237.891ZM37.5 237.5V250H50V237.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 237.891H50.3906V249.609H62.1094V237.891ZM50 237.5V250H62.5V237.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 237.891H62.8906V249.609H74.6094V237.891ZM62.5 237.5V250H75V237.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 237.891H75.3906V249.609H87.1094V237.891ZM75 237.5V250H87.5V237.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 237.891H87.8906V249.609H99.6094V237.891ZM87.5 237.5V250H100V237.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 237.891H100.391V249.609H112.109V237.891ZM100 237.5V250H112.5V237.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 237.891H112.891V249.609H124.609V237.891ZM112.5 237.5V250H125V237.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 237.891H125.391V249.609H137.109V237.891ZM125 237.5V250H137.5V237.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 237.891H137.891V249.609H149.609V237.891ZM137.5 237.5V250H150V237.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 237.891H150.391V249.609H162.109V237.891ZM150 237.5V250H162.5V237.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 237.891H162.891V249.609H174.609V237.891ZM162.5 237.5V250H175V237.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 237.891H175.391V249.609H187.109V237.891ZM175 237.5V250H187.5V237.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 237.891H187.891V249.609H199.609V237.891ZM187.5 237.5V250H200V237.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 237.891H200.391V249.609H212.109V237.891ZM200 237.5V250H212.5V237.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 237.891H212.891V249.609H224.609V237.891ZM212.5 237.5V250H225V237.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 237.891H225.391V249.609H237.109V237.891ZM225 237.5V250H237.5V237.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 237.891H237.891V249.609H249.609V237.891ZM237.5 237.5V250H250V237.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 237.891H250.391V249.609H262.109V237.891ZM250 237.5V250H262.5V237.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 237.891H262.891V249.609H274.609V237.891ZM262.5 237.5V250H275V237.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 237.891H275.391V249.609H287.109V237.891ZM275 237.5V250H287.5V237.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 237.891H287.891V249.609H299.609V237.891ZM287.5 237.5V250H300V237.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 237.891H300.391V249.609H312.109V237.891ZM300 237.5V250H312.5V237.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 237.891H312.891V249.609H324.609V237.891ZM312.5 237.5V250H325V237.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 237.891H325.391V249.609H337.109V237.891ZM325 237.5V250H337.5V237.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 237.891H337.891V249.609H349.609V237.891ZM337.5 237.5V250H350V237.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 237.891H350.391V249.609H362.109V237.891ZM350 237.5V250H362.5V237.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 237.891H362.891V249.609H374.609V237.891ZM362.5 237.5V250H375V237.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 237.891H375.391V249.609H387.109V237.891ZM375 237.5V250H387.5V237.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 237.891H387.891V249.609H399.609V237.891ZM387.5 237.5V250H400V237.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 237.891H400.391V249.609H412.109V237.891ZM400 237.5V250H412.5V237.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 237.891H412.891V249.609H424.609V237.891ZM412.5 237.5V250H425V237.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 237.891H425.391V249.609H437.109V237.891ZM425 237.5V250H437.5V237.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 237.891H437.891V249.609H449.609V237.891ZM437.5 237.5V250H450V237.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 237.891H450.391V249.609H462.109V237.891ZM450 237.5V250H462.5V237.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 237.891H462.891V249.609H474.609V237.891ZM462.5 237.5V250H475V237.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 237.891H475.391V249.609H487.109V237.891ZM475 237.5V250H487.5V237.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 237.891H487.891V249.609H499.609V237.891ZM487.5 237.5V250H500V237.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 237.891H500.391V249.609H512.109V237.891ZM500 237.5V250H512.5V237.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 237.891H512.891V249.609H524.609V237.891ZM512.5 237.5V250H525V237.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 237.891H525.391V249.609H537.109V237.891ZM525 237.5V250H537.5V237.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 237.891H537.891V249.609H549.609V237.891ZM537.5 237.5V250H550V237.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 237.891H550.391V249.609H562.109V237.891ZM550 237.5V250H562.5V237.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 237.891H562.891V249.609H574.609V237.891ZM562.5 237.5V250H575V237.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 237.891H575.391V249.609H587.109V237.891ZM575 237.5V250H587.5V237.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 237.891H587.891V249.609H599.609V237.891ZM587.5 237.5V250H600V237.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 250.391H0.390625V262.109H12.1094V250.391ZM0 250V262.5H12.5V250H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 250.391H12.8906V262.109H24.6094V250.391ZM12.5 250V262.5H25V250H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 250.391H25.3906V262.109H37.1094V250.391ZM25 250V262.5H37.5V250H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 250.391H37.8906V262.109H49.6094V250.391ZM37.5 250V262.5H50V250H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 250.391H50.3906V262.109H62.1094V250.391ZM50 250V262.5H62.5V250H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 250.391H62.8906V262.109H74.6094V250.391ZM62.5 250V262.5H75V250H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 250.391H75.3906V262.109H87.1094V250.391ZM75 250V262.5H87.5V250H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 250.391H87.8906V262.109H99.6094V250.391ZM87.5 250V262.5H100V250H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 250.391H100.391V262.109H112.109V250.391ZM100 250V262.5H112.5V250H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 250.391H112.891V262.109H124.609V250.391ZM112.5 250V262.5H125V250H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 250.391H125.391V262.109H137.109V250.391ZM125 250V262.5H137.5V250H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 250.391H137.891V262.109H149.609V250.391ZM137.5 250V262.5H150V250H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 250.391H150.391V262.109H162.109V250.391ZM150 250V262.5H162.5V250H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 250.391H162.891V262.109H174.609V250.391ZM162.5 250V262.5H175V250H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 250.391H175.391V262.109H187.109V250.391ZM175 250V262.5H187.5V250H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 250.391H187.891V262.109H199.609V250.391ZM187.5 250V262.5H200V250H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 250.391H200.391V262.109H212.109V250.391ZM200 250V262.5H212.5V250H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 250.391H212.891V262.109H224.609V250.391ZM212.5 250V262.5H225V250H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 250.391H225.391V262.109H237.109V250.391ZM225 250V262.5H237.5V250H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 250.391H237.891V262.109H249.609V250.391ZM237.5 250V262.5H250V250H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 250.391H250.391V262.109H262.109V250.391ZM250 250V262.5H262.5V250H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 250.391H262.891V262.109H274.609V250.391ZM262.5 250V262.5H275V250H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 250.391H275.391V262.109H287.109V250.391ZM275 250V262.5H287.5V250H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 250.391H287.891V262.109H299.609V250.391ZM287.5 250V262.5H300V250H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 250.391H300.391V262.109H312.109V250.391ZM300 250V262.5H312.5V250H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 250.391H312.891V262.109H324.609V250.391ZM312.5 250V262.5H325V250H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 250.391H325.391V262.109H337.109V250.391ZM325 250V262.5H337.5V250H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 250.391H337.891V262.109H349.609V250.391ZM337.5 250V262.5H350V250H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 250.391H350.391V262.109H362.109V250.391ZM350 250V262.5H362.5V250H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 250.391H362.891V262.109H374.609V250.391ZM362.5 250V262.5H375V250H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 250.391H375.391V262.109H387.109V250.391ZM375 250V262.5H387.5V250H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 250.391H387.891V262.109H399.609V250.391ZM387.5 250V262.5H400V250H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 250.391H400.391V262.109H412.109V250.391ZM400 250V262.5H412.5V250H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 250.391H412.891V262.109H424.609V250.391ZM412.5 250V262.5H425V250H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 250.391H425.391V262.109H437.109V250.391ZM425 250V262.5H437.5V250H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 250.391H437.891V262.109H449.609V250.391ZM437.5 250V262.5H450V250H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 250.391H450.391V262.109H462.109V250.391ZM450 250V262.5H462.5V250H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 250.391H462.891V262.109H474.609V250.391ZM462.5 250V262.5H475V250H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 250.391H475.391V262.109H487.109V250.391ZM475 250V262.5H487.5V250H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 250.391H487.891V262.109H499.609V250.391ZM487.5 250V262.5H500V250H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 250.391H500.391V262.109H512.109V250.391ZM500 250V262.5H512.5V250H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 250.391H512.891V262.109H524.609V250.391ZM512.5 250V262.5H525V250H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 250.391H525.391V262.109H537.109V250.391ZM525 250V262.5H537.5V250H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 250.391H537.891V262.109H549.609V250.391ZM537.5 250V262.5H550V250H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 250.391H550.391V262.109H562.109V250.391ZM550 250V262.5H562.5V250H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 250.391H562.891V262.109H574.609V250.391ZM562.5 250V262.5H575V250H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 250.391H575.391V262.109H587.109V250.391ZM575 250V262.5H587.5V250H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 250.391H587.891V262.109H599.609V250.391ZM587.5 250V262.5H600V250H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 262.891H0.390625V274.609H12.1094V262.891ZM0 262.5V275H12.5V262.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 262.891H12.8906V274.609H24.6094V262.891ZM12.5 262.5V275H25V262.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 262.891H25.3906V274.609H37.1094V262.891ZM25 262.5V275H37.5V262.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 262.891H37.8906V274.609H49.6094V262.891ZM37.5 262.5V275H50V262.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 262.891H50.3906V274.609H62.1094V262.891ZM50 262.5V275H62.5V262.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 262.891H62.8906V274.609H74.6094V262.891ZM62.5 262.5V275H75V262.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 262.891H75.3906V274.609H87.1094V262.891ZM75 262.5V275H87.5V262.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 262.891H87.8906V274.609H99.6094V262.891ZM87.5 262.5V275H100V262.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 262.891H100.391V274.609H112.109V262.891ZM100 262.5V275H112.5V262.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 262.891H112.891V274.609H124.609V262.891ZM112.5 262.5V275H125V262.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 262.891H125.391V274.609H137.109V262.891ZM125 262.5V275H137.5V262.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 262.891H137.891V274.609H149.609V262.891ZM137.5 262.5V275H150V262.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 262.891H150.391V274.609H162.109V262.891ZM150 262.5V275H162.5V262.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 262.891H162.891V274.609H174.609V262.891ZM162.5 262.5V275H175V262.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 262.891H175.391V274.609H187.109V262.891ZM175 262.5V275H187.5V262.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 262.891H187.891V274.609H199.609V262.891ZM187.5 262.5V275H200V262.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 262.891H200.391V274.609H212.109V262.891ZM200 262.5V275H212.5V262.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 262.891H212.891V274.609H224.609V262.891ZM212.5 262.5V275H225V262.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 262.891H225.391V274.609H237.109V262.891ZM225 262.5V275H237.5V262.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 262.891H237.891V274.609H249.609V262.891ZM237.5 262.5V275H250V262.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 262.891H250.391V274.609H262.109V262.891ZM250 262.5V275H262.5V262.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 262.891H262.891V274.609H274.609V262.891ZM262.5 262.5V275H275V262.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 262.891H275.391V274.609H287.109V262.891ZM275 262.5V275H287.5V262.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 262.891H287.891V274.609H299.609V262.891ZM287.5 262.5V275H300V262.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 262.891H300.391V274.609H312.109V262.891ZM300 262.5V275H312.5V262.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 262.891H312.891V274.609H324.609V262.891ZM312.5 262.5V275H325V262.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 262.891H325.391V274.609H337.109V262.891ZM325 262.5V275H337.5V262.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 262.891H337.891V274.609H349.609V262.891ZM337.5 262.5V275H350V262.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 262.891H350.391V274.609H362.109V262.891ZM350 262.5V275H362.5V262.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 262.891H362.891V274.609H374.609V262.891ZM362.5 262.5V275H375V262.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 262.891H375.391V274.609H387.109V262.891ZM375 262.5V275H387.5V262.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 262.891H387.891V274.609H399.609V262.891ZM387.5 262.5V275H400V262.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 262.891H400.391V274.609H412.109V262.891ZM400 262.5V275H412.5V262.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 262.891H412.891V274.609H424.609V262.891ZM412.5 262.5V275H425V262.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 262.891H425.391V274.609H437.109V262.891ZM425 262.5V275H437.5V262.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 262.891H437.891V274.609H449.609V262.891ZM437.5 262.5V275H450V262.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 262.891H450.391V274.609H462.109V262.891ZM450 262.5V275H462.5V262.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 262.891H462.891V274.609H474.609V262.891ZM462.5 262.5V275H475V262.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 262.891H475.391V274.609H487.109V262.891ZM475 262.5V275H487.5V262.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 262.891H487.891V274.609H499.609V262.891ZM487.5 262.5V275H500V262.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 262.891H500.391V274.609H512.109V262.891ZM500 262.5V275H512.5V262.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 262.891H512.891V274.609H524.609V262.891ZM512.5 262.5V275H525V262.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 262.891H525.391V274.609H537.109V262.891ZM525 262.5V275H537.5V262.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 262.891H537.891V274.609H549.609V262.891ZM537.5 262.5V275H550V262.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 262.891H550.391V274.609H562.109V262.891ZM550 262.5V275H562.5V262.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 262.891H562.891V274.609H574.609V262.891ZM562.5 262.5V275H575V262.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 262.891H575.391V274.609H587.109V262.891ZM575 262.5V275H587.5V262.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 262.891H587.891V274.609H599.609V262.891ZM587.5 262.5V275H600V262.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 275.391H0.390625V287.109H12.1094V275.391ZM0 275V287.5H12.5V275H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 275.391H12.8906V287.109H24.6094V275.391ZM12.5 275V287.5H25V275H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 275.391H25.3906V287.109H37.1094V275.391ZM25 275V287.5H37.5V275H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 275.391H37.8906V287.109H49.6094V275.391ZM37.5 275V287.5H50V275H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 275.391H50.3906V287.109H62.1094V275.391ZM50 275V287.5H62.5V275H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 275.391H62.8906V287.109H74.6094V275.391ZM62.5 275V287.5H75V275H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 275.391H75.3906V287.109H87.1094V275.391ZM75 275V287.5H87.5V275H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 275.391H87.8906V287.109H99.6094V275.391ZM87.5 275V287.5H100V275H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 275.391H100.391V287.109H112.109V275.391ZM100 275V287.5H112.5V275H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 275.391H112.891V287.109H124.609V275.391ZM112.5 275V287.5H125V275H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 275.391H125.391V287.109H137.109V275.391ZM125 275V287.5H137.5V275H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 275.391H137.891V287.109H149.609V275.391ZM137.5 275V287.5H150V275H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 275.391H150.391V287.109H162.109V275.391ZM150 275V287.5H162.5V275H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 275.391H162.891V287.109H174.609V275.391ZM162.5 275V287.5H175V275H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 275.391H175.391V287.109H187.109V275.391ZM175 275V287.5H187.5V275H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 275.391H187.891V287.109H199.609V275.391ZM187.5 275V287.5H200V275H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 275.391H200.391V287.109H212.109V275.391ZM200 275V287.5H212.5V275H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 275.391H212.891V287.109H224.609V275.391ZM212.5 275V287.5H225V275H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 275.391H225.391V287.109H237.109V275.391ZM225 275V287.5H237.5V275H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 275.391H237.891V287.109H249.609V275.391ZM237.5 275V287.5H250V275H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 275.391H250.391V287.109H262.109V275.391ZM250 275V287.5H262.5V275H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 275.391H262.891V287.109H274.609V275.391ZM262.5 275V287.5H275V275H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 275.391H275.391V287.109H287.109V275.391ZM275 275V287.5H287.5V275H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 275.391H287.891V287.109H299.609V275.391ZM287.5 275V287.5H300V275H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 275.391H300.391V287.109H312.109V275.391ZM300 275V287.5H312.5V275H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 275.391H312.891V287.109H324.609V275.391ZM312.5 275V287.5H325V275H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 275.391H325.391V287.109H337.109V275.391ZM325 275V287.5H337.5V275H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 275.391H337.891V287.109H349.609V275.391ZM337.5 275V287.5H350V275H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 275.391H350.391V287.109H362.109V275.391ZM350 275V287.5H362.5V275H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 275.391H362.891V287.109H374.609V275.391ZM362.5 275V287.5H375V275H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 275.391H375.391V287.109H387.109V275.391ZM375 275V287.5H387.5V275H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 275.391H387.891V287.109H399.609V275.391ZM387.5 275V287.5H400V275H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 275.391H400.391V287.109H412.109V275.391ZM400 275V287.5H412.5V275H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 275.391H412.891V287.109H424.609V275.391ZM412.5 275V287.5H425V275H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 275.391H425.391V287.109H437.109V275.391ZM425 275V287.5H437.5V275H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 275.391H437.891V287.109H449.609V275.391ZM437.5 275V287.5H450V275H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 275.391H450.391V287.109H462.109V275.391ZM450 275V287.5H462.5V275H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 275.391H462.891V287.109H474.609V275.391ZM462.5 275V287.5H475V275H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 275.391H475.391V287.109H487.109V275.391ZM475 275V287.5H487.5V275H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 275.391H487.891V287.109H499.609V275.391ZM487.5 275V287.5H500V275H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 275.391H500.391V287.109H512.109V275.391ZM500 275V287.5H512.5V275H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 275.391H512.891V287.109H524.609V275.391ZM512.5 275V287.5H525V275H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 275.391H525.391V287.109H537.109V275.391ZM525 275V287.5H537.5V275H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 275.391H537.891V287.109H549.609V275.391ZM537.5 275V287.5H550V275H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 275.391H550.391V287.109H562.109V275.391ZM550 275V287.5H562.5V275H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 275.391H562.891V287.109H574.609V275.391ZM562.5 275V287.5H575V275H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 275.391H575.391V287.109H587.109V275.391ZM575 275V287.5H587.5V275H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 275.391H587.891V287.109H599.609V275.391ZM587.5 275V287.5H600V275H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 287.891H0.390625V299.609H12.1094V287.891ZM0 287.5V300H12.5V287.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 287.891H12.8906V299.609H24.6094V287.891ZM12.5 287.5V300H25V287.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 287.891H25.3906V299.609H37.1094V287.891ZM25 287.5V300H37.5V287.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 287.891H37.8906V299.609H49.6094V287.891ZM37.5 287.5V300H50V287.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 287.891H50.3906V299.609H62.1094V287.891ZM50 287.5V300H62.5V287.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 287.891H62.8906V299.609H74.6094V287.891ZM62.5 287.5V300H75V287.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 287.891H75.3906V299.609H87.1094V287.891ZM75 287.5V300H87.5V287.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 287.891H87.8906V299.609H99.6094V287.891ZM87.5 287.5V300H100V287.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 287.891H100.391V299.609H112.109V287.891ZM100 287.5V300H112.5V287.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 287.891H112.891V299.609H124.609V287.891ZM112.5 287.5V300H125V287.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 287.891H125.391V299.609H137.109V287.891ZM125 287.5V300H137.5V287.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 287.891H137.891V299.609H149.609V287.891ZM137.5 287.5V300H150V287.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 287.891H150.391V299.609H162.109V287.891ZM150 287.5V300H162.5V287.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 287.891H162.891V299.609H174.609V287.891ZM162.5 287.5V300H175V287.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 287.891H175.391V299.609H187.109V287.891ZM175 287.5V300H187.5V287.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 287.891H187.891V299.609H199.609V287.891ZM187.5 287.5V300H200V287.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 287.891H200.391V299.609H212.109V287.891ZM200 287.5V300H212.5V287.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 287.891H212.891V299.609H224.609V287.891ZM212.5 287.5V300H225V287.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 287.891H225.391V299.609H237.109V287.891ZM225 287.5V300H237.5V287.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 287.891H237.891V299.609H249.609V287.891ZM237.5 287.5V300H250V287.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 287.891H250.391V299.609H262.109V287.891ZM250 287.5V300H262.5V287.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 287.891H262.891V299.609H274.609V287.891ZM262.5 287.5V300H275V287.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 287.891H275.391V299.609H287.109V287.891ZM275 287.5V300H287.5V287.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 287.891H287.891V299.609H299.609V287.891ZM287.5 287.5V300H300V287.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 287.891H300.391V299.609H312.109V287.891ZM300 287.5V300H312.5V287.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 287.891H312.891V299.609H324.609V287.891ZM312.5 287.5V300H325V287.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 287.891H325.391V299.609H337.109V287.891ZM325 287.5V300H337.5V287.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 287.891H337.891V299.609H349.609V287.891ZM337.5 287.5V300H350V287.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 287.891H350.391V299.609H362.109V287.891ZM350 287.5V300H362.5V287.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 287.891H362.891V299.609H374.609V287.891ZM362.5 287.5V300H375V287.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 287.891H375.391V299.609H387.109V287.891ZM375 287.5V300H387.5V287.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 287.891H387.891V299.609H399.609V287.891ZM387.5 287.5V300H400V287.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 287.891H400.391V299.609H412.109V287.891ZM400 287.5V300H412.5V287.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 287.891H412.891V299.609H424.609V287.891ZM412.5 287.5V300H425V287.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 287.891H425.391V299.609H437.109V287.891ZM425 287.5V300H437.5V287.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 287.891H437.891V299.609H449.609V287.891ZM437.5 287.5V300H450V287.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 287.891H450.391V299.609H462.109V287.891ZM450 287.5V300H462.5V287.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 287.891H462.891V299.609H474.609V287.891ZM462.5 287.5V300H475V287.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 287.891H475.391V299.609H487.109V287.891ZM475 287.5V300H487.5V287.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 287.891H487.891V299.609H499.609V287.891ZM487.5 287.5V300H500V287.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 287.891H500.391V299.609H512.109V287.891ZM500 287.5V300H512.5V287.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 287.891H512.891V299.609H524.609V287.891ZM512.5 287.5V300H525V287.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 287.891H525.391V299.609H537.109V287.891ZM525 287.5V300H537.5V287.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 287.891H537.891V299.609H549.609V287.891ZM537.5 287.5V300H550V287.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 287.891H550.391V299.609H562.109V287.891ZM550 287.5V300H562.5V287.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 287.891H562.891V299.609H574.609V287.891ZM562.5 287.5V300H575V287.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 287.891H575.391V299.609H587.109V287.891ZM575 287.5V300H587.5V287.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 287.891H587.891V299.609H599.609V287.891ZM587.5 287.5V300H600V287.5H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 300.391H0.390625V312.109H12.1094V300.391ZM0 300V312.5H12.5V300H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 300.391H12.8906V312.109H24.6094V300.391ZM12.5 300V312.5H25V300H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 300.391H25.3906V312.109H37.1094V300.391ZM25 300V312.5H37.5V300H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 300.391H37.8906V312.109H49.6094V300.391ZM37.5 300V312.5H50V300H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 300.391H50.3906V312.109H62.1094V300.391ZM50 300V312.5H62.5V300H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 300.391H62.8906V312.109H74.6094V300.391ZM62.5 300V312.5H75V300H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 300.391H75.3906V312.109H87.1094V300.391ZM75 300V312.5H87.5V300H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 300.391H87.8906V312.109H99.6094V300.391ZM87.5 300V312.5H100V300H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 300.391H100.391V312.109H112.109V300.391ZM100 300V312.5H112.5V300H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 300.391H112.891V312.109H124.609V300.391ZM112.5 300V312.5H125V300H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 300.391H125.391V312.109H137.109V300.391ZM125 300V312.5H137.5V300H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 300.391H137.891V312.109H149.609V300.391ZM137.5 300V312.5H150V300H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 300.391H150.391V312.109H162.109V300.391ZM150 300V312.5H162.5V300H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 300.391H162.891V312.109H174.609V300.391ZM162.5 300V312.5H175V300H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 300.391H175.391V312.109H187.109V300.391ZM175 300V312.5H187.5V300H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 300.391H187.891V312.109H199.609V300.391ZM187.5 300V312.5H200V300H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 300.391H200.391V312.109H212.109V300.391ZM200 300V312.5H212.5V300H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 300.391H212.891V312.109H224.609V300.391ZM212.5 300V312.5H225V300H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 300.391H225.391V312.109H237.109V300.391ZM225 300V312.5H237.5V300H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 300.391H237.891V312.109H249.609V300.391ZM237.5 300V312.5H250V300H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 300.391H250.391V312.109H262.109V300.391ZM250 300V312.5H262.5V300H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 300.391H262.891V312.109H274.609V300.391ZM262.5 300V312.5H275V300H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 300.391H275.391V312.109H287.109V300.391ZM275 300V312.5H287.5V300H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 300.391H287.891V312.109H299.609V300.391ZM287.5 300V312.5H300V300H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 300.391H300.391V312.109H312.109V300.391ZM300 300V312.5H312.5V300H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 300.391H312.891V312.109H324.609V300.391ZM312.5 300V312.5H325V300H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 300.391H325.391V312.109H337.109V300.391ZM325 300V312.5H337.5V300H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 300.391H337.891V312.109H349.609V300.391ZM337.5 300V312.5H350V300H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 300.391H350.391V312.109H362.109V300.391ZM350 300V312.5H362.5V300H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 300.391H362.891V312.109H374.609V300.391ZM362.5 300V312.5H375V300H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 300.391H375.391V312.109H387.109V300.391ZM375 300V312.5H387.5V300H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 300.391H387.891V312.109H399.609V300.391ZM387.5 300V312.5H400V300H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 300.391H400.391V312.109H412.109V300.391ZM400 300V312.5H412.5V300H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 300.391H412.891V312.109H424.609V300.391ZM412.5 300V312.5H425V300H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 300.391H425.391V312.109H437.109V300.391ZM425 300V312.5H437.5V300H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 300.391H437.891V312.109H449.609V300.391ZM437.5 300V312.5H450V300H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 300.391H450.391V312.109H462.109V300.391ZM450 300V312.5H462.5V300H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 300.391H462.891V312.109H474.609V300.391ZM462.5 300V312.5H475V300H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 300.391H475.391V312.109H487.109V300.391ZM475 300V312.5H487.5V300H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 300.391H487.891V312.109H499.609V300.391ZM487.5 300V312.5H500V300H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 300.391H500.391V312.109H512.109V300.391ZM500 300V312.5H512.5V300H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 300.391H512.891V312.109H524.609V300.391ZM512.5 300V312.5H525V300H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 300.391H525.391V312.109H537.109V300.391ZM525 300V312.5H537.5V300H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 300.391H537.891V312.109H549.609V300.391ZM537.5 300V312.5H550V300H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 300.391H550.391V312.109H562.109V300.391ZM550 300V312.5H562.5V300H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 300.391H562.891V312.109H574.609V300.391ZM562.5 300V312.5H575V300H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 300.391H575.391V312.109H587.109V300.391ZM575 300V312.5H587.5V300H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 300.391H587.891V312.109H599.609V300.391ZM587.5 300V312.5H600V300H587.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1094 312.891H0.390625V324.609H12.1094V312.891ZM0 312.5V325H12.5V312.5H0Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M24.6094 312.891H12.8906V324.609H24.6094V312.891ZM12.5 312.5V325H25V312.5H12.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1094 312.891H25.3906V324.609H37.1094V312.891ZM25 312.5V325H37.5V312.5H25Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6094 312.891H37.8906V324.609H49.6094V312.891ZM37.5 312.5V325H50V312.5H37.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M62.1094 312.891H50.3906V324.609H62.1094V312.891ZM50 312.5V325H62.5V312.5H50Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M74.6094 312.891H62.8906V324.609H74.6094V312.891ZM62.5 312.5V325H75V312.5H62.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.1094 312.891H75.3906V324.609H87.1094V312.891ZM75 312.5V325H87.5V312.5H75Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M99.6094 312.891H87.8906V324.609H99.6094V312.891ZM87.5 312.5V325H100V312.5H87.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M112.109 312.891H100.391V324.609H112.109V312.891ZM100 312.5V325H112.5V312.5H100Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M124.609 312.891H112.891V324.609H124.609V312.891ZM112.5 312.5V325H125V312.5H112.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M137.109 312.891H125.391V324.609H137.109V312.891ZM125 312.5V325H137.5V312.5H125Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M149.609 312.891H137.891V324.609H149.609V312.891ZM137.5 312.5V325H150V312.5H137.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M162.109 312.891H150.391V324.609H162.109V312.891ZM150 312.5V325H162.5V312.5H150Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M174.609 312.891H162.891V324.609H174.609V312.891ZM162.5 312.5V325H175V312.5H162.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.109 312.891H175.391V324.609H187.109V312.891ZM175 312.5V325H187.5V312.5H175Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M199.609 312.891H187.891V324.609H199.609V312.891ZM187.5 312.5V325H200V312.5H187.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.109 312.891H200.391V324.609H212.109V312.891ZM200 312.5V325H212.5V312.5H200Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M224.609 312.891H212.891V324.609H224.609V312.891ZM212.5 312.5V325H225V312.5H212.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M237.109 312.891H225.391V324.609H237.109V312.891ZM225 312.5V325H237.5V312.5H225Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M249.609 312.891H237.891V324.609H249.609V312.891ZM237.5 312.5V325H250V312.5H237.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M262.109 312.891H250.391V324.609H262.109V312.891ZM250 312.5V325H262.5V312.5H250Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M274.609 312.891H262.891V324.609H274.609V312.891ZM262.5 312.5V325H275V312.5H262.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M287.109 312.891H275.391V324.609H287.109V312.891ZM275 312.5V325H287.5V312.5H275Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M299.609 312.891H287.891V324.609H299.609V312.891ZM287.5 312.5V325H300V312.5H287.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M312.109 312.891H300.391V324.609H312.109V312.891ZM300 312.5V325H312.5V312.5H300Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M324.609 312.891H312.891V324.609H324.609V312.891ZM312.5 312.5V325H325V312.5H312.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M337.109 312.891H325.391V324.609H337.109V312.891ZM325 312.5V325H337.5V312.5H325Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M349.609 312.891H337.891V324.609H349.609V312.891ZM337.5 312.5V325H350V312.5H337.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M362.109 312.891H350.391V324.609H362.109V312.891ZM350 312.5V325H362.5V312.5H350Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M374.609 312.891H362.891V324.609H374.609V312.891ZM362.5 312.5V325H375V312.5H362.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M387.109 312.891H375.391V324.609H387.109V312.891ZM375 312.5V325H387.5V312.5H375Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M399.609 312.891H387.891V324.609H399.609V312.891ZM387.5 312.5V325H400V312.5H387.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M412.109 312.891H400.391V324.609H412.109V312.891ZM400 312.5V325H412.5V312.5H400Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M424.609 312.891H412.891V324.609H424.609V312.891ZM412.5 312.5V325H425V312.5H412.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M437.109 312.891H425.391V324.609H437.109V312.891ZM425 312.5V325H437.5V312.5H425Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M449.609 312.891H437.891V324.609H449.609V312.891ZM437.5 312.5V325H450V312.5H437.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M462.109 312.891H450.391V324.609H462.109V312.891ZM450 312.5V325H462.5V312.5H450Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M474.609 312.891H462.891V324.609H474.609V312.891ZM462.5 312.5V325H475V312.5H462.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M487.109 312.891H475.391V324.609H487.109V312.891ZM475 312.5V325H487.5V312.5H475Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M499.609 312.891H487.891V324.609H499.609V312.891ZM487.5 312.5V325H500V312.5H487.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M512.109 312.891H500.391V324.609H512.109V312.891ZM500 312.5V325H512.5V312.5H500Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M524.609 312.891H512.891V324.609H524.609V312.891ZM512.5 312.5V325H525V312.5H512.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M537.109 312.891H525.391V324.609H537.109V312.891ZM525 312.5V325H537.5V312.5H525Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M549.609 312.891H537.891V324.609H549.609V312.891ZM537.5 312.5V325H550V312.5H537.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M562.109 312.891H550.391V324.609H562.109V312.891ZM550 312.5V325H562.5V312.5H550Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M574.609 312.891H562.891V324.609H574.609V312.891ZM562.5 312.5V325H575V312.5H562.5Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M587.109 312.891H575.391V324.609H587.109V312.891ZM575 312.5V325H587.5V312.5H575Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M599.609 312.891H587.891V324.609H599.609V312.891ZM587.5 312.5V325H600V312.5H587.5Z" fill="black"/>
+</g>
+<defs>
+<clipPath id="clip0_2906_6463">
+<rect width="515" height="126" fill="white"/>
+</clipPath>
+</defs>
+</svg>
@@ -0,0 +1 @@
@@ -0,0 +1,46 @@
+<svg width="257" height="47" viewBox="0 0 257 47" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_100_26)">
+<path d="M119.922 24.4481L109.212 5.8996C107.081 2.20815 103.26 0 98.9973 0C94.7394 0 90.9279 2.19855 88.7918 5.8804L66.6239 44.2734C66.3646 44.7198 66.3646 45.2671 66.6239 45.7135C66.8831 46.1599 67.3583 46.4336 67.8719 46.4336H73.7715C74.2852 46.4336 74.7604 46.1599 75.0196 45.7135L76.9302 42.4013L77.5158 41.4652C77.5158 41.4652 77.535 41.4364 77.5398 41.422L84.6923 29.0324C84.7211 28.9844 84.7499 28.9316 84.7691 28.874C84.8363 28.7203 84.9035 28.5763 84.9851 28.4419L95.6946 9.89347C96.3811 8.70299 97.6148 7.98774 98.9925 7.98774C100.37 7.98774 101.604 8.69819 102.29 9.89347L113 28.4419C113.686 29.6324 113.691 31.0581 113 32.2486C112.313 33.4391 111.08 34.1543 109.702 34.1543H102.232C101.718 34.1543 101.243 34.4279 100.984 34.8744L98.0318 39.9819C97.7726 40.4283 97.7726 40.9756 98.0318 41.422C98.291 41.8684 98.7662 42.1421 99.2799 42.1421H109.404C113.965 42.1421 118.079 39.7275 120.133 35.844C122.039 32.2438 121.957 27.9859 119.912 24.4433L119.922 24.4481Z" fill="white"/>
+<path d="M82.8564 34.1538H104.17L103.872 42.1416H79.9042C79.3905 42.1416 78.9153 41.8679 78.6561 41.4215C78.3969 40.9751 78.3969 40.4278 78.6561 39.9814L81.6083 34.8739C81.8675 34.4274 82.3427 34.1538 82.8564 34.1538Z" fill="url(#paint0_linear_100_26)"/>
+<path d="M43.7123 25.1535C43.7123 24.9039 43.6451 24.659 43.5155 24.443L32.8972 6.14897C30.7131 2.36152 26.7288 -0.000244141 22.5093 -0.000244141C22.3701 -0.000244141 22.2309 -0.000244141 22.0869 0.00935651C18.0162 0.158167 14.368 2.36152 12.323 5.89936L1.61829 24.4478C-1.31951 29.5314 -0.181833 35.6374 4.44568 39.6361C6.31301 41.2538 8.78518 42.1418 11.4062 42.1418H31.3851C31.8987 42.1418 32.374 41.8682 32.6332 41.4218L35.5806 36.3142C35.8398 35.8678 35.8398 35.3206 35.5806 34.8741C35.3214 34.4277 34.8461 34.1541 34.3325 34.1541H11.8334C10.4557 34.1541 9.22201 33.4436 8.53556 32.2532C7.84431 31.0627 7.84431 29.6418 8.53556 28.4465L19.2451 9.89803C19.9315 8.70755 21.1652 7.9923 22.5429 7.9923C23.9206 7.9923 25.1495 8.70275 25.8407 9.89803L41.1346 36.4198C41.461 36.9863 42.1282 37.2599 42.7619 37.0919C43.3955 36.9191 43.8276 36.343 43.8228 35.6902L43.7171 25.1583L43.7123 25.1535Z" fill="url(#paint1_linear_100_26)"/>
+<path d="M4.44544 39.6362C-0.182077 35.6376 -1.31975 29.5315 1.61805 24.448L8.53532 28.4467C7.84407 29.6419 7.84407 31.0628 8.53532 32.2533C9.22176 33.4438 10.4554 34.1543 11.8331 34.1543H34.3323C34.8459 34.1543 35.3211 34.4279 35.5804 34.8743C35.8396 35.3207 35.8396 35.868 35.5804 36.3144L32.633 41.422C32.3737 41.8684 31.8985 42.142 31.3849 42.142H11.4059C8.78493 42.142 6.31276 41.2539 4.44544 39.6362Z" fill="white"/>
+<path d="M74.6308 34.8696C74.3716 34.4231 73.8964 34.1495 73.3827 34.1495H51.2532C49.8755 34.1495 48.6418 33.4391 47.9554 32.2486C47.2642 31.0581 47.2642 29.6372 47.9554 28.4419L58.6649 9.89347C59.3514 8.70299 60.5851 7.98774 61.9628 7.98774C63.3404 7.98774 64.5693 8.69819 65.2606 9.89347L65.899 10.9975C66.1582 11.444 66.6335 11.7176 67.1471 11.7176C67.6607 11.7176 68.1408 11.4392 68.3952 10.9927L71.2322 6.01961C71.5298 5.49637 71.4722 4.84353 71.0882 4.3827C68.7648 1.59851 65.4238 0 61.9291 0C61.7899 0 61.6507 0 61.5067 0.00960065C57.436 0.158411 53.7878 2.36176 51.7429 5.8996L41.0333 24.4481C38.0955 29.5316 39.2332 35.6376 43.8607 39.6363C45.728 41.254 48.2002 42.1421 50.8212 42.1421H70.4257C70.9394 42.1421 71.4146 41.8684 71.6738 41.422L74.626 36.3145C74.8852 35.868 74.8852 35.3208 74.626 34.8744L74.6308 34.8696Z" fill="url(#paint2_linear_100_26)"/>
+</g>
@@ -41,7 +41,7 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
- "ctrl-shift-i": "edit_prediction::ToggleMenu",
+ "ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
@@ -64,8 +64,8 @@
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
- "ctrl-backspace": "editor::DeleteToPreviousWordStart",
- "ctrl-delete": "editor::DeleteToNextWordEnd",
+ "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
+ "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"cut": "editor::Cut",
"shift-delete": "editor::Cut",
"ctrl-x": "editor::Cut",
@@ -121,7 +121,7 @@
"alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
- "ctrl-shift-e": "editor::ToggleEditPrediction",
+ "ctrl-alt-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint"
}
@@ -131,14 +131,14 @@
"bindings": {
"shift-enter": "editor::Newline",
"enter": "editor::Newline",
- "ctrl-enter": "editor::NewlineAbove",
- "ctrl-shift-enter": "editor::NewlineBelow",
+ "ctrl-enter": "editor::NewlineBelow",
+ "ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
- "ctrl->": "assistant::QuoteSelection",
+ "ctrl->": "agent::QuoteSelection",
"ctrl-<": "assistant::InsertIntoEditor",
"ctrl-alt-e": "editor::SelectEnclosingSymbol",
"ctrl-shift-backspace": "editor::GoToPreviousChange",
@@ -171,6 +171,7 @@
"context": "Markdown",
"bindings": {
"copy": "markdown::Copy",
+ "ctrl-insert": "markdown::Copy",
"ctrl-c": "markdown::Copy"
}
},
@@ -241,12 +242,15 @@
"ctrl-shift-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
- "ctrl->": "assistant::QuoteSelection",
+ "ctrl->": "agent::QuoteSelection",
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
"super-ctrl-b": "agent::ToggleBurnMode",
- "alt-enter": "agent::ContinueWithBurnMode"
+ "alt-enter": "agent::ContinueWithBurnMode",
+ "ctrl-y": "agent::AllowOnce",
+ "ctrl-alt-y": "agent::AllowAlways",
+ "ctrl-d": "agent::RejectOnce"
}
},
{
@@ -259,6 +263,7 @@
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
+ "ctrl-insert": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
@@ -327,17 +332,32 @@
}
},
{
- "context": "AcpThread > Editor",
+ "context": "AcpThread > ModeSelector",
+ "bindings": {
+ "ctrl-enter": "menu::Confirm"
+ }
+ },
+ {
+ "context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
- "up": "agent::PreviousHistoryMessage",
- "down": "agent::NextHistoryMessage",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
}
},
+ {
+ "context": "AcpThread > Editor && use_modifier_to_send",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "agent::Chat",
+ "shift-ctrl-r": "agent::OpenAgentDiff",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll",
+ "shift-tab": "agent::CycleModeSelector"
+ }
+ },
{
"context": "ThreadHistory",
"bindings": {
@@ -476,8 +496,8 @@
"alt-down": "editor::MoveLineDown",
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
- "alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
- "alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
+ "alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand selection
+ "alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
@@ -573,7 +593,7 @@
"ctrl-n": "workspace::NewFile",
"shift-new": "workspace::NewWindow",
"ctrl-shift-n": "workspace::NewWindow",
- "ctrl-`": "terminal_panel::ToggleFocus",
+ "ctrl-`": "terminal_panel::Toggle",
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
"alt-1": ["workspace::ActivatePane", 0],
"alt-2": ["workspace::ActivatePane", 1],
@@ -618,6 +638,7 @@
"alt-save": "workspace::SaveAll",
"ctrl-alt-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
+ "ctrl-k ctrl-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
@@ -848,7 +869,7 @@
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager",
- "ctrl-shift-enter": "project_panel::OpenWithSystem",
+ "ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
@@ -1018,6 +1039,13 @@
"tab": "channel_modal::ToggleMode"
}
},
+ {
+ "context": "ToolchainSelector",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-a": "toolchain::AddToolchain"
+ }
+ },
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"bindings": {
@@ -1187,9 +1215,16 @@
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage",
- "ctrl-escape": "onboarding::Finish",
- "alt-tab": "onboarding::SignIn",
+ "ctrl-enter": "onboarding::Finish",
+ "alt-shift-l": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
+ },
+ {
+ "context": "InvalidBuffer",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-enter": "workspace::OpenWithSystem"
+ }
}
]
@@ -70,9 +70,9 @@
"cmd-k q": "editor::Rewrap",
"cmd-backspace": "editor::DeleteToBeginningOfLine",
"cmd-delete": "editor::DeleteToEndOfLine",
- "alt-backspace": "editor::DeleteToPreviousWordStart",
- "ctrl-w": "editor::DeleteToPreviousWordStart",
- "alt-delete": "editor::DeleteToNextWordEnd",
+ "alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
+ "ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
+ "alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"cmd-x": "editor::Cut",
"cmd-c": "editor::Copy",
"cmd-v": "editor::Paste",
@@ -162,7 +162,7 @@
"cmd-alt-f": "buffer_search::DeployReplace",
"cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }],
"cmd-e": ["buffer_search::Deploy", { "focus": false }],
- "cmd->": "assistant::QuoteSelection",
+ "cmd->": "agent::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
"cmd-alt-e": "editor::SelectEnclosingSymbol",
"alt-enter": "editor::OpenSelectionsInMultibuffer"
@@ -218,7 +218,7 @@
}
},
{
- "context": "Editor && !agent_diff",
+ "context": "Editor && !agent_diff && !AgentPanel",
"use_key_equivalents": true,
"bindings": {
"cmd-alt-z": "git::Restore",
@@ -281,12 +281,15 @@
"cmd-shift-i": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
- "cmd->": "assistant::QuoteSelection",
+ "cmd->": "agent::QuoteSelection",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-ctrl-b": "agent::ToggleBurnMode",
"cmd-shift-enter": "agent::ContinueThread",
- "alt-enter": "agent::ContinueWithBurnMode"
+ "alt-enter": "agent::ContinueWithBurnMode",
+ "cmd-y": "agent::AllowOnce",
+ "cmd-alt-y": "agent::AllowAlways",
+ "cmd-d": "agent::RejectOnce"
}
},
{
@@ -379,15 +382,31 @@
}
},
{
- "context": "AcpThread > Editor",
+ "context": "AcpThread > ModeSelector",
+ "bindings": {
+ "cmd-enter": "menu::Confirm"
+ }
+ },
+ {
+ "context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
- "up": "agent::PreviousHistoryMessage",
- "down": "agent::NextHistoryMessage",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
- "cmd-shift-n": "agent::RejectAll"
+ "cmd-shift-n": "agent::RejectAll",
+ "shift-tab": "agent::CycleModeSelector"
+ }
+ },
+ {
+ "context": "AcpThread > Editor && use_modifier_to_send",
+ "use_key_equivalents": true,
+ "bindings": {
+ "cmd-enter": "agent::Chat",
+ "shift-ctrl-r": "agent::OpenAgentDiff",
+ "cmd-shift-y": "agent::KeepAll",
+ "cmd-shift-n": "agent::RejectAll",
+ "shift-tab": "agent::CycleModeSelector"
}
},
{
@@ -528,8 +547,10 @@
"alt-down": "editor::MoveLineDown",
"alt-shift-up": "editor::DuplicateLineUp",
"alt-shift-down": "editor::DuplicateLineDown",
- "ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
- "ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
+ "cmd-ctrl-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
+ "cmd-ctrl-right": "editor::SelectLargerSyntaxNode", // Expand selection
+ "cmd-ctrl-up": "editor::SelectPreviousSyntaxNode", // Move selection up
+ "cmd-ctrl-down": "editor::SelectNextSyntaxNode", // Move selection down
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word
@@ -641,7 +662,7 @@
"alt-shift-enter": "toast::RunAction",
"cmd-shift-s": "workspace::SaveAs",
"cmd-shift-n": "workspace::NewWindow",
- "ctrl-`": "terminal_panel::ToggleFocus",
+ "ctrl-`": "terminal_panel::Toggle",
"cmd-1": ["workspace::ActivatePane", 0],
"cmd-2": ["workspace::ActivatePane", 1],
"cmd-3": ["workspace::ActivatePane", 2],
@@ -682,6 +703,7 @@
"cmd-?": "agent::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
+ "cmd-k cmd-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"cmd-k cmd-left": "workspace::ActivatePaneLeft",
"cmd-k cmd-right": "workspace::ActivatePaneRight",
@@ -907,7 +929,7 @@
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFileManager",
- "ctrl-shift-enter": "project_panel::OpenWithSystem",
+ "ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
@@ -1086,6 +1108,13 @@
"tab": "channel_modal::ToggleMode"
}
},
+ {
+ "context": "ToolchainSelector",
+ "use_key_equivalents": true,
+ "bindings": {
+ "cmd-shift-a": "toolchain::AddToolchain"
+ }
+ },
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"use_key_equivalents": true,
@@ -1293,5 +1322,12 @@
"alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
+ },
+ {
+ "context": "InvalidBuffer",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-enter": "workspace::OpenWithSystem"
+ }
}
]
@@ -0,0 +1,1241 @@
+[
+ // Standard Windows bindings
+ {
+ "use_key_equivalents": true,
+ "bindings": {
+ "home": "menu::SelectFirst",
+ "shift-pageup": "menu::SelectFirst",
+ "pageup": "menu::SelectFirst",
+ "end": "menu::SelectLast",
+ "shift-pagedown": "menu::SelectLast",
+ "pagedown": "menu::SelectLast",
+ "ctrl-n": "menu::SelectNext",
+ "tab": "menu::SelectNext",
+ "down": "menu::SelectNext",
+ "ctrl-p": "menu::SelectPrevious",
+ "shift-tab": "menu::SelectPrevious",
+ "up": "menu::SelectPrevious",
+ "enter": "menu::Confirm",
+ "ctrl-enter": "menu::SecondaryConfirm",
+ "ctrl-escape": "menu::Cancel",
+ "ctrl-c": "menu::Cancel",
+ "escape": "menu::Cancel",
+ "shift-alt-enter": "menu::Restart",
+ "alt-enter": ["picker::ConfirmInput", { "secondary": false }],
+ "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
+ "ctrl-shift-w": "workspace::CloseWindow",
+ "shift-escape": "workspace::ToggleZoom",
+ "ctrl-o": "workspace::Open",
+ "ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
+ "ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
+ "ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }],
+ "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }],
+ "ctrl-,": "zed::OpenSettings",
+ "ctrl-q": "zed::Quit",
+ "f4": "debugger::Start",
+ "shift-f5": "debugger::Stop",
+ "ctrl-shift-f5": "debugger::RerunSession",
+ "f6": "debugger::Pause",
+ "f7": "debugger::StepOver",
+ "ctrl-f11": "debugger::StepInto",
+ "shift-f11": "debugger::StepOut",
+ "f11": "zed::ToggleFullScreen",
+ "ctrl-shift-i": "edit_prediction::ToggleMenu",
+ "shift-alt-l": "lsp_tool::ToggleMenu"
+ }
+ },
+ {
+ "context": "Picker || menu",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "menu::SelectPrevious",
+ "down": "menu::SelectNext"
+ }
+ },
+ {
+ "context": "Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "editor::Cancel",
+ "shift-backspace": "editor::Backspace",
+ "backspace": "editor::Backspace",
+ "delete": "editor::Delete",
+ "tab": "editor::Tab",
+ "shift-tab": "editor::Backtab",
+ "ctrl-k": "editor::CutToEndOfLine",
+ "ctrl-k ctrl-q": "editor::Rewrap",
+ "ctrl-k q": "editor::Rewrap",
+ "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
+ "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
+ "shift-delete": "editor::Cut",
+ "ctrl-x": "editor::Cut",
+ "ctrl-insert": "editor::Copy",
+ "ctrl-c": "editor::Copy",
+ "shift-insert": "editor::Paste",
+ "ctrl-v": "editor::Paste",
+ "ctrl-z": "editor::Undo",
+ "ctrl-y": "editor::Redo",
+ "ctrl-shift-z": "editor::Redo",
+ "up": "editor::MoveUp",
+ "ctrl-up": "editor::LineUp",
+ "ctrl-down": "editor::LineDown",
+ "pageup": "editor::MovePageUp",
+ "alt-pageup": "editor::PageUp",
+ "shift-pageup": "editor::SelectPageUp",
+ "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
+ "down": "editor::MoveDown",
+ "pagedown": "editor::MovePageDown",
+ "alt-pagedown": "editor::PageDown",
+ "shift-pagedown": "editor::SelectPageDown",
+ "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }],
+ "left": "editor::MoveLeft",
+ "right": "editor::MoveRight",
+ "ctrl-left": "editor::MoveToPreviousWordStart",
+ "ctrl-right": "editor::MoveToNextWordEnd",
+ "ctrl-home": "editor::MoveToBeginning",
+ "ctrl-end": "editor::MoveToEnd",
+ "shift-up": "editor::SelectUp",
+ "shift-down": "editor::SelectDown",
+ "shift-left": "editor::SelectLeft",
+ "shift-right": "editor::SelectRight",
+ "ctrl-shift-left": "editor::SelectToPreviousWordStart",
+ "ctrl-shift-right": "editor::SelectToNextWordEnd",
+ "ctrl-shift-home": "editor::SelectToBeginning",
+ "ctrl-shift-end": "editor::SelectToEnd",
+ "ctrl-a": "editor::SelectAll",
+ "ctrl-l": "editor::SelectLine",
+ "shift-alt-f": "editor::Format",
+ "shift-alt-o": "editor::OrganizeImports",
+ "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }],
+ "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }],
+ "ctrl-alt-space": "editor::ShowCharacterPalette",
+ "ctrl-;": "editor::ToggleLineNumbers",
+ "ctrl-'": "editor::ToggleSelectedDiffHunks",
+ "ctrl-\"": "editor::ExpandAllDiffHunks",
+ "ctrl-i": "editor::ShowSignatureHelp",
+ "alt-g b": "git::Blame",
+ "alt-g m": "git::OpenModifiedFiles",
+ "menu": "editor::OpenContextMenu",
+ "shift-f10": "editor::OpenContextMenu",
+ "ctrl-shift-e": "editor::ToggleEditPrediction",
+ "f9": "editor::ToggleBreakpoint",
+ "shift-f9": "editor::EditLogBreakpoint"
+ }
+ },
+ {
+ "context": "Editor && mode == full",
+ "use_key_equivalents": true,
+ "bindings": {
+ "shift-enter": "editor::Newline",
+ "enter": "editor::Newline",
+ "ctrl-enter": "editor::NewlineBelow",
+ "ctrl-shift-enter": "editor::NewlineAbove",
+ "ctrl-k ctrl-z": "editor::ToggleSoftWrap",
+ "ctrl-k z": "editor::ToggleSoftWrap",
+ "ctrl-f": "buffer_search::Deploy",
+ "ctrl-h": "buffer_search::DeployReplace",
+ "ctrl-shift-.": "assistant::QuoteSelection",
+ "ctrl-shift-,": "assistant::InsertIntoEditor",
+ "shift-alt-e": "editor::SelectEnclosingSymbol",
+ "ctrl-shift-backspace": "editor::GoToPreviousChange",
+ "ctrl-shift-alt-backspace": "editor::GoToNextChange",
+ "alt-enter": "editor::OpenSelectionsInMultibuffer"
+ }
+ },
+ {
+ "context": "Editor && mode == full && edit_prediction",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-]": "editor::NextEditPrediction",
+ "alt-[": "editor::PreviousEditPrediction"
+ }
+ },
+ {
+ "context": "Editor && !edit_prediction",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-\\": "editor::ShowEditPrediction"
+ }
+ },
+ {
+ "context": "Editor && mode == auto_height",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "editor::Newline",
+ "shift-enter": "editor::Newline",
+ "ctrl-shift-enter": "editor::NewlineBelow"
+ }
+ },
+ {
+ "context": "Markdown",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-c": "markdown::Copy"
+ }
+ },
+ {
+ "context": "Editor && jupyter && !ContextEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-enter": "repl::Run",
+ "ctrl-alt-enter": "repl::RunInPlace"
+ }
+ },
+ {
+ "context": "Editor && !agent_diff",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-k ctrl-r": "git::Restore",
+ "alt-y": "git::StageAndNext",
+ "shift-alt-y": "git::UnstageAndNext"
+ }
+ },
+ {
+ "context": "Editor && editor_agent_diff",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-y": "agent::Keep",
+ "ctrl-n": "agent::Reject",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll",
+ "ctrl-shift-r": "agent::OpenAgentDiff"
+ }
+ },
+ {
+ "context": "AgentDiff",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-y": "agent::Keep",
+ "ctrl-n": "agent::Reject",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll"
+ }
+ },
+ {
+ "context": "ContextEditor > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "assistant::Assist",
+ "ctrl-s": "workspace::Save",
+ "ctrl-shift-,": "assistant::InsertIntoEditor",
+ "shift-enter": "assistant::Split",
+ "ctrl-r": "assistant::CycleMessageRole",
+ "enter": "assistant::ConfirmCommand",
+ "alt-enter": "editor::Newline",
+ "ctrl-k c": "assistant::CopyCode",
+ "ctrl-g": "search::SelectNextMatch",
+ "ctrl-shift-g": "search::SelectPreviousMatch",
+ "ctrl-k l": "agent::OpenRulesLibrary"
+ }
+ },
+ {
+ "context": "AgentPanel",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-n": "agent::NewThread",
+ "shift-alt-n": "agent::NewTextThread",
+ "ctrl-shift-h": "agent::OpenHistory",
+ "shift-alt-c": "agent::OpenSettings",
+ "shift-alt-p": "agent::OpenRulesLibrary",
+ "ctrl-i": "agent::ToggleProfileSelector",
+ "shift-alt-/": "agent::ToggleModelSelector",
+ "ctrl-shift-a": "agent::ToggleContextPicker",
+ "ctrl-shift-j": "agent::ToggleNavigationMenu",
+ "ctrl-shift-i": "agent::ToggleOptionsMenu",
+ // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
+ "shift-alt-escape": "agent::ExpandMessageEditor",
+ "ctrl-shift-.": "assistant::QuoteSelection",
+ "shift-alt-e": "agent::RemoveAllContext",
+ "ctrl-shift-e": "project_panel::ToggleFocus",
+ "ctrl-shift-enter": "agent::ContinueThread",
+ "super-ctrl-b": "agent::ToggleBurnMode",
+ "alt-enter": "agent::ContinueWithBurnMode",
+ "ctrl-y": "agent::AllowOnce",
+ "ctrl-alt-y": "agent::AllowAlways",
+ "ctrl-d": "agent::RejectOnce"
+ }
+ },
+ {
+ "context": "AgentPanel > NavigationMenu",
+ "use_key_equivalents": true,
+ "bindings": {
+ "shift-backspace": "agent::DeleteRecentlyOpenThread"
+ }
+ },
+ {
+ "context": "AgentPanel > Markdown",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-c": "markdown::CopyAsMarkdown"
+ }
+ },
+ {
+ "context": "AgentPanel && prompt_editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-n": "agent::NewTextThread",
+ "ctrl-alt-t": "agent::NewThread"
+ }
+ },
+ {
+ "context": "AgentPanel && external_agent_thread",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-n": "agent::NewExternalAgentThread",
+ "ctrl-alt-t": "agent::NewThread"
+ }
+ },
+ {
+ "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "agent::Chat",
+ "ctrl-enter": "agent::ChatWithFollow",
+ "ctrl-i": "agent::ToggleProfileSelector",
+ "ctrl-shift-r": "agent::OpenAgentDiff",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll"
+ }
+ },
+ {
+ "context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "agent::Chat",
+ "enter": "editor::Newline",
+ "ctrl-i": "agent::ToggleProfileSelector",
+ "ctrl-shift-r": "agent::OpenAgentDiff",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll"
+ }
+ },
+ {
+ "context": "EditMessageEditor > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel",
+ "enter": "menu::Confirm",
+ "alt-enter": "editor::Newline"
+ }
+ },
+ {
+ "context": "AgentFeedbackMessageEditor > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel",
+ "enter": "menu::Confirm",
+ "alt-enter": "editor::Newline"
+ }
+ },
+ {
+ "context": "ContextStrip",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "agent::FocusUp",
+ "right": "agent::FocusRight",
+ "left": "agent::FocusLeft",
+ "down": "agent::FocusDown",
+ "backspace": "agent::RemoveFocusedContext",
+ "enter": "agent::AcceptSuggestedContext"
+ }
+ },
+ {
+ "context": "AcpThread > ModeSelector",
+ "bindings": {
+ "ctrl-enter": "menu::Confirm"
+ }
+ },
+ {
+ "context": "AcpThread > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "agent::Chat",
+ "ctrl-shift-r": "agent::OpenAgentDiff",
+ "ctrl-shift-y": "agent::KeepAll",
+ "ctrl-shift-n": "agent::RejectAll",
+ "shift-tab": "agent::CycleModeSelector"
+ }
+ },
+ {
+ "context": "ThreadHistory",
+ "use_key_equivalents": true,
+ "bindings": {
+ "backspace": "agent::RemoveSelectedThread"
+ }
+ },
+ {
+ "context": "PromptLibrary",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-n": "rules_library::NewRule",
+ "ctrl-shift-s": "rules_library::ToggleDefaultRule"
+ }
+ },
+ {
+ "context": "BufferSearchBar",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "buffer_search::Dismiss",
+ "tab": "buffer_search::FocusEditor",
+ "enter": "search::SelectNextMatch",
+ "shift-enter": "search::SelectPreviousMatch",
+ "alt-enter": "search::SelectAllMatches",
+ "ctrl-f": "search::FocusSearch",
+ "ctrl-h": "search::ToggleReplace",
+ "ctrl-l": "search::ToggleSelection"
+ }
+ },
+ {
+ "context": "BufferSearchBar && in_replace > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "search::ReplaceNext",
+ "ctrl-enter": "search::ReplaceAll"
+ }
+ },
+ {
+ "context": "BufferSearchBar && !in_replace > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "search::PreviousHistoryQuery",
+ "down": "search::NextHistoryQuery"
+ }
+ },
+ {
+ "context": "ProjectSearchBar",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "project_search::ToggleFocus",
+ "ctrl-shift-f": "search::FocusSearch",
+ "ctrl-shift-h": "search::ToggleReplace",
+ "alt-r": "search::ToggleRegex" // vscode
+ }
+ },
+ {
+ "context": "ProjectSearchBar > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "search::PreviousHistoryQuery",
+ "down": "search::NextHistoryQuery"
+ }
+ },
+ {
+ "context": "ProjectSearchBar && in_replace > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "search::ReplaceNext",
+ "ctrl-alt-enter": "search::ReplaceAll"
+ }
+ },
+ {
+ "context": "ProjectSearchView",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "project_search::ToggleFocus",
+ "ctrl-shift-h": "search::ToggleReplace",
+ "alt-r": "search::ToggleRegex" // vscode
+ }
+ },
+ {
+ "context": "Pane",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-1": ["pane::ActivateItem", 0],
+ "alt-2": ["pane::ActivateItem", 1],
+ "alt-3": ["pane::ActivateItem", 2],
+ "alt-4": ["pane::ActivateItem", 3],
+ "alt-5": ["pane::ActivateItem", 4],
+ "alt-6": ["pane::ActivateItem", 5],
+ "alt-7": ["pane::ActivateItem", 6],
+ "alt-8": ["pane::ActivateItem", 7],
+ "alt-9": ["pane::ActivateItem", 8],
+ "alt-0": "pane::ActivateLastItem",
+ "ctrl-pageup": "pane::ActivatePreviousItem",
+ "ctrl-pagedown": "pane::ActivateNextItem",
+ "ctrl-shift-pageup": "pane::SwapItemLeft",
+ "ctrl-shift-pagedown": "pane::SwapItemRight",
+ "ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }],
+ "ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }],
+ "ctrl-shift-alt-t": ["pane::CloseOtherItems", { "close_pinned": false }],
+ "ctrl-shift-alt-w": "workspace::CloseInactiveTabsAndPanes",
+ "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }],
+ "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }],
+ "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }],
+ "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }],
+ "ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes",
+ "back": "pane::GoBack",
+ "alt--": "pane::GoBack",
+ "alt-=": "pane::GoForward",
+ "forward": "pane::GoForward",
+ "f3": "search::SelectNextMatch",
+ "shift-f3": "search::SelectPreviousMatch",
+ "ctrl-shift-f": "project_search::ToggleFocus",
+ "shift-alt-h": "search::ToggleReplace",
+ "alt-l": "search::ToggleSelection",
+ "alt-enter": "search::SelectAllMatches",
+ "alt-c": "search::ToggleCaseSensitive",
+ "alt-w": "search::ToggleWholeWord",
+ "alt-f": "project_search::ToggleFilters",
+ "alt-r": "search::ToggleRegex",
+ // "ctrl-shift-alt-x": "search::ToggleRegex",
+ "ctrl-k shift-enter": "pane::TogglePinTab"
+ }
+ },
+ // Bindings from VS Code
+ {
+ "context": "Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-[": "editor::Outdent",
+ "ctrl-]": "editor::Indent",
+ "ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above
+ "ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below
+ "ctrl-shift-k": "editor::DeleteLine",
+ "alt-up": "editor::MoveLineUp",
+ "alt-down": "editor::MoveLineDown",
+ "shift-alt-up": "editor::DuplicateLineUp",
+ "shift-alt-down": "editor::DuplicateLineDown",
+ "shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand selection
+ "shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
+ "ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
+ "ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
+ "ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
+ "ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch
+ "ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
+ "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
+ "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
+ "ctrl-k ctrl-i": "editor::Hover",
+ "ctrl-k ctrl-b": "editor::BlameHover",
+ "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
+ "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
+ "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
+ "f2": "editor::Rename",
+ "f12": "editor::GoToDefinition",
+ "alt-f12": "editor::GoToDefinitionSplit",
+ "ctrl-shift-f10": "editor::GoToDefinitionSplit",
+ "ctrl-f12": "editor::GoToImplementation",
+ "shift-f12": "editor::GoToTypeDefinition",
+ "ctrl-alt-f12": "editor::GoToTypeDefinitionSplit",
+ "shift-alt-f12": "editor::FindAllReferences",
+ "ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
+ "ctrl-shift-\\": "editor::MoveToEnclosingBracket",
+ "ctrl-shift-[": "editor::Fold",
+ "ctrl-shift-]": "editor::UnfoldLines",
+ "ctrl-k ctrl-l": "editor::ToggleFold",
+ "ctrl-k ctrl-[": "editor::FoldRecursive",
+ "ctrl-k ctrl-]": "editor::UnfoldRecursive",
+ "ctrl-k ctrl-1": ["editor::FoldAtLevel", 1],
+ "ctrl-k ctrl-2": ["editor::FoldAtLevel", 2],
+ "ctrl-k ctrl-3": ["editor::FoldAtLevel", 3],
+ "ctrl-k ctrl-4": ["editor::FoldAtLevel", 4],
+ "ctrl-k ctrl-5": ["editor::FoldAtLevel", 5],
+ "ctrl-k ctrl-6": ["editor::FoldAtLevel", 6],
+ "ctrl-k ctrl-7": ["editor::FoldAtLevel", 7],
+ "ctrl-k ctrl-8": ["editor::FoldAtLevel", 8],
+ "ctrl-k ctrl-9": ["editor::FoldAtLevel", 9],
+ "ctrl-k ctrl-0": "editor::FoldAll",
+ "ctrl-k ctrl-j": "editor::UnfoldAll",
+ "ctrl-space": "editor::ShowCompletions",
+ "ctrl-shift-space": "editor::ShowWordCompletions",
+ "ctrl-.": "editor::ToggleCodeActions",
+ "ctrl-k r": "editor::RevealInFileManager",
+ "ctrl-k p": "editor::CopyPath",
+ "ctrl-\\": "pane::SplitRight",
+ "ctrl-shift-alt-c": "editor::DisplayCursorNames",
+ "alt-.": "editor::GoToHunk",
+ "alt-,": "editor::GoToPreviousHunk"
+ }
+ },
+ {
+ "context": "Editor && extension == md",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-k v": "markdown::OpenPreviewToTheSide",
+ "ctrl-shift-v": "markdown::OpenPreview"
+ }
+ },
+ {
+ "context": "Editor && extension == svg",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-k v": "svg::OpenPreviewToTheSide",
+ "ctrl-shift-v": "svg::OpenPreview"
+ }
+ },
+ {
+ "context": "Editor && mode == full",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-o": "outline::Toggle",
+ "ctrl-g": "go_to_line::Toggle"
+ }
+ },
+ {
+ "context": "Workspace",
+ "use_key_equivalents": true,
+ "bindings": {
+ // Change the default action on `menu::Confirm` by setting the parameter
+ // "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }],
+ "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
+ // Change to open path modal for existing remote connection by setting the parameter
+ // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
+ "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
+ "shift-alt-b": "branches::OpenRecent",
+ "shift-alt-enter": "toast::RunAction",
+ "ctrl-shift-`": "workspace::NewTerminal",
+ "ctrl-s": "workspace::Save",
+ "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat",
+ "ctrl-shift-s": "workspace::SaveAs",
+ "ctrl-n": "workspace::NewFile",
+ "ctrl-shift-n": "workspace::NewWindow",
+ "ctrl-`": "terminal_panel::Toggle",
+ "f10": ["app_menu::OpenApplicationMenu", "Zed"],
+ "alt-1": ["workspace::ActivatePane", 0],
+ "alt-2": ["workspace::ActivatePane", 1],
+ "alt-3": ["workspace::ActivatePane", 2],
+ "alt-4": ["workspace::ActivatePane", 3],
+ "alt-5": ["workspace::ActivatePane", 4],
+ "alt-6": ["workspace::ActivatePane", 5],
+ "alt-7": ["workspace::ActivatePane", 6],
+ "alt-8": ["workspace::ActivatePane", 7],
+ "alt-9": ["workspace::ActivatePane", 8],
+ "ctrl-alt-b": "workspace::ToggleRightDock",
+ "ctrl-b": "workspace::ToggleLeftDock",
+ "ctrl-j": "workspace::ToggleBottomDock",
+ "ctrl-shift-y": "workspace::CloseAllDocks",
+ "alt-r": "workspace::ResetActiveDockSize",
+ // For 0px parameter, uses UI font size value.
+ "shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }],
+ "shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }],
+ "shift-alt-0": "workspace::ResetOpenDocksSize",
+ "ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
+ "ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
+ "ctrl-shift-f": "pane::DeploySearch",
+ "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
+ "ctrl-shift-t": "pane::ReopenClosedItem",
+ "ctrl-k ctrl-s": "zed::OpenKeymapEditor",
+ "ctrl-k ctrl-t": "theme_selector::Toggle",
+ "ctrl-alt-super-p": "settings_profile_selector::Toggle",
+ "ctrl-t": "project_symbols::Toggle",
+ "ctrl-p": "file_finder::Toggle",
+ "ctrl-tab": "tab_switcher::Toggle",
+ "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
+ "ctrl-e": "file_finder::Toggle",
+ "f1": "command_palette::Toggle",
+ "ctrl-shift-p": "command_palette::Toggle",
+ "ctrl-shift-m": "diagnostics::Deploy",
+ "ctrl-shift-e": "project_panel::ToggleFocus",
+ "ctrl-shift-b": "outline_panel::ToggleFocus",
+ "ctrl-shift-g": "git_panel::ToggleFocus",
+ "ctrl-shift-d": "debug_panel::ToggleFocus",
+ "ctrl-shift-/": "agent::ToggleFocus",
+ "ctrl-k s": "workspace::SaveAll",
+ "ctrl-k m": "language_selector::Toggle",
+ "ctrl-m ctrl-m": "toolchain::AddToolchain",
+ "escape": "workspace::Unfollow",
+ "ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
+ "ctrl-k ctrl-right": "workspace::ActivatePaneRight",
+ "ctrl-k ctrl-up": "workspace::ActivatePaneUp",
+ "ctrl-k ctrl-down": "workspace::ActivatePaneDown",
+ "ctrl-k shift-left": "workspace::SwapPaneLeft",
+ "ctrl-k shift-right": "workspace::SwapPaneRight",
+ "ctrl-k shift-up": "workspace::SwapPaneUp",
+ "ctrl-k shift-down": "workspace::SwapPaneDown",
+ "ctrl-shift-x": "zed::Extensions",
+ "ctrl-shift-r": "task::Rerun",
+ "alt-t": "task::Rerun",
+ "shift-alt-t": "task::Spawn",
+ "shift-alt-r": ["task::Spawn", { "reveal_target": "center" }],
+ // also possible to spawn tasks by name:
+ // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
+ // or by tag:
+ // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }],
+ "f5": "debugger::Rerun",
+ "ctrl-f4": "workspace::CloseActiveDock",
+ "ctrl-w": "workspace::CloseActiveDock"
+ }
+ },
+ {
+ "context": "Workspace && debugger_running",
+ "use_key_equivalents": true,
+ "bindings": {
+ "f5": "zed::NoAction"
+ }
+ },
+ {
+ "context": "Workspace && debugger_stopped",
+ "use_key_equivalents": true,
+ "bindings": {
+ "f5": "debugger::Continue"
+ }
+ },
+ {
+ "context": "ApplicationMenu",
+ "use_key_equivalents": true,
+ "bindings": {
+ "f10": "menu::Cancel",
+ "left": "app_menu::ActivateMenuLeft",
+ "right": "app_menu::ActivateMenuRight"
+ }
+ },
+ // Bindings from Sublime Text
+ {
+ "context": "Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-u": "editor::UndoSelection",
+ "ctrl-shift-u": "editor::RedoSelection",
+ "ctrl-shift-j": "editor::JoinLines",
+ "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
+ "shift-alt-h": "editor::DeleteToPreviousSubwordStart",
+ "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
+ "shift-alt-d": "editor::DeleteToNextSubwordEnd",
+ "ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
+ "ctrl-alt-right": "editor::MoveToNextSubwordEnd",
+ "ctrl-shift-alt-left": "editor::SelectToPreviousSubwordStart",
+ "ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd"
+ }
+ },
+ // Bindings from Atom
+ {
+ "context": "Pane",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-k up": "pane::SplitUp",
+ "ctrl-k down": "pane::SplitDown",
+ "ctrl-k left": "pane::SplitLeft",
+ "ctrl-k right": "pane::SplitRight"
+ }
+ },
+ // Bindings that should be unified with bindings for more general actions
+ {
+ "context": "Editor && renaming",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "editor::ConfirmRename"
+ }
+ },
+ {
+ "context": "Editor && showing_completions",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "editor::ConfirmCompletion",
+ "shift-enter": "editor::ConfirmCompletionReplace",
+ "tab": "editor::ComposeCompletion"
+ }
+ },
+ // Bindings for accepting edit predictions
+ //
+ // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is
+ // because alt-tab may not be available, as it is often used for window switching.
+ {
+ "context": "Editor && edit_prediction",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-tab": "editor::AcceptEditPrediction",
+ "alt-l": "editor::AcceptEditPrediction",
+ "tab": "editor::AcceptEditPrediction",
+ "alt-right": "editor::AcceptPartialEditPrediction"
+ }
+ },
+ {
+ "context": "Editor && edit_prediction_conflict",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-tab": "editor::AcceptEditPrediction",
+ "alt-l": "editor::AcceptEditPrediction",
+ "alt-right": "editor::AcceptPartialEditPrediction"
+ }
+ },
+ {
+ "context": "Editor && showing_code_actions",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "editor::ConfirmCodeAction"
+ }
+ },
+ {
+ "context": "Editor && (showing_code_actions || showing_completions)",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-p": "editor::ContextMenuPrevious",
+ "up": "editor::ContextMenuPrevious",
+ "ctrl-n": "editor::ContextMenuNext",
+ "down": "editor::ContextMenuNext",
+ "pageup": "editor::ContextMenuFirst",
+ "pagedown": "editor::ContextMenuLast"
+ }
+ },
+ {
+ "context": "Editor && showing_signature_help && !showing_completions",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "editor::SignatureHelpPrevious",
+ "down": "editor::SignatureHelpNext"
+ }
+ },
+ // Custom bindings
+ {
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-alt-f": "workspace::FollowNextCollaborator",
+ // Only available in debug builds: opens an element inspector for development.
+ "shift-alt-i": "dev::ToggleInspector"
+ }
+ },
+ {
+ "context": "!Terminal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-c": "collab_panel::ToggleFocus"
+ }
+ },
+ {
+ "context": "!ContextEditor > Editor && mode == full",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-enter": "editor::OpenExcerpts",
+ "shift-enter": "editor::ExpandExcerpts",
+ "ctrl-alt-enter": "editor::OpenExcerptsSplit",
+ "ctrl-shift-e": "pane::RevealInProjectPanel",
+ "ctrl-f8": "editor::GoToHunk",
+ "ctrl-shift-f8": "editor::GoToPreviousHunk",
+ "ctrl-enter": "assistant::InlineAssist",
+ "ctrl-shift-;": "editor::ToggleInlayHints"
+ }
+ },
+ {
+ "context": "PromptEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-[": "agent::CyclePreviousInlineAssist",
+ "ctrl-]": "agent::CycleNextInlineAssist",
+ "shift-alt-e": "agent::RemoveAllContext"
+ }
+ },
+ {
+ "context": "Prompt",
+ "use_key_equivalents": true,
+ "bindings": {
+ "left": "menu::SelectPrevious",
+ "right": "menu::SelectNext",
+ "h": "menu::SelectPrevious",
+ "l": "menu::SelectNext"
+ }
+ },
+ {
+ "context": "ProjectSearchBar && !in_replace",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "project_search::SearchInNew"
+ }
+ },
+ {
+ "context": "OutlinePanel && not_editing",
+ "use_key_equivalents": true,
+ "bindings": {
+ "left": "outline_panel::CollapseSelectedEntry",
+ "right": "outline_panel::ExpandSelectedEntry",
+ "shift-alt-c": "outline_panel::CopyPath",
+ "ctrl-shift-alt-c": "workspace::CopyRelativePath",
+ "ctrl-alt-r": "outline_panel::RevealInFileManager",
+ "space": "outline_panel::OpenSelectedEntry",
+ "shift-down": "menu::SelectNext",
+ "shift-up": "menu::SelectPrevious",
+ "alt-enter": "editor::OpenExcerpts",
+ "ctrl-alt-enter": "editor::OpenExcerptsSplit"
+ }
+ },
+ {
+ "context": "ProjectPanel",
+ "use_key_equivalents": true,
+ "bindings": {
+ "left": "project_panel::CollapseSelectedEntry",
+ "right": "project_panel::ExpandSelectedEntry",
+ "ctrl-n": "project_panel::NewFile",
+ "alt-n": "project_panel::NewDirectory",
+ "ctrl-x": "project_panel::Cut",
+ "ctrl-insert": "project_panel::Copy",
+ "ctrl-c": "project_panel::Copy",
+ "shift-insert": "project_panel::Paste",
+ "ctrl-v": "project_panel::Paste",
+ "shift-alt-c": "project_panel::CopyPath",
+ "ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
+ "enter": "project_panel::Rename",
+ "f2": "project_panel::Rename",
+ "backspace": ["project_panel::Trash", { "skip_prompt": false }],
+ "delete": ["project_panel::Trash", { "skip_prompt": false }],
+ "shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
+ "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
+ "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
+ "ctrl-alt-r": "project_panel::RevealInFileManager",
+ "ctrl-shift-enter": "project_panel::OpenWithSystem",
+ "alt-d": "project_panel::CompareMarkedFiles",
+ "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory",
+ "shift-down": "menu::SelectNext",
+ "shift-up": "menu::SelectPrevious",
+ "escape": "menu::Cancel"
+ }
+ },
+ {
+ "context": "ProjectPanel && not_editing",
+ "use_key_equivalents": true,
+ "bindings": {
+ "space": "project_panel::Open"
+ }
+ },
+ {
+ "context": "GitPanel && ChangesList",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "menu::SelectPrevious",
+ "down": "menu::SelectNext",
+ "enter": "menu::Confirm",
+ "alt-y": "git::StageFile",
+ "shift-alt-y": "git::UnstageFile",
+ "space": "git::ToggleStaged",
+ "shift-space": "git::StageRange",
+ "tab": "git_panel::FocusEditor",
+ "shift-tab": "git_panel::FocusEditor",
+ "escape": "git_panel::ToggleFocus",
+ "alt-enter": "menu::SecondaryConfirm",
+ "delete": ["git::RestoreFile", { "skip_prompt": false }],
+ "backspace": ["git::RestoreFile", { "skip_prompt": false }],
+ "shift-delete": ["git::RestoreFile", { "skip_prompt": false }],
+ "ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }],
+ "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
+ }
+ },
+ {
+ "context": "GitPanel && CommitEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "git::Cancel"
+ }
+ },
+ {
+ "context": "GitCommit > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel",
+ "enter": "editor::Newline",
+ "ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
+ "alt-l": "git::GenerateCommitMessage"
+ }
+ },
+ {
+ "context": "GitPanel",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-g ctrl-g": "git::Fetch",
+ "ctrl-g up": "git::Push",
+ "ctrl-g down": "git::Pull",
+ "ctrl-g shift-up": "git::ForcePush",
+ "ctrl-g d": "git::Diff",
+ "ctrl-g backspace": "git::RestoreTrackedFiles",
+ "ctrl-g shift-backspace": "git::TrashUntrackedFiles",
+ "ctrl-space": "git::StageAll",
+ "ctrl-shift-space": "git::UnstageAll",
+ "ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend"
+ }
+ },
+ {
+ "context": "GitDiff > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
+ "ctrl-space": "git::StageAll",
+ "ctrl-shift-space": "git::UnstageAll"
+ }
+ },
+ {
+ "context": "AskPass > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "menu::Confirm"
+ }
+ },
+ {
+ "context": "CommitEditor > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "git_panel::FocusChanges",
+ "tab": "git_panel::FocusChanges",
+ "shift-tab": "git_panel::FocusChanges",
+ "enter": "editor::Newline",
+ "ctrl-enter": "git::Commit",
+ "ctrl-shift-enter": "git::Amend",
+ "alt-up": "git_panel::FocusChanges",
+ "alt-l": "git::GenerateCommitMessage"
+ }
+ },
+ {
+ "context": "DebugPanel",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-t": "debugger::ToggleThreadPicker",
+ "ctrl-i": "debugger::ToggleSessionPicker",
+ "shift-alt-escape": "debugger::ToggleExpandItem"
+ }
+ },
+ {
+ "context": "VariableList",
+ "use_key_equivalents": true,
+ "bindings": {
+ "left": "variable_list::CollapseSelectedEntry",
+ "right": "variable_list::ExpandSelectedEntry",
+ "enter": "variable_list::EditVariable",
+ "ctrl-c": "variable_list::CopyVariableValue",
+ "ctrl-alt-c": "variable_list::CopyVariableName",
+ "delete": "variable_list::RemoveWatch",
+ "backspace": "variable_list::RemoveWatch",
+ "alt-enter": "variable_list::AddWatch"
+ }
+ },
+ {
+ "context": "BreakpointList",
+ "use_key_equivalents": true,
+ "bindings": {
+ "space": "debugger::ToggleEnableBreakpoint",
+ "backspace": "debugger::UnsetBreakpoint",
+ "left": "debugger::PreviousBreakpointProperty",
+ "right": "debugger::NextBreakpointProperty"
+ }
+ },
+ {
+ "context": "CollabPanel && not_editing",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-backspace": "collab_panel::Remove",
+ "space": "menu::Confirm"
+ }
+ },
+ {
+ "context": "CollabPanel",
+ "use_key_equivalents": true,
+ "bindings": {
+ "alt-up": "collab_panel::MoveChannelUp",
+ "alt-down": "collab_panel::MoveChannelDown"
+ }
+ },
+ {
+ "context": "(CollabPanel && editing) > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "space": "collab_panel::InsertSpace"
+ }
+ },
+ {
+ "context": "ChannelModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "tab": "channel_modal::ToggleMode"
+ }
+ },
+ {
+ "context": "Picker > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel",
+ "up": "menu::SelectPrevious",
+ "down": "menu::SelectNext",
+ "tab": "picker::ConfirmCompletion",
+ "alt-enter": ["picker::ConfirmInput", { "secondary": false }]
+ }
+ },
+ {
+ "context": "ChannelModal > Picker > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "tab": "channel_modal::ToggleMode"
+ }
+ },
+ {
+ "context": "ToolchainSelector",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-a": "toolchain::AddToolchain"
+ }
+ },
+ {
+ "context": "FileFinder || (FileFinder > Picker > Editor)",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-p": "file_finder::Toggle",
+ "ctrl-shift-a": "file_finder::ToggleSplitMenu",
+ "ctrl-shift-i": "file_finder::ToggleFilterMenu"
+ }
+ },
+ {
+ "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-p": "file_finder::SelectPrevious",
+ "ctrl-j": "pane::SplitDown",
+ "ctrl-k": "pane::SplitUp",
+ "ctrl-h": "pane::SplitLeft",
+ "ctrl-l": "pane::SplitRight"
+ }
+ },
+ {
+ "context": "TabSwitcher",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-shift-tab": "menu::SelectPrevious",
+ "ctrl-up": "menu::SelectPrevious",
+ "ctrl-down": "menu::SelectNext",
+ "ctrl-backspace": "tab_switcher::CloseSelectedItem"
+ }
+ },
+ {
+ "context": "Terminal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-alt-space": "terminal::ShowCharacterPalette",
+ "ctrl-insert": "terminal::Copy",
+ "ctrl-shift-c": "terminal::Copy",
+ "shift-insert": "terminal::Paste",
+ "ctrl-shift-v": "terminal::Paste",
+ "ctrl-enter": "assistant::InlineAssist",
+ "alt-b": ["terminal::SendText", "\u001bb"],
+ "alt-f": ["terminal::SendText", "\u001bf"],
+ "alt-.": ["terminal::SendText", "\u001b."],
+ "ctrl-delete": ["terminal::SendText", "\u001bd"],
+ // Overrides for conflicting keybindings
+ "ctrl-b": ["terminal::SendKeystroke", "ctrl-b"],
+ "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
+ "ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
+ "ctrl-o": ["terminal::SendKeystroke", "ctrl-o"],
+ "ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
+ "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
+ "ctrl-shift-a": "editor::SelectAll",
+ "ctrl-shift-f": "buffer_search::Deploy",
+ "ctrl-shift-l": "terminal::Clear",
+ "ctrl-shift-w": "pane::CloseActiveItem",
+ "up": ["terminal::SendKeystroke", "up"],
+ "pageup": ["terminal::SendKeystroke", "pageup"],
+ "down": ["terminal::SendKeystroke", "down"],
+ "pagedown": ["terminal::SendKeystroke", "pagedown"],
+ "escape": ["terminal::SendKeystroke", "escape"],
+ "enter": ["terminal::SendKeystroke", "enter"],
+ "shift-pageup": "terminal::ScrollPageUp",
+ "shift-pagedown": "terminal::ScrollPageDown",
+ "shift-up": "terminal::ScrollLineUp",
+ "shift-down": "terminal::ScrollLineDown",
+ "shift-home": "terminal::ScrollToTop",
+ "shift-end": "terminal::ScrollToBottom",
+ "ctrl-shift-space": "terminal::ToggleViMode",
+ "ctrl-shift-r": "terminal::RerunTask",
+ "ctrl-alt-r": "terminal::RerunTask",
+ "alt-t": "terminal::RerunTask"
+ }
+ },
+ {
+ "context": "ZedPredictModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel"
+ }
+ },
+ {
+ "context": "ConfigureContextServerModal > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel",
+ "enter": "editor::Newline",
+ "ctrl-enter": "menu::Confirm"
+ }
+ },
+ {
+ "context": "OnboardingAiConfigurationModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel"
+ }
+ },
+ {
+ "context": "Diagnostics",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
+ }
+ },
+ {
+ "context": "DebugConsole > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "menu::Confirm",
+ "alt-enter": "console::WatchExpression"
+ }
+ },
+ {
+ "context": "RunModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-tab": "pane::ActivateNextItem",
+ "ctrl-shift-tab": "pane::ActivatePreviousItem"
+ }
+ },
+ {
+ "context": "MarkdownPreview",
+ "use_key_equivalents": true,
+ "bindings": {
+ "pageup": "markdown::MovePageUp",
+ "pagedown": "markdown::MovePageDown"
+ }
+ },
+ {
+ "context": "KeymapEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-f": "search::FocusSearch",
+ "alt-f": "keymap_editor::ToggleKeystrokeSearch",
+ "alt-c": "keymap_editor::ToggleConflictFilter",
+ "enter": "keymap_editor::EditBinding",
+ "alt-enter": "keymap_editor::CreateBinding",
+ "ctrl-c": "keymap_editor::CopyAction",
+ "ctrl-shift-c": "keymap_editor::CopyContext",
+ "ctrl-t": "keymap_editor::ShowMatchingKeybinds"
+ }
+ },
+ {
+ "context": "KeystrokeInput",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "keystroke_input::StartRecording",
+ "escape escape escape": "keystroke_input::StopRecording",
+ "delete": "keystroke_input::ClearKeystrokes"
+ }
+ },
+ {
+ "context": "KeybindEditorModal",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-enter": "menu::Confirm",
+ "escape": "menu::Cancel"
+ }
+ },
+ {
+ "context": "KeybindEditorModal > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "up": "menu::SelectPrevious",
+ "down": "menu::SelectNext"
+ }
+ },
+ {
+ "context": "Onboarding",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-1": "onboarding::ActivateBasicsPage",
+ "ctrl-2": "onboarding::ActivateEditingPage",
+ "ctrl-3": "onboarding::ActivateAISetupPage",
+ "ctrl-escape": "onboarding::Finish",
+ "alt-tab": "onboarding::SignIn",
+ "shift-alt-a": "onboarding::OpenAccount"
+ }
+ }
+]
@@ -17,8 +17,8 @@
"bindings": {
"ctrl-i": "agent::ToggleFocus",
"ctrl-shift-i": "agent::ToggleFocus",
- "ctrl-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode
- "ctrl-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode
+ "ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
+ "ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
"ctrl-k": "assistant::InlineAssist",
"ctrl-shift-k": "assistant::InsertIntoEditor"
}
@@ -38,10 +38,11 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
+ "alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
- "alt-d": "editor::DeleteToNextWordEnd", // kill-word
+ "alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
"ctrl-k": "editor::KillRingCut", // kill-line
"ctrl-w": "editor::Cut", // kill-region
"alt-w": "editor::Copy", // kill-ring-save
@@ -125,7 +125,7 @@
{
"context": "Workspace || Editor",
"bindings": {
- "alt-f12": "terminal_panel::ToggleFocus",
+ "alt-f12": "terminal_panel::Toggle",
"ctrl-shift-k": "git::Push"
}
},
@@ -50,8 +50,8 @@
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
- "ctrl-backspace": "editor::DeleteToPreviousWordStart",
- "ctrl-delete": "editor::DeleteToNextWordEnd",
+ "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
+ "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-right": "editor::MoveToNextSubwordEnd",
"alt-left": "editor::MoveToPreviousSubwordStart",
"alt-shift-right": "editor::SelectToNextSubwordEnd",
@@ -17,8 +17,8 @@
"bindings": {
"cmd-i": "agent::ToggleFocus",
"cmd-shift-i": "agent::ToggleFocus",
- "cmd-shift-l": "assistant::QuoteSelection", // In cursor uses "Ask" mode
- "cmd-l": "assistant::QuoteSelection", // In cursor uses "Agent" mode
+ "cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode
+ "cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode
"cmd-k": "assistant::InlineAssist",
"cmd-shift-k": "assistant::InsertIntoEditor"
}
@@ -38,10 +38,11 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
+ "alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
- "alt-d": "editor::DeleteToNextWordEnd", // kill-word
+ "alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
"ctrl-k": "editor::KillRingCut", // kill-line
"ctrl-w": "editor::Cut", // kill-region
"alt-w": "editor::Copy", // kill-ring-save
@@ -127,7 +127,7 @@
{
"context": "Workspace || Editor",
"bindings": {
- "alt-f12": "terminal_panel::ToggleFocus",
+ "alt-f12": "terminal_panel::Toggle",
"cmd-shift-k": "git::Push"
}
},
@@ -52,8 +52,8 @@
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
- "ctrl-backspace": "editor::DeleteToPreviousWordStart",
- "ctrl-delete": "editor::DeleteToNextWordEnd",
+ "ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
+ "ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-right": "editor::MoveToNextSubwordEnd",
"ctrl-left": "editor::MoveToPreviousSubwordStart",
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
@@ -21,10 +21,10 @@
{
"context": "Editor",
"bindings": {
- "alt-backspace": "editor::DeleteToPreviousWordStart",
- "alt-shift-backspace": "editor::DeleteToNextWordEnd",
- "alt-delete": "editor::DeleteToNextWordEnd",
- "alt-shift-delete": "editor::DeleteToNextWordEnd",
+ "alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
+ "alt-shift-backspace": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
+ "alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
+ "alt-shift-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-delete": "editor::DeleteToNextSubwordEnd",
"alt-left": ["editor::MoveToPreviousWordStart", { "stop_at_soft_wraps": true }],
@@ -32,32 +32,6 @@
"(": "vim::SentenceBackward",
")": "vim::SentenceForward",
"|": "vim::GoToColumn",
- "] ]": "vim::NextSectionStart",
- "] [": "vim::NextSectionEnd",
- "[ [": "vim::PreviousSectionStart",
- "[ ]": "vim::PreviousSectionEnd",
- "] m": "vim::NextMethodStart",
- "] shift-m": "vim::NextMethodEnd",
- "[ m": "vim::PreviousMethodStart",
- "[ shift-m": "vim::PreviousMethodEnd",
- "[ *": "vim::PreviousComment",
- "[ /": "vim::PreviousComment",
- "] *": "vim::NextComment",
- "] /": "vim::NextComment",
- "[ -": "vim::PreviousLesserIndent",
- "[ +": "vim::PreviousGreaterIndent",
- "[ =": "vim::PreviousSameIndent",
- "] -": "vim::NextLesserIndent",
- "] +": "vim::NextGreaterIndent",
- "] =": "vim::NextSameIndent",
- "] b": "pane::ActivateNextItem",
- "[ b": "pane::ActivatePreviousItem",
- "] shift-b": "pane::ActivateLastItem",
- "[ shift-b": ["pane::ActivateItem", 0],
- "] space": "vim::InsertEmptyLineBelow",
- "[ space": "vim::InsertEmptyLineAbove",
- "[ e": "editor::MoveLineUp",
- "] e": "editor::MoveLineDown",
// Word motions
"w": "vim::NextWordStart",
@@ -81,10 +55,6 @@
"n": "vim::MoveToNextMatch",
"shift-n": "vim::MoveToPreviousMatch",
"%": "vim::Matching",
- "] }": ["vim::UnmatchedForward", { "char": "}" }],
- "[ {": ["vim::UnmatchedBackward", { "char": "{" }],
- "] )": ["vim::UnmatchedForward", { "char": ")" }],
- "[ (": ["vim::UnmatchedBackward", { "char": "(" }],
"f": ["vim::PushFindForward", { "before": false, "multiline": false }],
"t": ["vim::PushFindForward", { "before": true, "multiline": false }],
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": false }],
@@ -217,6 +187,46 @@
".": "vim::Repeat"
}
},
+ {
+ "context": "vim_mode == normal || vim_mode == visual || vim_mode == operator",
+ "bindings": {
+ "] ]": "vim::NextSectionStart",
+ "] [": "vim::NextSectionEnd",
+ "[ [": "vim::PreviousSectionStart",
+ "[ ]": "vim::PreviousSectionEnd",
+ "] m": "vim::NextMethodStart",
+ "] shift-m": "vim::NextMethodEnd",
+ "[ m": "vim::PreviousMethodStart",
+ "[ shift-m": "vim::PreviousMethodEnd",
+ "[ *": "vim::PreviousComment",
+ "[ /": "vim::PreviousComment",
+ "] *": "vim::NextComment",
+ "] /": "vim::NextComment",
+ "[ -": "vim::PreviousLesserIndent",
+ "[ +": "vim::PreviousGreaterIndent",
+ "[ =": "vim::PreviousSameIndent",
+ "] -": "vim::NextLesserIndent",
+ "] +": "vim::NextGreaterIndent",
+ "] =": "vim::NextSameIndent",
+ "] b": "pane::ActivateNextItem",
+ "[ b": "pane::ActivatePreviousItem",
+ "] shift-b": "pane::ActivateLastItem",
+ "[ shift-b": ["pane::ActivateItem", 0],
+ "] space": "vim::InsertEmptyLineBelow",
+ "[ space": "vim::InsertEmptyLineAbove",
+ "[ e": "editor::MoveLineUp",
+ "] e": "editor::MoveLineDown",
+ "[ f": "workspace::FollowNextCollaborator",
+ "] f": "workspace::FollowNextCollaborator",
+ "] }": ["vim::UnmatchedForward", { "char": "}" }],
+ "[ {": ["vim::UnmatchedBackward", { "char": "{" }],
+ "] )": ["vim::UnmatchedForward", { "char": ")" }],
+ "[ (": ["vim::UnmatchedBackward", { "char": "(" }],
+ // tree-sitter related commands
+ "[ x": "vim::SelectLargerSyntaxNode",
+ "] x": "vim::SelectSmallerSyntaxNode"
+ }
+ },
{
"context": "vim_mode == normal",
"bindings": {
@@ -247,9 +257,6 @@
"g w": "vim::PushRewrap",
"g q": "vim::PushRewrap",
"insert": "vim::InsertBefore",
- // tree-sitter related commands
- "[ x": "vim::SelectLargerSyntaxNode",
- "] x": "vim::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPreviousDiagnostic",
"] c": "editor::GoToHunk",
@@ -315,10 +322,7 @@
"g w": "vim::Rewrap",
"g ?": "vim::ConvertToRot13",
// "g ?": "vim::ConvertToRot47",
- "\"": "vim::PushRegister",
- // tree-sitter related commands
- "[ x": "editor::SelectLargerSyntaxNode",
- "] x": "editor::SelectSmallerSyntaxNode"
+ "\"": "vim::PushRegister"
}
},
{
@@ -335,7 +339,7 @@
"ctrl-x ctrl-z": "editor::Cancel",
"ctrl-x ctrl-e": "vim::LineDown",
"ctrl-x ctrl-y": "vim::LineUp",
- "ctrl-w": "editor::DeleteToPreviousWordStart",
+ "ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent",
@@ -352,6 +356,15 @@
"ctrl-s": "editor::ShowSignatureHelp"
}
},
+ {
+ "context": "showing_completions",
+ "bindings": {
+ "ctrl-d": "vim::ScrollDown",
+ "ctrl-u": "vim::ScrollUp",
+ "ctrl-e": "vim::LineDown",
+ "ctrl-y": "vim::LineUp"
+ }
+ },
{
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
"bindings": {
@@ -386,11 +399,14 @@
"ctrl-[": "editor::Cancel",
";": "vim::HelixCollapseSelection",
":": "command_palette::Toggle",
+ "m": "vim::PushHelixMatch",
+ "]": ["vim::PushHelixNext", { "around": true }],
+ "[": ["vim::PushHelixPrevious", { "around": true }],
"left": "vim::WrappingLeft",
"right": "vim::WrappingRight",
"h": "vim::WrappingLeft",
"l": "vim::WrappingRight",
- "y": "editor::Copy",
+ "y": "vim::HelixYank",
"alt-;": "vim::OtherEnd",
"ctrl-r": "vim::Redo",
"f": ["vim::PushFindForward", { "before": false, "multiline": true }],
@@ -407,13 +423,7 @@
"g w": "vim::PushRewrap",
"insert": "vim::InsertBefore",
"alt-.": "vim::RepeatFind",
- // tree-sitter related commands
- "[ x": "editor::SelectLargerSyntaxNode",
- "] x": "editor::SelectSmallerSyntaxNode",
- "] d": "editor::GoToDiagnostic",
- "[ d": "editor::GoToPreviousDiagnostic",
- "] c": "editor::GoToHunk",
- "[ c": "editor::GoToPreviousHunk",
+ "alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }],
// Goto mode
"g n": "pane::ActivateNextItem",
"g p": "pane::ActivatePreviousItem",
@@ -425,12 +435,14 @@
"g h": "vim::StartOfLine",
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
"g e": "vim::EndOfDocument",
+ "g .": "vim::HelixGotoLastModification", // go to last modification
"g r": "editor::FindAllReferences", // zed specific
"g t": "vim::WindowTop",
"g c": "vim::WindowMiddle",
"g b": "vim::WindowBottom",
- "x": "editor::SelectLine",
+ "shift-r": "editor::Paste",
+ "x": "vim::HelixSelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode
@@ -455,9 +467,6 @@
"space c": "editor::ToggleComments",
"space y": "editor::Copy",
"space p": "editor::Paste",
- // Match mode
- "m m": "vim::Matching",
- "m i w": ["workspace::SendKeystrokes", "v i w"],
"shift-u": "editor::Redo",
"ctrl-c": "editor::ToggleComments",
"d": "vim::HelixDelete",
@@ -526,7 +535,7 @@
}
},
{
- "context": "vim_operator == a || vim_operator == i || vim_operator == cs",
+ "context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous",
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignore_punctuation": true }],
@@ -563,6 +572,48 @@
"e": "vim::EntireFile"
}
},
+ {
+ "context": "vim_operator == helix_m",
+ "bindings": {
+ "m": "vim::Matching"
+ }
+ },
+ {
+ "context": "vim_operator == helix_next",
+ "bindings": {
+ "z": "vim::NextSectionStart",
+ "shift-z": "vim::NextSectionEnd",
+ "*": "vim::NextComment",
+ "/": "vim::NextComment",
+ "-": "vim::NextLesserIndent",
+ "+": "vim::NextGreaterIndent",
+ "=": "vim::NextSameIndent",
+ "b": "pane::ActivateNextItem",
+ "shift-b": "pane::ActivateLastItem",
+ "x": "editor::SelectSmallerSyntaxNode",
+ "d": "editor::GoToDiagnostic",
+ "c": "editor::GoToHunk",
+ "space": "vim::InsertEmptyLineBelow"
+ }
+ },
+ {
+ "context": "vim_operator == helix_previous",
+ "bindings": {
+ "z": "vim::PreviousSectionStart",
+ "shift-z": "vim::PreviousSectionEnd",
+ "*": "vim::PreviousComment",
+ "/": "vim::PreviousComment",
+ "-": "vim::PreviousLesserIndent",
+ "+": "vim::PreviousGreaterIndent",
+ "=": "vim::PreviousSameIndent",
+ "b": "pane::ActivatePreviousItem",
+ "shift-b": ["pane::ActivateItem", 0],
+ "x": "editor::SelectLargerSyntaxNode",
+ "d": "editor::GoToPreviousDiagnostic",
+ "c": "editor::GoToPreviousHunk",
+ "space": "vim::InsertEmptyLineAbove"
+ }
+ },
{
"context": "vim_operator == c",
"bindings": {
@@ -809,14 +860,14 @@
"j": "menu::SelectNext",
"k": "menu::SelectPrevious",
"l": "project_panel::ExpandSelectedEntry",
- "o": "project_panel::OpenPermanent",
"shift-d": "project_panel::Delete",
"shift-r": "project_panel::Rename",
"t": "project_panel::OpenPermanent",
- "v": "project_panel::OpenPermanent",
+ "v": "project_panel::OpenSplitVertical",
+ "o": "project_panel::OpenSplitHorizontal",
"p": "project_panel::Open",
"x": "project_panel::RevealInFileManager",
- "s": "project_panel::OpenWithSystem",
+ "s": "workspace::OpenWithSystem",
"z d": "project_panel::CompareMarkedFiles",
"] c": "project_panel::SelectNextGitEntry",
"[ c": "project_panel::SelectPrevGitEntry",
@@ -172,7 +172,7 @@ The user has specified the following rules that should be applied:
Rules title: {{title}}
{{/if}}
``````
-{{contents}}}
+{{contents}}
``````
{{/each}}
{{/if}}
@@ -1,4 +1,5 @@
{
+ "project_name": null,
// The name of the Zed theme to use for the UI.
//
// `mode` is one of:
@@ -71,8 +72,8 @@
"ui_font_weight": 400,
// The default font size for text in the UI
"ui_font_size": 16,
- // The default font size for text in the agent panel
- "agent_font_size": 16,
+ // The default font size for text in the agent panel. Falls back to the UI font size if unset.
+ "agent_font_size": null,
// How much to fade out unused code.
"unnecessary_code_fade": 0.3,
// Active pane styling settings.
@@ -162,6 +163,12 @@
// 2. Always quit the application
// "on_last_window_closed": "quit_app",
"on_last_window_closed": "platform_default",
+ // Whether to show padding for zoomed panels.
+ // When enabled, zoomed center panels (e.g. code editor) will have padding all around,
+ // while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
+ //
+ // Default: true
+ "zoomed_padding": true,
// Whether to use the system provided dialogs for Open and Save As.
// When set to false, Zed will use the built-in keyboard-first pickers.
"use_system_path_prompts": true,
@@ -182,8 +189,8 @@
// 4. A box drawn around the following character
// "hollow"
//
- // Default: not set, defaults to "bar"
- "cursor_shape": null,
+ // Default: "bar"
+ "cursor_shape": "bar",
// Determines when the mouse cursor should be hidden in an editor or input box.
//
// 1. Never hide the mouse cursor:
@@ -217,9 +224,25 @@
"current_line_highlight": "all",
// Whether to highlight all occurrences of the selected text in an editor.
"selection_highlight": true,
+ // Whether the text selection should have rounded corners.
+ "rounded_selection": true,
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
+ // The minimum APCA perceptual contrast between foreground and background colors.
+ // APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
+ // especially for dark mode. Values range from 0 to 106.
+ //
+ // Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
+ // https://readtech.org/ARC/tests/bronze-simple-mode/
+ // - 0: No contrast adjustment
+ // - 45: Minimum for large fluent text (36px+)
+ // - 60: Minimum for other content text
+ // - 75: Minimum for body text
+ // - 90: Preferred for body text
+ //
+ // This only affects text drawn over highlight backgrounds in the editor.
+ "minimum_contrast_for_highlights": 45,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
@@ -260,8 +283,8 @@
// - "warning"
// - "info"
// - "hint"
- // - null — allow all diagnostics (default)
- "diagnostics_max_severity": null,
+ // - "all" — allow all diagnostics (default)
+ "diagnostics_max_severity": "all",
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -273,6 +296,8 @@
"redact_private_values": false,
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 5,
+ // The default number of context lines shown in multibuffer excerpts.
+ "excerpt_context_lines": 2,
// Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
// Whether to use additional LSP queries to format (and amend) the code after
@@ -286,6 +311,8 @@
// bracket, brace, single or double quote characters.
// For example, when you select text and type (, Zed will surround the text with ().
"use_auto_surround": true,
+ /// Whether indentation should be adjusted based on the context whilst typing.
+ "auto_indent": true,
// Whether indentation of pasted content should be adjusted based on the context.
"auto_indent_on_paste": true,
// Controls how the editor handles the autoclosed characters.
@@ -335,6 +362,11 @@
// - It is adjacent to an edge (start or end)
// - It is adjacent to a whitespace (left or right)
"show_whitespaces": "selection",
+ // Visible characters used to render whitespace when show_whitespaces is enabled.
+ "whitespace_map": {
+ "space": "•",
+ "tab": "→"
+ },
// Settings related to calls in Zed
"calls": {
// Join calls with the microphone live by default
@@ -355,6 +387,8 @@
// Whether to show code action buttons in the editor toolbar.
"code_actions": false
},
+ // Whether to allow windows to tab together based on the user’s tabbing preference (macOS only).
+ "use_system_window_tabs": false,
// Titlebar related settings
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.
@@ -645,6 +679,8 @@
// "never"
"show": "always"
},
+ // Whether to enable drag-and-drop operations in the project panel.
+ "drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false
},
@@ -710,20 +746,10 @@
// Default width of the collaboration panel.
"default_width": 240
},
- "chat_panel": {
- // When to show the chat panel button in the status bar.
- // Can be 'never', 'always', or 'when_in_call',
- // or a boolean (interpreted as 'never'/'always').
- "button": "when_in_call",
- // Where to the chat panel. Can be 'left' or 'right'.
- "dock": "right",
- // Default width of the chat panel.
- "default_width": 240
- },
"git_panel": {
// Whether to show the git panel button in the status bar.
"button": true,
- // Where to show the git panel. Can be 'left' or 'right'.
+ // Where to dock the git panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the git panel.
"default_width": 360,
@@ -808,6 +834,9 @@
// }
],
// When enabled, the agent can run potentially destructive actions without asking for your confirmation.
+ //
+ // Note: This setting has no effect on external agents that support permission modes, such as Claude Code.
+ // You can set `agent_servers.claude.default_mode` to `bypassPermissions` to skip all permission requests.
"always_allow_tool_actions": false,
// When enabled, the agent will stream edits.
"stream_edits": false,
@@ -887,11 +916,6 @@
},
// The settings for slash commands.
"slash_commands": {
- // Settings for the `/docs` slash command.
- "docs": {
- // Whether `/docs` is enabled.
- "enabled": false
- },
// Settings for the `/project` slash command.
"project": {
// Whether `/project` is enabled.
@@ -937,7 +961,7 @@
// Show git status colors in the editor tabs.
"git_status": false,
// Position of the close button on the editor tabs.
- // One of: ["right", "left", "hidden"]
+ // One of: ["right", "left"]
"close_position": "right",
// Whether to show the file icon for a tab.
"file_icons": false,
@@ -1136,11 +1160,6 @@
// The minimum severity of the diagnostics to show inline.
// Inherits editor's diagnostics' max severity settings when `null`.
"max_severity": null
- },
- "cargo": {
- // When enabled, Zed disables rust-analyzer's check on save and starts to query
- // Cargo diagnostics separately.
- "fetch_cargo_diagnostics": false
}
},
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
@@ -1185,6 +1204,10 @@
// The minimum column number to show the inline blame information at
"min_column": 0
},
+ // Control which information is shown in the branch picker.
+ "branch_picker": {
+ "show_author_name": true
+ },
// How git hunks are displayed visually in the editor.
// This setting can take two values:
//
@@ -1256,7 +1279,9 @@
// Status bar-related settings.
"status_bar": {
// Whether to show the active language button in the status bar.
- "active_language_button": true
+ "active_language_button": true,
+ // Whether to show the cursor position button in the status bar.
+ "cursor_position_button": true
},
// Settings specific to the terminal
"terminal": {
@@ -1504,6 +1529,11 @@
//
// Default: fallback
"words": "fallback",
+ // Minimum number of characters required to automatically trigger word-based completions.
+ // Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
+ //
+ // Default: 3
+ "words_min_length": 3,
// Whether to fetch LSP completions or not.
//
// Default: true
@@ -1576,7 +1606,7 @@
"ensure_final_newline_on_save": false
},
"Elixir": {
- "language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
+ "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
},
"Elm": {
"tab_size": 4
@@ -1601,7 +1631,7 @@
}
},
"HEEX": {
- "language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
+ "language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
},
"HTML": {
"prettier": {
@@ -1630,6 +1660,9 @@
"allowed": true
}
},
+ "Kotlin": {
+ "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
+ },
"LaTeX": {
"formatter": "language_server",
"language_servers": ["texlab", "..."],
@@ -1643,9 +1676,6 @@
"use_on_type_format": false,
"allow_rewrap": "anywhere",
"soft_wrap": "editor_width",
- "completions": {
- "words": "disabled"
- },
"prettier": {
"allowed": true
}
@@ -1659,9 +1689,6 @@
}
},
"Plain Text": {
- "completions": {
- "words": "disabled"
- },
"allow_rewrap": "anywhere"
},
"Python": {
@@ -1752,7 +1779,7 @@
"api_url": "http://localhost:1234/api/v0"
},
"deepseek": {
- "api_url": "https://api.deepseek.com"
+ "api_url": "https://api.deepseek.com/v1"
},
"mistral": {
"api_url": "https://api.mistral.ai/v1"
@@ -1900,7 +1927,10 @@
"debugger": {
"stepping_granularity": "line",
"save_breakpoints": true,
+ "timeout": 2000,
"dock": "bottom",
+ "log_dap_communications": true,
+ "format_dap_log_messages": true,
"button": true
},
// Configures any number of settings profiles that are temporarily applied on
@@ -43,8 +43,8 @@
// "args": ["--login"]
// }
// }
- "shell": "system",
+ "shell": "system"
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
- "tags": []
+ // "tags": []
}
]
@@ -93,7 +93,7 @@
"terminal.ansi.bright_cyan": "#4c806fff",
"terminal.ansi.dim_cyan": "#cbf2e4ff",
"terminal.ansi.white": "#bfbdb6ff",
- "terminal.ansi.bright_white": "#bfbdb6ff",
+ "terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#787876ff",
"link_text.hover": "#5ac1feff",
"conflict": "#feb454ff",
@@ -316,6 +316,11 @@
"font_style": null,
"font_weight": null
},
+ "punctuation.markup": {
+ "color": "#a6a5a0ff",
+ "font_style": null,
+ "font_weight": null
+ },
"punctuation.special": {
"color": "#d2a6ffff",
"font_style": null,
@@ -479,7 +484,7 @@
"terminal.ansi.bright_cyan": "#ace0cbff",
"terminal.ansi.dim_cyan": "#2a5f4aff",
"terminal.ansi.white": "#fcfcfcff",
- "terminal.ansi.bright_white": "#fcfcfcff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#bcbec0ff",
"link_text.hover": "#3b9ee5ff",
"conflict": "#f1ad49ff",
@@ -702,6 +707,11 @@
"font_style": null,
"font_weight": null
},
+ "punctuation.markup": {
+ "color": "#73777bff",
+ "font_style": null,
+ "font_weight": null
+ },
"punctuation.special": {
"color": "#a37accff",
"font_style": null,
@@ -865,7 +875,7 @@
"terminal.ansi.bright_cyan": "#4c806fff",
"terminal.ansi.dim_cyan": "#cbf2e4ff",
"terminal.ansi.white": "#cccac2ff",
- "terminal.ansi.bright_white": "#cccac2ff",
+ "terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#898a8aff",
"link_text.hover": "#72cffeff",
"conflict": "#fecf72ff",
@@ -1088,6 +1098,11 @@
"font_style": null,
"font_weight": null
},
+ "punctuation.markup": {
+ "color": "#b4b3aeff",
+ "font_style": null,
+ "font_weight": null
+ },
"punctuation.special": {
"color": "#dfbfffff",
"font_style": null,
@@ -94,7 +94,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
- "terminal.ansi.bright_white": "#fbf1c7ff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -325,6 +325,11 @@
"font_style": null,
"font_weight": null
},
+ "punctuation.markup": {
+ "color": "#83a598ff",
+ "font_style": null,
+ "font_weight": null
+ },
"punctuation.special": {
"color": "#e5d5adff",
"font_style": null,
@@ -494,7 +499,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
- "terminal.ansi.bright_white": "#fbf1c7ff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -725,6 +730,11 @@
"font_style": null,
"font_weight": null
},
+ "punctuation.markup": {
+ "color": "#83a598ff",
+ "font_style": null,
+ "font_weight": null
+ },
"punctuation.special": {
"color": "#e5d5adff",
"font_style": null,
@@ -894,7 +904,7 @@
"terminal.ansi.bright_cyan": "#45603eff",
"terminal.ansi.dim_cyan": "#c7dfbdff",
"terminal.ansi.white": "#fbf1c7ff",
- "terminal.ansi.bright_white": "#fbf1c7ff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#83a598ff",
"version_control.added": "#b7bb26ff",
@@ -1125,6 +1135,11 @@
"font_style": null,
"font_weight": null
},
+ "punctuation.markup": {
+ "color": "#83a598ff",
+ "font_style": null,
+ "font_weight": null
+ },
"punctuation.special": {
"color": "#e5d5adff",
"font_style": null,
@@ -1294,7 +1309,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#fbf1c7ff",
- "terminal.ansi.bright_white": "#fbf1c7ff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
@@ -1525,6 +1540,11 @@
"font_style": null,
"font_weight": null
},
+ "punctuation.markup": {
+ "color": "#066578ff",
+ "font_style": null,
+ "font_weight": null
+ },
"punctuation.special": {
"color": "#413d3aff",
"font_style": null,
@@ -1694,7 +1714,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f9f5d7ff",
- "terminal.ansi.bright_white": "#f9f5d7ff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
@@ -1925,6 +1945,11 @@
"font_style": null,
"font_weight": null
},
+ "punctuation.markup": {
+ "color": "#066578ff",
+ "font_style": null,
+ "font_weight": null
+ },
"punctuation.special": {
"color": "#413d3aff",
"font_style": null,
@@ -2094,7 +2119,7 @@
"terminal.ansi.bright_cyan": "#9fbca8ff",
"terminal.ansi.dim_cyan": "#253e2eff",
"terminal.ansi.white": "#f2e5bcff",
- "terminal.ansi.bright_white": "#f2e5bcff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#b0a189ff",
"link_text.hover": "#0b6678ff",
"version_control.added": "#797410ff",
@@ -2325,6 +2350,11 @@
"font_style": null,
"font_weight": null
},
+ "punctuation.markup": {
+ "color": "#066578ff",
+ "font_style": null,
+ "font_weight": null
+ },
"punctuation.special": {
"color": "#413d3aff",
"font_style": null,
@@ -93,7 +93,7 @@
"terminal.ansi.bright_cyan": "#3a565bff",
"terminal.ansi.dim_cyan": "#b9d9dfff",
"terminal.ansi.white": "#dce0e5ff",
- "terminal.ansi.bright_white": "#dce0e5ff",
+ "terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#575d65ff",
"link_text.hover": "#74ade8ff",
"version_control.added": "#27a657ff",
@@ -321,6 +321,11 @@
"font_style": null,
"font_weight": null
},
+ "punctuation.markup": {
+ "color": "#d07277ff",
+ "font_style": null,
+ "font_weight": null
+ },
"punctuation.special": {
"color": "#b1574bff",
"font_style": null,
@@ -468,7 +473,7 @@
"terminal.bright_foreground": "#242529ff",
"terminal.dim_foreground": "#fafafaff",
"terminal.ansi.black": "#242529ff",
- "terminal.ansi.bright_black": "#242529ff",
+ "terminal.ansi.bright_black": "#747579ff",
"terminal.ansi.dim_black": "#97979aff",
"terminal.ansi.red": "#d36151ff",
"terminal.ansi.bright_red": "#f0b0a4ff",
@@ -489,7 +494,7 @@
"terminal.ansi.bright_cyan": "#a3bedaff",
"terminal.ansi.dim_cyan": "#254058ff",
"terminal.ansi.white": "#fafafaff",
- "terminal.ansi.bright_white": "#fafafaff",
+ "terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#aaaaaaff",
"link_text.hover": "#5c78e2ff",
"version_control.added": "#27a657ff",
@@ -715,6 +720,11 @@
"font_style": null,
"font_weight": null
},
+ "punctuation.markup": {
+ "color": "#d3604fff",
+ "font_style": null,
+ "font_weight": null
+ },
"punctuation.special": {
"color": "#b92b46ff",
"font_style": null,
@@ -13,33 +13,39 @@ path = "src/acp_thread.rs"
doctest = false
[features]
-test-support = ["gpui/test-support", "project/test-support"]
+test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"]
[dependencies]
action_log.workspace = true
agent-client-protocol.workspace = true
-agent.workspace = true
+agent_settings.workspace = true
anyhow.workspace = true
buffer_diff.workspace = true
collections.workspace = true
editor.workspace = true
+file_icons.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
+language_model.workspace = true
markdown.workspace = true
+parking_lot = { workspace = true, optional = true }
+portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
+task.workspace = true
terminal.workspace = true
ui.workspace = true
url.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
+which.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
@@ -3,13 +3,20 @@ mod diff;
mod mention;
mod terminal;
+use agent_settings::AgentSettings;
+use collections::HashSet;
pub use connection::*;
pub use diff::*;
+use futures::future::Shared;
+use language::language_settings::FormatOnSave;
pub use mention::*;
+use project::lsp_store::{FormatTrigger, LspFormatTarget};
+use serde::{Deserialize, Serialize};
+use settings::Settings as _;
pub use terminal::*;
use action_log::ActionLog;
-use agent_client_protocol as acp;
+use agent_client_protocol::{self as acp};
use anyhow::{Context as _, Result, anyhow};
use editor::Bias;
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
@@ -24,21 +31,34 @@ use std::fmt::{Formatter, Write};
use std::ops::Range;
use std::process::ExitStatus;
use std::rc::Rc;
+use std::time::{Duration, Instant};
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
use ui::App;
-use util::ResultExt;
+use util::{ResultExt, get_system_shell};
+use uuid::Uuid;
#[derive(Debug)]
pub struct UserMessage {
pub id: Option<UserMessageId>,
pub content: ContentBlock,
- pub checkpoint: Option<GitStoreCheckpoint>,
+ pub chunks: Vec<acp::ContentBlock>,
+ pub checkpoint: Option<Checkpoint>,
+}
+
+#[derive(Debug)]
+pub struct Checkpoint {
+ git_checkpoint: GitStoreCheckpoint,
+ pub show: bool,
}
impl UserMessage {
fn to_markdown(&self, cx: &App) -> String {
let mut markdown = String::new();
- if let Some(_) = self.checkpoint {
+ if self
+ .checkpoint
+ .as_ref()
+ .is_some_and(|checkpoint| checkpoint.show)
+ {
writeln!(markdown, "## User (checkpoint)").unwrap();
} else {
writeln!(markdown, "## User").unwrap();
@@ -98,7 +118,7 @@ pub enum AgentThreadEntry {
}
impl AgentThreadEntry {
- fn to_markdown(&self, cx: &App) -> String {
+ pub fn to_markdown(&self, cx: &App) -> String {
match self {
Self::UserMessage(message) => message.to_markdown(cx),
Self::AssistantMessage(message) => message.to_markdown(cx),
@@ -106,6 +126,14 @@ impl AgentThreadEntry {
}
}
+ pub fn user_message(&self) -> Option<&UserMessage> {
+ if let AgentThreadEntry::UserMessage(message) = self {
+ Some(message)
+ } else {
+ None
+ }
+ }
+
pub fn diffs(&self) -> impl Iterator<Item = &Entity<Diff>> {
if let AgentThreadEntry::ToolCall(call) = self {
itertools::Either::Left(call.diffs())
@@ -157,38 +185,46 @@ impl ToolCall {
tool_call: acp::ToolCall,
status: ToolCallStatus,
language_registry: Arc<LanguageRegistry>,
+ terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
cx: &mut App,
- ) -> Self {
- Self {
+ ) -> Result<Self> {
+ let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
+ first_line.to_owned() + "…"
+ } else {
+ tool_call.title
+ };
+ let mut content = Vec::with_capacity(tool_call.content.len());
+ for item in tool_call.content {
+ content.push(ToolCallContent::from_acp(
+ item,
+ language_registry.clone(),
+ terminals,
+ cx,
+ )?);
+ }
+
+ let result = Self {
id: tool_call.id,
- label: cx.new(|cx| {
- Markdown::new(
- tool_call.title.into(),
- Some(language_registry.clone()),
- None,
- cx,
- )
- }),
+ label: cx
+ .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
kind: tool_call.kind,
- content: tool_call
- .content
- .into_iter()
- .map(|content| ToolCallContent::from_acp(content, language_registry.clone(), cx))
- .collect(),
+ content,
locations: tool_call.locations,
resolved_locations: Vec::default(),
status,
raw_input: tool_call.raw_input,
raw_output: tool_call.raw_output,
- }
+ };
+ Ok(result)
}
fn update_fields(
&mut self,
fields: acp::ToolCallUpdateFields,
language_registry: Arc<LanguageRegistry>,
+ terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
cx: &mut App,
- ) {
+ ) -> Result<()> {
let acp::ToolCallUpdateFields {
kind,
status,
@@ -204,20 +240,36 @@ impl ToolCall {
}
if let Some(status) = status {
- self.status = ToolCallStatus::Allowed { status };
+ self.status = status.into();
}
if let Some(title) = title {
self.label.update(cx, |label, cx| {
- label.replace(title, cx);
+ if let Some((first_line, _)) = title.split_once("\n") {
+ label.replace(first_line.to_owned() + "…", cx)
+ } else {
+ label.replace(title, cx);
+ }
});
}
if let Some(content) = content {
- self.content = content
- .into_iter()
- .map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx))
- .collect();
+ let new_content_len = content.len();
+ let mut content = content.into_iter();
+
+ // Reuse existing content if we can
+ for (old, new) in self.content.iter_mut().zip(content.by_ref()) {
+ old.update_from_acp(new, language_registry.clone(), terminals, cx)?;
+ }
+ for new in content {
+ self.content.push(ToolCallContent::from_acp(
+ new,
+ language_registry.clone(),
+ terminals,
+ cx,
+ )?)
+ }
+ self.content.truncate(new_content_len);
}
if let Some(locations) = locations {
@@ -229,17 +281,17 @@ impl ToolCall {
}
if let Some(raw_output) = raw_output {
- if self.content.is_empty() {
- if let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx)
- {
- self.content
- .push(ToolCallContent::ContentBlock(ContentBlock::Markdown {
- markdown,
- }));
- }
+ if self.content.is_empty()
+ && let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx)
+ {
+ self.content
+ .push(ToolCallContent::ContentBlock(ContentBlock::Markdown {
+ markdown,
+ }));
}
self.raw_output = Some(raw_output);
}
+ Ok(())
}
pub fn diffs(&self) -> impl Iterator<Item = &Entity<Diff>> {
@@ -278,11 +330,9 @@ impl ToolCall {
) -> Option<AgentLocation> {
let buffer = project
.update(cx, |project, cx| {
- if let Some(path) = project.project_path_for_absolute_path(&location.path, cx) {
- Some(project.open_buffer(path, cx))
- } else {
- None
- }
+ project
+ .project_path_for_absolute_path(&location.path, cx)
+ .map(|path| project.open_buffer(path, cx))
})
.ok()??;
let buffer = buffer.await.log_err()?;
@@ -325,30 +375,48 @@ impl ToolCall {
#[derive(Debug)]
pub enum ToolCallStatus {
+ /// The tool call hasn't started running yet, but we start showing it to
+ /// the user.
+ Pending,
+ /// The tool call is waiting for confirmation from the user.
WaitingForConfirmation {
options: Vec<acp::PermissionOption>,
respond_tx: oneshot::Sender<acp::PermissionOptionId>,
},
- Allowed {
- status: acp::ToolCallStatus,
- },
+ /// The tool call is currently running.
+ InProgress,
+ /// The tool call completed successfully.
+ Completed,
+ /// The tool call failed.
+ Failed,
+ /// The user rejected the tool call.
Rejected,
+ /// The user canceled generation so the tool call was canceled.
Canceled,
}
+impl From<acp::ToolCallStatus> for ToolCallStatus {
+ fn from(status: acp::ToolCallStatus) -> Self {
+ match status {
+ acp::ToolCallStatus::Pending => Self::Pending,
+ acp::ToolCallStatus::InProgress => Self::InProgress,
+ acp::ToolCallStatus::Completed => Self::Completed,
+ acp::ToolCallStatus::Failed => Self::Failed,
+ }
+ }
+}
+
impl Display for ToolCallStatus {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
+ ToolCallStatus::Pending => "Pending",
ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation",
- ToolCallStatus::Allowed { status } => match status {
- acp::ToolCallStatus::Pending => "Pending",
- acp::ToolCallStatus::InProgress => "In Progress",
- acp::ToolCallStatus::Completed => "Completed",
- acp::ToolCallStatus::Failed => "Failed",
- },
+ ToolCallStatus::InProgress => "In Progress",
+ ToolCallStatus::Completed => "Completed",
+ ToolCallStatus::Failed => "Failed",
ToolCallStatus::Rejected => "Rejected",
ToolCallStatus::Canceled => "Canceled",
}
@@ -392,11 +460,11 @@ impl ContentBlock {
language_registry: &Arc<LanguageRegistry>,
cx: &mut App,
) {
- if matches!(self, ContentBlock::Empty) {
- if let acp::ContentBlock::ResourceLink(resource_link) = block {
- *self = ContentBlock::ResourceLink { resource_link };
- return;
- }
+ if matches!(self, ContentBlock::Empty)
+ && let acp::ContentBlock::ResourceLink(resource_link) = block
+ {
+ *self = ContentBlock::ResourceLink { resource_link };
+ return;
}
let new_content = self.block_string_contents(block);
@@ -430,7 +498,7 @@ impl ContentBlock {
fn block_string_contents(&self, block: acp::ContentBlock) -> String {
match block {
- acp::ContentBlock::Text(text_content) => text_content.text.clone(),
+ acp::ContentBlock::Text(text_content) => text_content.text,
acp::ContentBlock::ResourceLink(resource_link) => {
Self::resource_link_md(&resource_link.uri)
}
@@ -442,21 +510,24 @@ impl ContentBlock {
}),
..
}) => Self::resource_link_md(&uri),
- acp::ContentBlock::Image(_)
- | acp::ContentBlock::Audio(_)
- | acp::ContentBlock::Resource(_) => String::new(),
+ acp::ContentBlock::Image(image) => Self::image_md(&image),
+ acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => String::new(),
}
}
fn resource_link_md(uri: &str) -> String {
- if let Some(uri) = MentionUri::parse(&uri).log_err() {
+ if let Some(uri) = MentionUri::parse(uri).log_err() {
uri.as_link().to_string()
} else {
uri.to_string()
}
}
- fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
+ fn image_md(_image: &acp::ImageContent) -> String {
+ "`Image`".into()
+ }
+
+ pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
match self {
ContentBlock::Empty => "",
ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
@@ -491,16 +562,54 @@ impl ToolCallContent {
pub fn from_acp(
content: acp::ToolCallContent,
language_registry: Arc<LanguageRegistry>,
+ terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
cx: &mut App,
- ) -> Self {
+ ) -> Result<Self> {
match content {
- acp::ToolCallContent::Content { content } => {
- Self::ContentBlock(ContentBlock::new(content, &language_registry, cx))
- }
- acp::ToolCallContent::Diff { diff } => {
- Self::Diff(cx.new(|cx| Diff::from_acp(diff, language_registry, cx)))
+ acp::ToolCallContent::Content { content } => Ok(Self::ContentBlock(ContentBlock::new(
+ content,
+ &language_registry,
+ cx,
+ ))),
+ acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
+ Diff::finalized(
+ diff.path,
+ diff.old_text,
+ diff.new_text,
+ language_registry,
+ cx,
+ )
+ }))),
+ acp::ToolCallContent::Terminal { terminal_id } => terminals
+ .get(&terminal_id)
+ .cloned()
+ .map(Self::Terminal)
+ .ok_or_else(|| anyhow::anyhow!("Terminal with id `{}` not found", terminal_id)),
+ }
+ }
+
+ pub fn update_from_acp(
+ &mut self,
+ new: acp::ToolCallContent,
+ language_registry: Arc<LanguageRegistry>,
+ terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
+ cx: &mut App,
+ ) -> Result<()> {
+ let needs_update = match (&self, &new) {
+ (Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => {
+ old_diff.read(cx).needs_update(
+ new_diff.old_text.as_deref().unwrap_or(""),
+ &new_diff.new_text,
+ cx,
+ )
}
+ _ => true,
+ };
+
+ if needs_update {
+ *self = Self::from_acp(new, language_registry, terminals, cx)?;
}
+ Ok(())
}
pub fn to_markdown(&self, cx: &App) -> String {
@@ -618,6 +727,52 @@ impl PlanEntry {
}
}
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct TokenUsage {
+ pub max_tokens: u64,
+ pub used_tokens: u64,
+}
+
+impl TokenUsage {
+ pub fn ratio(&self) -> TokenUsageRatio {
+ #[cfg(debug_assertions)]
+ let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
+ .unwrap_or("0.8".to_string())
+ .parse()
+ .unwrap();
+ #[cfg(not(debug_assertions))]
+ let warning_threshold: f32 = 0.8;
+
+ // When the maximum is unknown because there is no selected model,
+ // avoid showing the token limit warning.
+ if self.max_tokens == 0 {
+ TokenUsageRatio::Normal
+ } else if self.used_tokens >= self.max_tokens {
+ TokenUsageRatio::Exceeded
+ } else if self.used_tokens as f32 / self.max_tokens as f32 >= warning_threshold {
+ TokenUsageRatio::Warning
+ } else {
+ TokenUsageRatio::Normal
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum TokenUsageRatio {
+ Normal,
+ Warning,
+ Exceeded,
+}
+
+#[derive(Debug, Clone)]
+pub struct RetryStatus {
+ pub last_error: SharedString,
+ pub attempt: usize,
+ pub max_attempts: usize,
+ pub started_at: Instant,
+ pub duration: Duration,
+}
+
pub struct AcpThread {
title: SharedString,
entries: Vec<AgentThreadEntry>,
@@ -628,44 +783,69 @@ pub struct AcpThread {
send_task: Option<Task<()>>,
connection: Rc<dyn AgentConnection>,
session_id: acp::SessionId,
+ token_usage: Option<TokenUsage>,
+ prompt_capabilities: acp::PromptCapabilities,
+ _observe_prompt_capabilities: Task<anyhow::Result<()>>,
+ determine_shell: Shared<Task<String>>,
+ terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
}
+#[derive(Debug)]
pub enum AcpThreadEvent {
NewEntry,
+ TitleUpdated,
+ TokenUsageUpdated,
EntryUpdated(usize),
EntriesRemoved(Range<usize>),
ToolAuthorizationRequired,
+ Retry(RetryStatus),
Stopped,
Error,
- ServerExited(ExitStatus),
+ LoadError(LoadError),
+ PromptCapabilitiesUpdated,
+ Refusal,
+ AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
+ ModeUpdated(acp::SessionModeId),
}
impl EventEmitter<AcpThreadEvent> for AcpThread {}
-#[derive(PartialEq, Eq)]
+#[derive(PartialEq, Eq, Debug)]
pub enum ThreadStatus {
Idle,
- WaitingForToolConfirmation,
Generating,
}
#[derive(Debug, Clone)]
pub enum LoadError {
Unsupported {
- error_message: SharedString,
- upgrade_message: SharedString,
- upgrade_command: String,
+ command: SharedString,
+ current_version: SharedString,
+ minimum_version: SharedString,
+ },
+ FailedToInstall(SharedString),
+ Exited {
+ status: ExitStatus,
},
- Exited(i32),
Other(SharedString),
}
impl Display for LoadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
- LoadError::Unsupported { error_message, .. } => write!(f, "{}", error_message),
- LoadError::Exited(status) => write!(f, "Server exited with status {}", status),
- LoadError::Other(msg) => write!(f, "{}", msg),
+ LoadError::Unsupported {
+ command: path,
+ current_version,
+ minimum_version,
+ } => {
+ write!(
+ f,
+ "version {current_version} from {path} is not supported (need at least {minimum_version})"
+ )
+ }
+ LoadError::FailedToInstall(msg) => write!(f, "Failed to install: {msg}"),
+ LoadError::Exited { status } => write!(f, "Server exited with status {status}"),
+ LoadError::Other(msg) => write!(f, "{msg}"),
}
}
}
@@ -677,10 +857,35 @@ impl AcpThread {
title: impl Into<SharedString>,
connection: Rc<dyn AgentConnection>,
project: Entity<Project>,
+ action_log: Entity<ActionLog>,
session_id: acp::SessionId,
+ mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
cx: &mut Context<Self>,
) -> Self {
- let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ let prompt_capabilities = *prompt_capabilities_rx.borrow();
+ let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
+ loop {
+ let caps = prompt_capabilities_rx.recv().await?;
+ this.update(cx, |this, cx| {
+ this.prompt_capabilities = caps;
+ cx.emit(AcpThreadEvent::PromptCapabilitiesUpdated);
+ })?;
+ }
+ });
+
+ let determine_shell = cx
+ .background_spawn(async move {
+ if cfg!(windows) {
+ return get_system_shell();
+ }
+
+ if which::which("bash").is_ok() {
+ "bash".into()
+ } else {
+ get_system_shell()
+ }
+ })
+ .shared();
Self {
action_log,
@@ -692,9 +897,18 @@ impl AcpThread {
send_task: None,
connection,
session_id,
+ token_usage: None,
+ prompt_capabilities,
+ _observe_prompt_capabilities: task,
+ terminals: HashMap::default(),
+ determine_shell,
}
}
+ pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
+ self.prompt_capabilities
+ }
+
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
&self.connection
}
@@ -721,27 +935,23 @@ impl AcpThread {
pub fn status(&self) -> ThreadStatus {
if self.send_task.is_some() {
- if self.waiting_for_tool_confirmation() {
- ThreadStatus::WaitingForToolConfirmation
- } else {
- ThreadStatus::Generating
- }
+ ThreadStatus::Generating
} else {
ThreadStatus::Idle
}
}
+ pub fn token_usage(&self) -> Option<&TokenUsage> {
+ self.token_usage.as_ref()
+ }
+
pub fn has_pending_edit_tool_calls(&self) -> bool {
for entry in self.entries.iter().rev() {
match entry {
AgentThreadEntry::UserMessage(_) => return false,
AgentThreadEntry::ToolCall(
call @ ToolCall {
- status:
- ToolCallStatus::Allowed {
- status:
- acp::ToolCallStatus::InProgress | acp::ToolCallStatus::Pending,
- },
+ status: ToolCallStatus::InProgress | ToolCallStatus::Pending,
..
},
) if call.diffs().next().is_some() => {
@@ -770,7 +980,7 @@ impl AcpThread {
&mut self,
update: acp::SessionUpdate,
cx: &mut Context<Self>,
- ) -> Result<()> {
+ ) -> Result<(), acp::Error> {
match update {
acp::SessionUpdate::UserMessageChunk { content } => {
self.push_user_content_block(None, content, cx);
@@ -782,7 +992,7 @@ impl AcpThread {
self.push_assistant_content_block(content, true, cx);
}
acp::SessionUpdate::ToolCall(tool_call) => {
- self.upsert_tool_call(tool_call, cx);
+ self.upsert_tool_call(tool_call, cx)?;
}
acp::SessionUpdate::ToolCallUpdate(tool_call_update) => {
self.update_tool_call(tool_call_update, cx)?;
@@ -790,6 +1000,12 @@ impl AcpThread {
acp::SessionUpdate::Plan(plan) => {
self.update_plan(plan, cx);
}
+ acp::SessionUpdate::AvailableCommandsUpdate { available_commands } => {
+ cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands))
+ }
+ acp::SessionUpdate::CurrentModeUpdate { current_mode_id } => {
+ cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id))
+ }
}
Ok(())
}
@@ -804,18 +1020,25 @@ impl AcpThread {
let entries_len = self.entries.len();
if let Some(last_entry) = self.entries.last_mut()
- && let AgentThreadEntry::UserMessage(UserMessage { id, content, .. }) = last_entry
+ && let AgentThreadEntry::UserMessage(UserMessage {
+ id,
+ content,
+ chunks,
+ ..
+ }) = last_entry
{
*id = message_id.or(id.take());
- content.append(chunk, &language_registry, cx);
+ content.append(chunk.clone(), &language_registry, cx);
+ chunks.push(chunk);
let idx = entries_len - 1;
cx.emit(AcpThreadEvent::EntryUpdated(idx));
} else {
- let content = ContentBlock::new(chunk, &language_registry, cx);
+ let content = ContentBlock::new(chunk.clone(), &language_registry, cx);
self.push_entry(
AgentThreadEntry::UserMessage(UserMessage {
id: message_id,
content,
+ chunks: vec![chunk],
checkpoint: None,
}),
cx,
@@ -872,6 +1095,30 @@ impl AcpThread {
cx.emit(AcpThreadEvent::NewEntry);
}
+ pub fn can_set_title(&mut self, cx: &mut Context<Self>) -> bool {
+ self.connection.set_title(&self.session_id, cx).is_some()
+ }
+
+ pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) -> Task<Result<()>> {
+ if title != self.title {
+ self.title = title.clone();
+ cx.emit(AcpThreadEvent::TitleUpdated);
+ if let Some(set_title) = self.connection.set_title(&self.session_id, cx) {
+ return set_title.run(title, cx);
+ }
+ }
+ Task::ready(Ok(()))
+ }
+
+ pub fn update_token_usage(&mut self, usage: Option<TokenUsage>, cx: &mut Context<Self>) {
+ self.token_usage = usage;
+ cx.emit(AcpThreadEvent::TokenUsageUpdated);
+ }
+
+ pub fn update_retry_status(&mut self, status: RetryStatus, cx: &mut Context<Self>) {
+ cx.emit(AcpThreadEvent::Retry(status));
+ }
+
pub fn update_tool_call(
&mut self,
update: impl Into<ToolCallUpdate>,
@@ -880,27 +1127,28 @@ impl AcpThread {
let update = update.into();
let languages = self.project.read(cx).languages().clone();
- let (ix, current_call) = self
- .tool_call_mut(update.id())
+ let ix = self
+ .index_for_tool_call(update.id())
.context("Tool call not found")?;
+ let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
+ unreachable!()
+ };
+
match update {
ToolCallUpdate::UpdateFields(update) => {
let location_updated = update.fields.locations.is_some();
- current_call.update_fields(update.fields, languages, cx);
+ call.update_fields(update.fields, languages, &self.terminals, cx)?;
if location_updated {
- self.resolve_locations(update.id.clone(), cx);
+ self.resolve_locations(update.id, cx);
}
}
ToolCallUpdate::UpdateDiff(update) => {
- current_call.content.clear();
- current_call
- .content
- .push(ToolCallContent::Diff(update.diff));
+ call.content.clear();
+ call.content.push(ToolCallContent::Diff(update.diff));
}
ToolCallUpdate::UpdateTerminal(update) => {
- current_call.content.clear();
- current_call
- .content
+ call.content.clear();
+ call.content
.push(ToolCallContent::Terminal(update.terminal));
}
}
@@ -911,32 +1159,63 @@ impl AcpThread {
}
/// Updates a tool call if id matches an existing entry, otherwise inserts a new one.
- pub fn upsert_tool_call(&mut self, tool_call: acp::ToolCall, cx: &mut Context<Self>) {
- let status = ToolCallStatus::Allowed {
- status: tool_call.status,
- };
- self.upsert_tool_call_inner(tool_call, status, cx)
+ pub fn upsert_tool_call(
+ &mut self,
+ tool_call: acp::ToolCall,
+ cx: &mut Context<Self>,
+ ) -> Result<(), acp::Error> {
+ let status = tool_call.status.into();
+ self.upsert_tool_call_inner(tool_call.into(), status, cx)
}
+ /// Fails if id does not match an existing entry.
pub fn upsert_tool_call_inner(
&mut self,
- tool_call: acp::ToolCall,
+ update: acp::ToolCallUpdate,
status: ToolCallStatus,
cx: &mut Context<Self>,
- ) {
+ ) -> Result<(), acp::Error> {
let language_registry = self.project.read(cx).languages().clone();
- let call = ToolCall::from_acp(tool_call, status, language_registry, cx);
- let id = call.id.clone();
+ let id = update.id.clone();
- if let Some((ix, current_call)) = self.tool_call_mut(&call.id) {
- *current_call = call;
+ if let Some(ix) = self.index_for_tool_call(&id) {
+ let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
+ unreachable!()
+ };
+
+ call.update_fields(update.fields, language_registry, &self.terminals, cx)?;
+ call.status = status;
cx.emit(AcpThreadEvent::EntryUpdated(ix));
} else {
+ let call = ToolCall::from_acp(
+ update.try_into()?,
+ status,
+ language_registry,
+ &self.terminals,
+ cx,
+ )?;
self.push_entry(AgentThreadEntry::ToolCall(call), cx);
};
self.resolve_locations(id, cx);
+ Ok(())
+ }
+
+ fn index_for_tool_call(&self, id: &acp::ToolCallId) -> Option<usize> {
+ self.entries
+ .iter()
+ .enumerate()
+ .rev()
+ .find_map(|(index, entry)| {
+ if let AgentThreadEntry::ToolCall(tool_call) = entry
+ && &tool_call.id == id
+ {
+ Some(index)
+ } else {
+ None
+ }
+ })
}
fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> {
@@ -957,6 +1236,22 @@ impl AcpThread {
})
}
+ pub fn tool_call(&mut self, id: &acp::ToolCallId) -> Option<(usize, &ToolCall)> {
+ self.entries
+ .iter()
+ .enumerate()
+ .rev()
+ .find_map(|(index, tool_call)| {
+ if let AgentThreadEntry::ToolCall(tool_call) = tool_call
+ && &tool_call.id == id
+ {
+ Some((index, tool_call))
+ } else {
+ None
+ }
+ })
+ }
+
pub fn resolve_locations(&mut self, id: acp::ToolCallId, cx: &mut Context<Self>) {
let project = self.project.clone();
let Some((_, tool_call)) = self.tool_call_mut(&id) else {
@@ -1005,20 +1300,50 @@ impl AcpThread {
pub fn request_tool_call_authorization(
&mut self,
- tool_call: acp::ToolCall,
+ tool_call: acp::ToolCallUpdate,
options: Vec<acp::PermissionOption>,
+ respect_always_allow_setting: bool,
cx: &mut Context<Self>,
- ) -> oneshot::Receiver<acp::PermissionOptionId> {
+ ) -> Result<BoxFuture<'static, acp::RequestPermissionOutcome>> {
let (tx, rx) = oneshot::channel();
+ if respect_always_allow_setting && AgentSettings::get_global(cx).always_allow_tool_actions {
+ // Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions,
+ // some tools would (incorrectly) continue to auto-accept.
+ if let Some(allow_once_option) = options.iter().find_map(|option| {
+ if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) {
+ Some(option.id.clone())
+ } else {
+ None
+ }
+ }) {
+ self.upsert_tool_call_inner(tool_call, ToolCallStatus::Pending, cx)?;
+ return Ok(async {
+ acp::RequestPermissionOutcome::Selected {
+ option_id: allow_once_option,
+ }
+ }
+ .boxed());
+ }
+ }
+
let status = ToolCallStatus::WaitingForConfirmation {
options,
respond_tx: tx,
};
- self.upsert_tool_call_inner(tool_call, status, cx);
+ self.upsert_tool_call_inner(tool_call, status, cx)?;
cx.emit(AcpThreadEvent::ToolAuthorizationRequired);
- rx
+
+ let fut = async {
+ match rx.await {
+ Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
+ Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
+ }
+ }
+ .boxed();
+
+ Ok(fut)
}
pub fn authorize_tool_call(
@@ -1037,9 +1362,7 @@ impl AcpThread {
ToolCallStatus::Rejected
}
acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => {
- ToolCallStatus::Allowed {
- status: acp::ToolCallStatus::InProgress,
- }
+ ToolCallStatus::InProgress
}
};
@@ -1054,23 +1377,27 @@ impl AcpThread {
cx.emit(AcpThreadEvent::EntryUpdated(ix));
}
- /// Returns true if the last turn is awaiting tool authorization
- pub fn waiting_for_tool_confirmation(&self) -> bool {
+ pub fn first_tool_awaiting_confirmation(&self) -> Option<&ToolCall> {
+ let mut first_tool_call = None;
+
for entry in self.entries.iter().rev() {
match &entry {
- AgentThreadEntry::ToolCall(call) => match call.status {
- ToolCallStatus::WaitingForConfirmation { .. } => return true,
- ToolCallStatus::Allowed { .. }
- | ToolCallStatus::Rejected
- | ToolCallStatus::Canceled => continue,
- },
+ AgentThreadEntry::ToolCall(call) => {
+ if let ToolCallStatus::WaitingForConfirmation { .. } = call.status {
+ first_tool_call = Some(call);
+ } else {
+ continue;
+ }
+ }
AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => {
- // Reached the beginning of the turn
- return false;
+ // Reached the beginning of the turn.
+ // If we had pending permission requests in the previous turn, they have been cancelled.
+ break;
}
}
}
- false
+
+ first_tool_call
}
pub fn plan(&self) -> &Plan {
@@ -1134,85 +1461,90 @@ impl AcpThread {
self.project.read(cx).languages().clone(),
cx,
);
+ let request = acp::PromptRequest {
+ prompt: message.clone(),
+ session_id: self.session_id.clone(),
+ };
let git_store = self.project.read(cx).git_store().clone();
- let old_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx));
- let message_id = if self
- .connection
- .session_editor(&self.session_id, cx)
- .is_some()
- {
+ let message_id = if self.connection.truncate(&self.session_id, cx).is_some() {
Some(UserMessageId::new())
} else {
None
};
- self.push_entry(
- AgentThreadEntry::UserMessage(UserMessage {
- id: message_id.clone(),
- content: block,
- checkpoint: None,
- }),
- cx,
- );
+
+ self.run_turn(cx, async move |this, cx| {
+ this.update(cx, |this, cx| {
+ this.push_entry(
+ AgentThreadEntry::UserMessage(UserMessage {
+ id: message_id.clone(),
+ content: block,
+ chunks: message,
+ checkpoint: None,
+ }),
+ cx,
+ );
+ })
+ .ok();
+
+ let old_checkpoint = git_store
+ .update(cx, |git, cx| git.checkpoint(cx))?
+ .await
+ .context("failed to get old checkpoint")
+ .log_err();
+ this.update(cx, |this, cx| {
+ if let Some((_ix, message)) = this.last_user_message() {
+ message.checkpoint = old_checkpoint.map(|git_checkpoint| Checkpoint {
+ git_checkpoint,
+ show: false,
+ });
+ }
+ this.connection.prompt(message_id, request, cx)
+ })?
+ .await
+ })
+ }
+
+ pub fn can_resume(&self, cx: &App) -> bool {
+ self.connection.resume(&self.session_id, cx).is_some()
+ }
+
+ pub fn resume(&mut self, cx: &mut Context<Self>) -> BoxFuture<'static, Result<()>> {
+ self.run_turn(cx, async move |this, cx| {
+ this.update(cx, |this, cx| {
+ this.connection
+ .resume(&this.session_id, cx)
+ .map(|resume| resume.run(cx))
+ })?
+ .context("resuming a session is not supported")?
+ .await
+ })
+ }
+
+ fn run_turn(
+ &mut self,
+ cx: &mut Context<Self>,
+ f: impl 'static + AsyncFnOnce(WeakEntity<Self>, &mut AsyncApp) -> Result<acp::PromptResponse>,
+ ) -> BoxFuture<'static, Result<()>> {
self.clear_completed_plan_entries(cx);
- let (old_checkpoint_tx, old_checkpoint_rx) = oneshot::channel();
let (tx, rx) = oneshot::channel();
let cancel_task = self.cancel(cx);
- let request = acp::PromptRequest {
- prompt: message,
- session_id: self.session_id.clone(),
- };
- self.send_task = Some(cx.spawn({
- let message_id = message_id.clone();
- async move |this, cx| {
- cancel_task.await;
-
- old_checkpoint_tx.send(old_checkpoint.await).ok();
- if let Ok(result) = this.update(cx, |this, cx| {
- this.connection.prompt(message_id, request, cx)
- }) {
- tx.send(result.await).log_err();
- }
- }
+ self.send_task = Some(cx.spawn(async move |this, cx| {
+ cancel_task.await;
+ tx.send(f(this, cx).await).ok();
}));
cx.spawn(async move |this, cx| {
- let old_checkpoint = old_checkpoint_rx
- .await
- .map_err(|_| anyhow!("send canceled"))
- .flatten()
- .context("failed to get old checkpoint")
- .log_err();
-
let response = rx.await;
- if let Some((old_checkpoint, message_id)) = old_checkpoint.zip(message_id) {
- let new_checkpoint = git_store
- .update(cx, |git, cx| git.checkpoint(cx))?
- .await
- .context("failed to get new checkpoint")
- .log_err();
- if let Some(new_checkpoint) = new_checkpoint {
- let equal = git_store
- .update(cx, |git, cx| {
- git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
- })?
- .await
- .unwrap_or(true);
- if !equal {
- this.update(cx, |this, cx| {
- if let Some((ix, message)) = this.user_message_mut(&message_id) {
- message.checkpoint = Some(old_checkpoint);
- cx.emit(AcpThreadEvent::EntryUpdated(ix));
- }
- })?;
- }
- }
- }
+ this.update(cx, |this, cx| this.update_last_checkpoint(cx))?
+ .await?;
this.update(cx, |this, cx| {
+ this.project
+ .update(cx, |project, cx| project.set_agent_location(None, cx));
match response {
Ok(Err(e)) => {
this.send_task.take();
@@ -2,13 +2,15 @@ use crate::AcpThread;
use agent_client_protocol::{self as acp};
use anyhow::Result;
use collections::IndexMap;
-use gpui::{AsyncApp, Entity, SharedString, Task};
+use gpui::{Entity, SharedString, Task};
+use language_model::LanguageModelProviderId;
use project::Project;
-use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc};
+use serde::{Deserialize, Serialize};
+use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc};
use ui::{App, IconName};
use uuid::Uuid;
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub struct UserMessageId(Arc<str>);
impl UserMessageId {
@@ -22,7 +24,7 @@ pub trait AgentConnection {
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
- cx: &mut AsyncApp,
+ cx: &mut App,
) -> Task<Result<Entity<AcpThread>>>;
fn auth_methods(&self) -> &[acp::AuthMethod];
@@ -36,13 +38,29 @@ pub trait AgentConnection {
cx: &mut App,
) -> Task<Result<acp::PromptResponse>>;
+ fn resume(
+ &self,
+ _session_id: &acp::SessionId,
+ _cx: &App,
+ ) -> Option<Rc<dyn AgentSessionResume>> {
+ None
+ }
+
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
- fn session_editor(
+ fn truncate(
+ &self,
+ _session_id: &acp::SessionId,
+ _cx: &App,
+ ) -> Option<Rc<dyn AgentSessionTruncate>> {
+ None
+ }
+
+ fn set_title(
&self,
_session_id: &acp::SessionId,
- _cx: &mut App,
- ) -> Option<Rc<dyn AgentSessionEditor>> {
+ _cx: &App,
+ ) -> Option<Rc<dyn AgentSessionSetTitle>> {
None
}
@@ -53,19 +71,90 @@ pub trait AgentConnection {
fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
None
}
+
+ fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
+ None
+ }
+
+ fn session_modes(
+ &self,
+ _session_id: &acp::SessionId,
+ _cx: &App,
+ ) -> Option<Rc<dyn AgentSessionModes>> {
+ None
+ }
+
+ fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
+}
+
+impl dyn AgentConnection {
+ pub fn downcast<T: 'static + AgentConnection + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
+ self.into_any().downcast().ok()
+ }
+}
+
+pub trait AgentSessionTruncate {
+ fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
+}
+
+pub trait AgentSessionResume {
+ fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>;
+}
+
+pub trait AgentSessionSetTitle {
+ fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>>;
+}
+
+pub trait AgentTelemetry {
+ /// The name of the agent used for telemetry.
+ fn agent_name(&self) -> String;
+
+ /// A representation of the current thread state that can be serialized for
+ /// storage with telemetry events.
+ fn thread_data(
+ &self,
+ session_id: &acp::SessionId,
+ cx: &mut App,
+ ) -> Task<Result<serde_json::Value>>;
}
-pub trait AgentSessionEditor {
- fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
+pub trait AgentSessionModes {
+ fn current_mode(&self) -> acp::SessionModeId;
+
+ fn all_modes(&self) -> Vec<acp::SessionMode>;
+
+ fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;
}
#[derive(Debug)]
-pub struct AuthRequired;
+pub struct AuthRequired {
+ pub description: Option<String>,
+ pub provider_id: Option<LanguageModelProviderId>,
+}
+
+impl AuthRequired {
+ pub fn new() -> Self {
+ Self {
+ description: None,
+ provider_id: None,
+ }
+ }
+
+ pub fn with_description(mut self, description: String) -> Self {
+ self.description = Some(description);
+ self
+ }
+
+ pub fn with_language_model_provider(mut self, provider_id: LanguageModelProviderId) -> Self {
+ self.provider_id = Some(provider_id);
+ self
+ }
+}
impl Error for AuthRequired {}
impl fmt::Display for AuthRequired {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "AuthRequired")
+ write!(f, "Authentication required")
}
}
@@ -160,3 +249,228 @@ impl AgentModelList {
}
}
}
+
+#[cfg(feature = "test-support")]
+mod test_support {
+ use std::sync::Arc;
+
+ use action_log::ActionLog;
+ use collections::HashMap;
+ use futures::{channel::oneshot, future::try_join_all};
+ use gpui::{AppContext as _, WeakEntity};
+ use parking_lot::Mutex;
+
+ use super::*;
+
+ #[derive(Clone, Default)]
+ pub struct StubAgentConnection {
+ sessions: Arc<Mutex<HashMap<acp::SessionId, Session>>>,
+ permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
+ next_prompt_updates: Arc<Mutex<Vec<acp::SessionUpdate>>>,
+ }
+
+ struct Session {
+ thread: WeakEntity<AcpThread>,
+ response_tx: Option<oneshot::Sender<acp::StopReason>>,
+ }
+
+ impl StubAgentConnection {
+ pub fn new() -> Self {
+ Self {
+ next_prompt_updates: Default::default(),
+ permission_requests: HashMap::default(),
+ sessions: Arc::default(),
+ }
+ }
+
+ pub fn set_next_prompt_updates(&self, updates: Vec<acp::SessionUpdate>) {
+ *self.next_prompt_updates.lock() = updates;
+ }
+
+ pub fn with_permission_requests(
+ mut self,
+ permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
+ ) -> Self {
+ self.permission_requests = permission_requests;
+ self
+ }
+
+ pub fn send_update(
+ &self,
+ session_id: acp::SessionId,
+ update: acp::SessionUpdate,
+ cx: &mut App,
+ ) {
+ assert!(
+ self.next_prompt_updates.lock().is_empty(),
+ "Use either send_update or set_next_prompt_updates"
+ );
+
+ self.sessions
+ .lock()
+ .get(&session_id)
+ .unwrap()
+ .thread
+ .update(cx, |thread, cx| {
+ thread.handle_session_update(update, cx).unwrap();
+ })
+ .unwrap();
+ }
+
+ pub fn end_turn(&self, session_id: acp::SessionId, stop_reason: acp::StopReason) {
+ self.sessions
+ .lock()
+ .get_mut(&session_id)
+ .unwrap()
+ .response_tx
+ .take()
+ .expect("No pending turn")
+ .send(stop_reason)
+ .unwrap();
+ }
+ }
+
+ impl AgentConnection for StubAgentConnection {
+ fn auth_methods(&self) -> &[acp::AuthMethod] {
+ &[]
+ }
+
+ fn new_thread(
+ self: Rc<Self>,
+ project: Entity<Project>,
+ _cwd: &Path,
+ cx: &mut gpui::App,
+ ) -> Task<gpui::Result<Entity<AcpThread>>> {
+ let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
+ let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ let thread = cx.new(|cx| {
+ AcpThread::new(
+ "Test",
+ self.clone(),
+ project,
+ action_log,
+ session_id.clone(),
+ watch::Receiver::constant(acp::PromptCapabilities {
+ image: true,
+ audio: true,
+ embedded_context: true,
+ }),
+ cx,
+ )
+ });
+ self.sessions.lock().insert(
+ session_id,
+ Session {
+ thread: thread.downgrade(),
+ response_tx: None,
+ },
+ );
+ Task::ready(Ok(thread))
+ }
+
+ fn authenticate(
+ &self,
+ _method_id: acp::AuthMethodId,
+ _cx: &mut App,
+ ) -> Task<gpui::Result<()>> {
+ unimplemented!()
+ }
+
+ fn prompt(
+ &self,
+ _id: Option<UserMessageId>,
+ params: acp::PromptRequest,
+ cx: &mut App,
+ ) -> Task<gpui::Result<acp::PromptResponse>> {
+ let mut sessions = self.sessions.lock();
+ let Session {
+ thread,
+ response_tx,
+ } = sessions.get_mut(¶ms.session_id).unwrap();
+ let mut tasks = vec![];
+ if self.next_prompt_updates.lock().is_empty() {
+ let (tx, rx) = oneshot::channel();
+ response_tx.replace(tx);
+ cx.spawn(async move |_| {
+ let stop_reason = rx.await?;
+ Ok(acp::PromptResponse { stop_reason })
+ })
+ } else {
+ for update in self.next_prompt_updates.lock().drain(..) {
+ let thread = thread.clone();
+ let update = update.clone();
+ let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) =
+ &update
+ && let Some(options) = self.permission_requests.get(&tool_call.id)
+ {
+ Some((tool_call.clone(), options.clone()))
+ } else {
+ None
+ };
+ let task = cx.spawn(async move |cx| {
+ if let Some((tool_call, options)) = permission_request {
+ thread
+ .update(cx, |thread, cx| {
+ thread.request_tool_call_authorization(
+ tool_call.clone().into(),
+ options.clone(),
+ false,
+ cx,
+ )
+ })??
+ .await;
+ }
+ thread.update(cx, |thread, cx| {
+ thread.handle_session_update(update.clone(), cx).unwrap();
+ })?;
+ anyhow::Ok(())
+ });
+ tasks.push(task);
+ }
+
+ cx.spawn(async move |_| {
+ try_join_all(tasks).await?;
+ Ok(acp::PromptResponse {
+ stop_reason: acp::StopReason::EndTurn,
+ })
+ })
+ }
+ }
+
+ fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
+ if let Some(end_turn_tx) = self
+ .sessions
+ .lock()
+ .get_mut(session_id)
+ .unwrap()
+ .response_tx
+ .take()
+ {
+ end_turn_tx.send(acp::StopReason::Cancelled).unwrap();
+ }
+ }
+
+ fn truncate(
+ &self,
+ _session_id: &agent_client_protocol::SessionId,
+ _cx: &App,
+ ) -> Option<Rc<dyn AgentSessionTruncate>> {
+ Some(Rc::new(StubAgentSessionEditor))
+ }
+
+ fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+ self
+ }
+ }
+
+ struct StubAgentSessionEditor;
+
+ impl AgentSessionTruncate for StubAgentSessionEditor {
+ fn run(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
+ Task::ready(Ok(()))
+ }
+ }
+}
+
+#[cfg(feature = "test-support")]
+pub use test_support::*;
@@ -1,7 +1,6 @@
-use agent_client_protocol as acp;
use anyhow::Result;
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
-use editor::{MultiBuffer, PathKey};
+use editor::{MultiBuffer, PathKey, multibuffer_context_lines};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
use itertools::Itertools;
use language::{
@@ -21,69 +20,54 @@ pub enum Diff {
}
impl Diff {
- pub fn from_acp(
- diff: acp::Diff,
+ pub fn finalized(
+ path: PathBuf,
+ old_text: Option<String>,
+ new_text: String,
language_registry: Arc<LanguageRegistry>,
cx: &mut Context<Self>,
) -> Self {
- let acp::Diff {
- path,
- old_text,
- new_text,
- } = diff;
-
let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
-
let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
- let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx));
- let new_buffer_snapshot = new_buffer.read(cx).text_snapshot();
- let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx));
-
+ let base_text = old_text.clone().unwrap_or(String::new()).into();
let task = cx.spawn({
let multibuffer = multibuffer.clone();
let path = path.clone();
+ let buffer = new_buffer.clone();
async move |_, cx| {
let language = language_registry
.language_for_file_path(&path)
.await
.log_err();
- new_buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?;
-
- let old_buffer_snapshot = old_buffer.update(cx, |buffer, cx| {
- buffer.set_language(language, cx);
- buffer.snapshot()
- })?;
+ buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?;
- buffer_diff
- .update(cx, |diff, cx| {
- diff.set_base_text(
- old_buffer_snapshot,
- Some(language_registry),
- new_buffer_snapshot,
- cx,
- )
- })?
- .await?;
+ let diff = build_buffer_diff(
+ old_text.unwrap_or("".into()).into(),
+ &buffer,
+ Some(language_registry.clone()),
+ cx,
+ )
+ .await?;
multibuffer
.update(cx, |multibuffer, cx| {
let hunk_ranges = {
- let buffer = new_buffer.read(cx);
- let diff = buffer_diff.read(cx);
- diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx)
- .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer))
+ let buffer = buffer.read(cx);
+ let diff = diff.read(cx);
+ diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
+ .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::<Vec<_>>()
};
multibuffer.set_excerpts_for_path(
- PathKey::for_buffer(&new_buffer, cx),
- new_buffer.clone(),
+ PathKey::for_buffer(&buffer, cx),
+ buffer.clone(),
hunk_ranges,
- editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ multibuffer_context_lines(cx),
cx,
);
- multibuffer.add_diff(buffer_diff, cx);
+ multibuffer.add_diff(diff, cx);
})
.log_err();
@@ -94,23 +78,26 @@ impl Diff {
Self::Finalized(FinalizedDiff {
multibuffer,
path,
+ base_text,
+ new_buffer,
_update_diff: task,
})
}
pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
- let buffer_snapshot = buffer.read(cx).snapshot();
- let base_text = buffer_snapshot.text();
- let language_registry = buffer.read(cx).language_registry();
- let text_snapshot = buffer.read(cx).text_snapshot();
+ let buffer_text_snapshot = buffer.read(cx).text_snapshot();
+ let base_text_snapshot = buffer.read(cx).snapshot();
+ let base_text = base_text_snapshot.text();
+ debug_assert_eq!(buffer_text_snapshot.text(), base_text);
let buffer_diff = cx.new(|cx| {
- let mut diff = BufferDiff::new(&text_snapshot, cx);
- let _ = diff.set_base_text(
- buffer_snapshot.clone(),
- language_registry,
- text_snapshot,
- cx,
- );
+ let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot);
+ let snapshot = diff.snapshot(cx);
+ let secondary_diff = cx.new(|cx| {
+ let mut diff = BufferDiff::new(&buffer_text_snapshot, cx);
+ diff.set_snapshot(snapshot, &buffer_text_snapshot, cx);
+ diff
+ });
+ diff.set_secondary_diff(secondary_diff);
diff
});
@@ -128,7 +115,7 @@ impl Diff {
diff.update(cx);
}
}),
- buffer,
+ new_buffer: buffer,
diff: buffer_diff,
revealed_ranges: Vec::new(),
update_diff: Task::ready(Ok(())),
@@ -163,9 +150,9 @@ impl Diff {
.map(|buffer| buffer.read(cx).text())
.join("\n");
let path = match self {
- Diff::Pending(PendingDiff { buffer, .. }) => {
- buffer.read(cx).file().map(|file| file.path().as_ref())
- }
+ Diff::Pending(PendingDiff {
+ new_buffer: buffer, ..
+ }) => buffer.read(cx).file().map(|file| file.path().as_ref()),
Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_path()),
};
format!(
@@ -178,12 +165,33 @@ impl Diff {
pub fn has_revealed_range(&self, cx: &App) -> bool {
self.multibuffer().read(cx).excerpt_paths().next().is_some()
}
+
+ pub fn needs_update(&self, old_text: &str, new_text: &str, cx: &App) -> bool {
+ match self {
+ Diff::Pending(PendingDiff {
+ base_text,
+ new_buffer,
+ ..
+ }) => {
+ base_text.as_str() != old_text
+ || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
+ }
+ Diff::Finalized(FinalizedDiff {
+ base_text,
+ new_buffer,
+ ..
+ }) => {
+ base_text.as_str() != old_text
+ || !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
+ }
+ }
+ }
}
pub struct PendingDiff {
multibuffer: Entity<MultiBuffer>,
base_text: Arc<String>,
- buffer: Entity<Buffer>,
+ new_buffer: Entity<Buffer>,
diff: Entity<BufferDiff>,
revealed_ranges: Vec<Range<Anchor>>,
_subscription: Subscription,
@@ -192,7 +200,7 @@ pub struct PendingDiff {
impl PendingDiff {
pub fn update(&mut self, cx: &mut Context<Diff>) {
- let buffer = self.buffer.clone();
+ let buffer = self.new_buffer.clone();
let buffer_diff = self.diff.clone();
let base_text = self.base_text.clone();
self.update_diff = cx.spawn(async move |diff, cx| {
@@ -209,7 +217,10 @@ impl PendingDiff {
)
.await?;
buffer_diff.update(cx, |diff, cx| {
- diff.set_snapshot(diff_snapshot, &text_snapshot, cx)
+ diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
+ diff.secondary_diff().unwrap().update(cx, |diff, cx| {
+ diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
+ });
})?;
diff.update(cx, |diff, cx| {
if let Diff::Pending(diff) = diff {
@@ -227,10 +238,10 @@ impl PendingDiff {
fn finalize(&self, cx: &mut Context<Diff>) -> FinalizedDiff {
let ranges = self.excerpt_ranges(cx);
let base_text = self.base_text.clone();
- let language_registry = self.buffer.read(cx).language_registry().clone();
+ let language_registry = self.new_buffer.read(cx).language_registry();
let path = self
- .buffer
+ .new_buffer
.read(cx)
.file()
.map(|file| file.path().as_ref())
@@ -239,12 +250,12 @@ impl PendingDiff {
// Replace the buffer in the multibuffer with the snapshot
let buffer = cx.new(|cx| {
- let language = self.buffer.read(cx).language().cloned();
+ let language = self.new_buffer.read(cx).language().cloned();
let buffer = TextBuffer::new_normalized(
0,
cx.entity_id().as_non_zero_u64().into(),
- self.buffer.read(cx).line_ending(),
- self.buffer.read(cx).as_rope().clone(),
+ self.new_buffer.read(cx).line_ending(),
+ self.new_buffer.read(cx).as_rope().clone(),
);
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
buffer.set_language(language, cx);
@@ -253,7 +264,6 @@ impl PendingDiff {
let buffer_diff = cx.spawn({
let buffer = buffer.clone();
- let language_registry = language_registry.clone();
async move |_this, cx| {
build_buffer_diff(base_text, &buffer, language_registry, cx).await
}
@@ -269,7 +279,7 @@ impl PendingDiff {
path_key,
buffer,
ranges,
- editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff.clone(), cx);
@@ -281,7 +291,9 @@ impl PendingDiff {
FinalizedDiff {
path,
+ base_text: self.base_text.clone(),
multibuffer: self.multibuffer.clone(),
+ new_buffer: self.new_buffer.clone(),
_update_diff: update_diff,
}
}
@@ -290,10 +302,10 @@ impl PendingDiff {
let ranges = self.excerpt_ranges(cx);
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.set_excerpts_for_path(
- PathKey::for_buffer(&self.buffer, cx),
- self.buffer.clone(),
+ PathKey::for_buffer(&self.new_buffer, cx),
+ self.new_buffer.clone(),
ranges,
- editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ multibuffer_context_lines(cx),
cx,
);
let end = multibuffer.len(cx);
@@ -303,16 +315,16 @@ impl PendingDiff {
}
fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
- let buffer = self.buffer.read(cx);
+ let buffer = self.new_buffer.read(cx);
let diff = self.diff.read(cx);
let mut ranges = diff
- .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx)
- .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer))
+ .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
+ .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::<Vec<_>>();
ranges.extend(
self.revealed_ranges
.iter()
- .map(|range| range.to_point(&buffer)),
+ .map(|range| range.to_point(buffer)),
);
ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end)));
@@ -337,6 +349,8 @@ impl PendingDiff {
pub struct FinalizedDiff {
path: PathBuf,
+ base_text: Arc<String>,
+ new_buffer: Entity<Buffer>,
multibuffer: Entity<MultiBuffer>,
_update_diff: Task<Result<()>>,
}
@@ -390,3 +404,21 @@ async fn build_buffer_diff(
diff
})
}
+
+#[cfg(test)]
+mod tests {
+ use gpui::{AppContext as _, TestAppContext};
+ use language::Buffer;
+
+ use crate::Diff;
+
+ #[gpui::test]
+ async fn test_pending_diff(cx: &mut TestAppContext) {
+ let buffer = cx.new(|cx| Buffer::local("hello!", cx));
+ let _diff = cx.new(|cx| Diff::new(buffer.clone(), cx));
+ buffer.update(cx, |buffer, cx| {
+ buffer.set_text("HELLO!", cx);
+ });
+ cx.run_until_parked();
+ }
+}
@@ -1,23 +1,33 @@
-use agent::ThreadId;
+use agent_client_protocol as acp;
use anyhow::{Context as _, Result, bail};
+use file_icons::FileIcons;
use prompt_store::{PromptId, UserPromptId};
+use serde::{Deserialize, Serialize};
use std::{
fmt,
- ops::Range,
+ ops::RangeInclusive,
path::{Path, PathBuf},
+ str::FromStr,
};
+use ui::{App, IconName, SharedString};
use url::Url;
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum MentionUri {
- File(PathBuf),
+ File {
+ abs_path: PathBuf,
+ },
+ PastedImage,
+ Directory {
+ abs_path: PathBuf,
+ },
Symbol {
- path: PathBuf,
+ abs_path: PathBuf,
name: String,
- line_range: Range<u32>,
+ line_range: RangeInclusive<u32>,
},
Thread {
- id: ThreadId,
+ id: acp::SessionId,
name: String,
},
TextThread {
@@ -29,8 +39,9 @@ pub enum MentionUri {
name: String,
},
Selection {
- path: PathBuf,
- line_range: Range<u32>,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ abs_path: Option<PathBuf>,
+ line_range: RangeInclusive<u32>,
},
Fetch {
url: Url,
@@ -39,51 +50,56 @@ pub enum MentionUri {
impl MentionUri {
pub fn parse(input: &str) -> Result<Self> {
+ fn parse_line_range(fragment: &str) -> Result<RangeInclusive<u32>> {
+ let range = fragment
+ .strip_prefix("L")
+ .context("Line range must start with \"L\"")?;
+ let (start, end) = range
+ .split_once(":")
+ .context("Line range must use colon as separator")?;
+ let range = start
+ .parse::<u32>()
+ .context("Parsing line range start")?
+ .checked_sub(1)
+ .context("Line numbers should be 1-based")?
+ ..=end
+ .parse::<u32>()
+ .context("Parsing line range end")?
+ .checked_sub(1)
+ .context("Line numbers should be 1-based")?;
+ Ok(range)
+ }
+
let url = url::Url::parse(input)?;
let path = url.path();
match url.scheme() {
"file" => {
+ let path = url.to_file_path().ok().context("Extracting file path")?;
if let Some(fragment) = url.fragment() {
- let range = fragment
- .strip_prefix("L")
- .context("Line range must start with \"L\"")?;
- let (start, end) = range
- .split_once(":")
- .context("Line range must use colon as separator")?;
- let line_range = start
- .parse::<u32>()
- .context("Parsing line range start")?
- .checked_sub(1)
- .context("Line numbers should be 1-based")?
- ..end
- .parse::<u32>()
- .context("Parsing line range end")?
- .checked_sub(1)
- .context("Line numbers should be 1-based")?;
+ let line_range = parse_line_range(fragment)?;
if let Some(name) = single_query_param(&url, "symbol")? {
Ok(Self::Symbol {
name,
- path: path.into(),
+ abs_path: path,
line_range,
})
} else {
Ok(Self::Selection {
- path: path.into(),
+ abs_path: Some(path),
line_range,
})
}
+ } else if input.ends_with("/") {
+ Ok(Self::Directory { abs_path: path })
} else {
- let file_path =
- PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
-
- Ok(Self::File(file_path))
+ Ok(Self::File { abs_path: path })
}
}
"zed" => {
if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
let name = single_query_param(&url, "name")?.context("Missing thread name")?;
Ok(Self::Thread {
- id: thread_id.into(),
+ id: acp::SessionId(thread_id.into()),
name,
})
} else if let Some(path) = path.strip_prefix("/agent/text-thread/") {
@@ -99,6 +115,17 @@ impl MentionUri {
id: rule_id.into(),
name,
})
+ } else if path.starts_with("/agent/pasted-image") {
+ Ok(Self::PastedImage)
+ } else if path.starts_with("/agent/untitled-buffer") {
+ let fragment = url
+ .fragment()
+ .context("Missing fragment for untitled buffer selection")?;
+ let line_range = parse_line_range(fragment)?;
+ Ok(Self::Selection {
+ abs_path: None,
+ line_range,
+ })
} else {
bail!("invalid zed url: {:?}", input);
}
@@ -108,57 +135,87 @@ impl MentionUri {
}
}
- fn name(&self) -> String {
+ pub fn name(&self) -> String {
match self {
- MentionUri::File(path) => path
+ MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => abs_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
+ MentionUri::PastedImage => "Image".to_string(),
MentionUri::Symbol { name, .. } => name.clone(),
MentionUri::Thread { name, .. } => name.clone(),
MentionUri::TextThread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(),
MentionUri::Selection {
- path, line_range, ..
- } => selection_name(path, line_range),
+ abs_path: path,
+ line_range,
+ ..
+ } => selection_name(path.as_deref(), line_range),
MentionUri::Fetch { url } => url.to_string(),
}
}
+ pub fn icon_path(&self, cx: &mut App) -> SharedString {
+ match self {
+ MentionUri::File { abs_path } => {
+ FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
+ }
+ MentionUri::PastedImage => IconName::Image.path().into(),
+ MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
+ .unwrap_or_else(|| IconName::Folder.path().into()),
+ MentionUri::Symbol { .. } => IconName::Code.path().into(),
+ MentionUri::Thread { .. } => IconName::Thread.path().into(),
+ MentionUri::TextThread { .. } => IconName::Thread.path().into(),
+ MentionUri::Rule { .. } => IconName::Reader.path().into(),
+ MentionUri::Selection { .. } => IconName::Reader.path().into(),
+ MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(),
+ }
+ }
+
pub fn as_link<'a>(&'a self) -> MentionLink<'a> {
MentionLink(self)
}
pub fn to_uri(&self) -> Url {
match self {
- MentionUri::File(path) => {
- let mut url = Url::parse("file:///").unwrap();
- url.set_path(&path.to_string_lossy());
- url
+ MentionUri::File { abs_path } => {
+ Url::from_file_path(abs_path).expect("mention path should be absolute")
+ }
+ MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
+ MentionUri::Directory { abs_path } => {
+ Url::from_directory_path(abs_path).expect("mention path should be absolute")
}
MentionUri::Symbol {
- path,
+ abs_path,
name,
line_range,
} => {
- let mut url = Url::parse("file:///").unwrap();
- url.set_path(&path.to_string_lossy());
+ let mut url =
+ Url::from_file_path(abs_path).expect("mention path should be absolute");
url.query_pairs_mut().append_pair("symbol", name);
url.set_fragment(Some(&format!(
"L{}:{}",
- line_range.start + 1,
- line_range.end + 1
+ line_range.start() + 1,
+ line_range.end() + 1
)));
url
}
- MentionUri::Selection { path, line_range } => {
- let mut url = Url::parse("file:///").unwrap();
- url.set_path(&path.to_string_lossy());
+ MentionUri::Selection {
+ abs_path: path,
+ line_range,
+ } => {
+ let mut url = if let Some(path) = path {
+ Url::from_file_path(path).expect("mention path should be absolute")
+ } else {
+ let mut url = Url::parse("zed:///").unwrap();
+ url.set_path("/agent/untitled-buffer");
+ url
+ };
url.set_fragment(Some(&format!(
"L{}:{}",
- line_range.start + 1,
- line_range.end + 1
+ line_range.start() + 1,
+ line_range.end() + 1
)));
url
}
@@ -170,7 +227,10 @@ impl MentionUri {
}
MentionUri::TextThread { path, name } => {
let mut url = Url::parse("zed:///").unwrap();
- url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy()));
+ url.set_path(&format!(
+ "/agent/text-thread/{}",
+ path.to_string_lossy().trim_start_matches('/')
+ ));
url.query_pairs_mut().append_pair("name", name);
url
}
@@ -185,6 +245,14 @@ impl MentionUri {
}
}
+impl FromStr for MentionUri {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> anyhow::Result<Self> {
+ Self::parse(s)
+ }
+}
+
pub struct MentionLink<'a>(&'a MentionUri);
impl fmt::Display for MentionLink<'_> {
@@ -208,44 +276,81 @@ fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
}
}
-pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
+pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive<u32>) -> String {
format!(
"{} ({}:{})",
- path.file_name().unwrap_or_default().display(),
- line_range.start + 1,
- line_range.end + 1
+ path.and_then(|path| path.file_name())
+ .unwrap_or("Untitled".as_ref())
+ .display(),
+ *line_range.start() + 1,
+ *line_range.end() + 1
)
}
#[cfg(test)]
mod tests {
+ use util::{path, uri};
+
use super::*;
#[test]
fn test_parse_file_uri() {
- let file_uri = "file:///path/to/file.rs";
+ let file_uri = uri!("file:///path/to/file.rs");
let parsed = MentionUri::parse(file_uri).unwrap();
match &parsed {
- MentionUri::File(path) => assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"),
+ MentionUri::File { abs_path } => {
+ assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/file.rs"));
+ }
_ => panic!("Expected File variant"),
}
assert_eq!(parsed.to_uri().to_string(), file_uri);
}
+ #[test]
+ fn test_parse_directory_uri() {
+ let file_uri = uri!("file:///path/to/dir/");
+ let parsed = MentionUri::parse(file_uri).unwrap();
+ match &parsed {
+ MentionUri::Directory { abs_path } => {
+ assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/dir/"));
+ }
+ _ => panic!("Expected Directory variant"),
+ }
+ assert_eq!(parsed.to_uri().to_string(), file_uri);
+ }
+
+ #[test]
+ fn test_to_directory_uri_with_slash() {
+ let uri = MentionUri::Directory {
+ abs_path: PathBuf::from(path!("/path/to/dir/")),
+ };
+ let expected = uri!("file:///path/to/dir/");
+ assert_eq!(uri.to_uri().to_string(), expected);
+ }
+
+ #[test]
+ fn test_to_directory_uri_without_slash() {
+ let uri = MentionUri::Directory {
+ abs_path: PathBuf::from(path!("/path/to/dir")),
+ };
+ let expected = uri!("file:///path/to/dir/");
+ assert_eq!(uri.to_uri().to_string(), expected);
+ }
+
#[test]
fn test_parse_symbol_uri() {
- let symbol_uri = "file:///path/to/file.rs?symbol=MySymbol#L10:20";
+ let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
let parsed = MentionUri::parse(symbol_uri).unwrap();
match &parsed {
MentionUri::Symbol {
- path,
+ abs_path: path,
name,
line_range,
} => {
- assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
+ assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
assert_eq!(name, "MySymbol");
- assert_eq!(line_range.start, 9);
- assert_eq!(line_range.end, 19);
+ assert_eq!(line_range.start(), &9);
+ assert_eq!(line_range.end(), &19);
}
_ => panic!("Expected Symbol variant"),
}
@@ -254,19 +359,42 @@ mod tests {
#[test]
fn test_parse_selection_uri() {
- let selection_uri = "file:///path/to/file.rs#L5:15";
+ let selection_uri = uri!("file:///path/to/file.rs#L5:15");
let parsed = MentionUri::parse(selection_uri).unwrap();
match &parsed {
- MentionUri::Selection { path, line_range } => {
- assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
- assert_eq!(line_range.start, 4);
- assert_eq!(line_range.end, 14);
+ MentionUri::Selection {
+ abs_path: path,
+ line_range,
+ } => {
+ assert_eq!(
+ path.as_ref().unwrap().to_str().unwrap(),
+ path!("/path/to/file.rs")
+ );
+ assert_eq!(line_range.start(), &4);
+ assert_eq!(line_range.end(), &14);
}
_ => panic!("Expected Selection variant"),
}
assert_eq!(parsed.to_uri().to_string(), selection_uri);
}
+ #[test]
+ fn test_parse_untitled_selection_uri() {
+ let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10");
+ let parsed = MentionUri::parse(selection_uri).unwrap();
+ match &parsed {
+ MentionUri::Selection {
+ abs_path: None,
+ line_range,
+ } => {
+ assert_eq!(line_range.start(), &0);
+ assert_eq!(line_range.end(), &9);
+ }
+ _ => panic!("Expected Selection variant without path"),
+ }
+ assert_eq!(parsed.to_uri().to_string(), selection_uri);
+ }
+
#[test]
fn test_parse_thread_uri() {
let thread_uri = "zed:///agent/thread/session123?name=Thread+name";
@@ -340,32 +468,35 @@ mod tests {
#[test]
fn test_invalid_line_range_format() {
// Missing L prefix
- assert!(MentionUri::parse("file:///path/to/file.rs#10:20").is_err());
+ assert!(MentionUri::parse(uri!("file:///path/to/file.rs#10:20")).is_err());
// Missing colon separator
- assert!(MentionUri::parse("file:///path/to/file.rs#L1020").is_err());
+ assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1020")).is_err());
// Invalid numbers
- assert!(MentionUri::parse("file:///path/to/file.rs#L10:abc").is_err());
- assert!(MentionUri::parse("file:///path/to/file.rs#Labc:20").is_err());
+ assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:abc")).is_err());
+ assert!(MentionUri::parse(uri!("file:///path/to/file.rs#Labc:20")).is_err());
}
#[test]
fn test_invalid_query_parameters() {
// Invalid query parameter name
- assert!(MentionUri::parse("file:///path/to/file.rs#L10:20?invalid=test").is_err());
+ assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:20?invalid=test")).is_err());
// Too many query parameters
assert!(
- MentionUri::parse("file:///path/to/file.rs#L10:20?symbol=test&another=param").is_err()
+ MentionUri::parse(uri!(
+ "file:///path/to/file.rs#L10:20?symbol=test&another=param"
+ ))
+ .is_err()
);
}
#[test]
fn test_zero_based_line_numbers() {
// Test that 0-based line numbers are rejected (should be 1-based)
- assert!(MentionUri::parse("file:///path/to/file.rs#L0:10").is_err());
- assert!(MentionUri::parse("file:///path/to/file.rs#L1:0").is_err());
- assert!(MentionUri::parse("file:///path/to/file.rs#L0:0").is_err());
+ assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:10")).is_err());
+ assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1:0")).is_err());
+ assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:0")).is_err());
}
}
@@ -1,34 +1,43 @@
-use gpui::{App, AppContext, Context, Entity};
+use agent_client_protocol as acp;
+
+use futures::{FutureExt as _, future::Shared};
+use gpui::{App, AppContext, Context, Entity, Task};
use language::LanguageRegistry;
use markdown::Markdown;
use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
pub struct Terminal {
+ id: acp::TerminalId,
command: Entity<Markdown>,
working_dir: Option<PathBuf>,
terminal: Entity<terminal::Terminal>,
started_at: Instant,
output: Option<TerminalOutput>,
+ output_byte_limit: Option<usize>,
+ _output_task: Shared<Task<acp::TerminalExitStatus>>,
}
pub struct TerminalOutput {
pub ended_at: Instant,
pub exit_status: Option<ExitStatus>,
- pub was_content_truncated: bool,
+ pub content: String,
pub original_content_len: usize,
pub content_line_count: usize,
- pub finished_with_empty_output: bool,
}
impl Terminal {
pub fn new(
+ id: acp::TerminalId,
command: String,
working_dir: Option<PathBuf>,
+ output_byte_limit: Option<usize>,
terminal: Entity<terminal::Terminal>,
language_registry: Arc<LanguageRegistry>,
cx: &mut Context<Self>,
) -> Self {
+ let command_task = terminal.read(cx).wait_for_completed_task(cx);
Self {
+ id,
command: cx.new(|cx| {
Markdown::new(
format!("```\n{}\n```", command).into(),
@@ -41,27 +50,93 @@ impl Terminal {
terminal,
started_at: Instant::now(),
output: None,
+ output_byte_limit,
+ _output_task: cx
+ .spawn(async move |this, cx| {
+ let exit_status = command_task.await;
+
+ this.update(cx, |this, cx| {
+ let (content, original_content_len) = this.truncated_output(cx);
+ let content_line_count = this.terminal.read(cx).total_lines();
+
+ this.output = Some(TerminalOutput {
+ ended_at: Instant::now(),
+ exit_status,
+ content,
+ original_content_len,
+ content_line_count,
+ });
+ cx.notify();
+ })
+ .ok();
+
+ let exit_status = exit_status.map(portable_pty::ExitStatus::from);
+
+ acp::TerminalExitStatus {
+ exit_code: exit_status.as_ref().map(|e| e.exit_code()),
+ signal: exit_status.and_then(|e| e.signal().map(Into::into)),
+ }
+ })
+ .shared(),
}
}
- pub fn finish(
- &mut self,
- exit_status: Option<ExitStatus>,
- original_content_len: usize,
- truncated_content_len: usize,
- content_line_count: usize,
- finished_with_empty_output: bool,
- cx: &mut Context<Self>,
- ) {
- self.output = Some(TerminalOutput {
- ended_at: Instant::now(),
- exit_status,
- was_content_truncated: truncated_content_len < original_content_len,
- original_content_len,
- content_line_count,
- finished_with_empty_output,
+ pub fn id(&self) -> &acp::TerminalId {
+ &self.id
+ }
+
+ pub fn wait_for_exit(&self) -> Shared<Task<acp::TerminalExitStatus>> {
+ self._output_task.clone()
+ }
+
+ pub fn kill(&mut self, cx: &mut App) {
+ self.terminal.update(cx, |terminal, _cx| {
+ terminal.kill_active_task();
});
- cx.notify();
+ }
+
+ pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
+ if let Some(output) = self.output.as_ref() {
+ let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
+
+ acp::TerminalOutputResponse {
+ output: output.content.clone(),
+ truncated: output.original_content_len > output.content.len(),
+ exit_status: Some(acp::TerminalExitStatus {
+ exit_code: exit_status.as_ref().map(|e| e.exit_code()),
+ signal: exit_status.and_then(|e| e.signal().map(Into::into)),
+ }),
+ }
+ } else {
+ let (current_content, original_len) = self.truncated_output(cx);
+
+ acp::TerminalOutputResponse {
+ truncated: current_content.len() < original_len,
+ output: current_content,
+ exit_status: None,
+ }
+ }
+ }
+
+ fn truncated_output(&self, cx: &App) -> (String, usize) {
+ let terminal = self.terminal.read(cx);
+ let mut content = terminal.get_content();
+
+ let original_content_len = content.len();
+
+ if let Some(limit) = self.output_byte_limit
+ && content.len() > limit
+ {
+ let mut end_ix = limit.min(content.len());
+ while !content.is_char_boundary(end_ix) {
+ end_ix -= 1;
+ }
+ // Don't truncate mid-line, clear the remainder of the last line
+ end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
+ content.truncate(end_ix);
+ }
+
+ (content, original_content_len)
}
pub fn command(&self) -> &Entity<Markdown> {
@@ -0,0 +1,30 @@
+[package]
+name = "acp_tools"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/acp_tools.rs"
+doctest = false
+
+[dependencies]
+agent-client-protocol.workspace = true
+collections.workspace = true
+gpui.workspace = true
+language.workspace= true
+markdown.workspace = true
+project.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+settings.workspace = true
+theme.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace-hack.workspace = true
+workspace.workspace = true
@@ -0,0 +1,494 @@
+use std::{
+ cell::RefCell,
+ collections::HashSet,
+ fmt::Display,
+ rc::{Rc, Weak},
+ sync::Arc,
+};
+
+use agent_client_protocol as acp;
+use collections::HashMap;
+use gpui::{
+ App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
+ StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
+};
+use language::LanguageRegistry;
+use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
+use project::Project;
+use settings::Settings;
+use theme::ThemeSettings;
+use ui::prelude::*;
+use util::ResultExt as _;
+use workspace::{Item, Workspace};
+
+actions!(dev, [OpenAcpLogs]);
+
+pub fn init(cx: &mut App) {
+ cx.observe_new(
+ |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
+ workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| {
+ let acp_tools =
+ Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx)));
+ workspace.add_item_to_active_pane(acp_tools, None, true, window, cx);
+ });
+ },
+ )
+ .detach();
+}
+
+struct GlobalAcpConnectionRegistry(Entity<AcpConnectionRegistry>);
+
+impl Global for GlobalAcpConnectionRegistry {}
+
+#[derive(Default)]
+pub struct AcpConnectionRegistry {
+ active_connection: RefCell<Option<ActiveConnection>>,
+}
+
+struct ActiveConnection {
+ server_name: SharedString,
+ connection: Weak<acp::ClientSideConnection>,
+}
+
+impl AcpConnectionRegistry {
+ pub fn default_global(cx: &mut App) -> Entity<Self> {
+ if cx.has_global::<GlobalAcpConnectionRegistry>() {
+ cx.global::<GlobalAcpConnectionRegistry>().0.clone()
+ } else {
+ let registry = cx.new(|_cx| AcpConnectionRegistry::default());
+ cx.set_global(GlobalAcpConnectionRegistry(registry.clone()));
+ registry
+ }
+ }
+
+ pub fn set_active_connection(
+ &self,
+ server_name: impl Into<SharedString>,
+ connection: &Rc<acp::ClientSideConnection>,
+ cx: &mut Context<Self>,
+ ) {
+ self.active_connection.replace(Some(ActiveConnection {
+ server_name: server_name.into(),
+ connection: Rc::downgrade(connection),
+ }));
+ cx.notify();
+ }
+}
+
+struct AcpTools {
+ project: Entity<Project>,
+ focus_handle: FocusHandle,
+ expanded: HashSet<usize>,
+ watched_connection: Option<WatchedConnection>,
+ connection_registry: Entity<AcpConnectionRegistry>,
+ _subscription: Subscription,
+}
+
+struct WatchedConnection {
+ server_name: SharedString,
+ messages: Vec<WatchedConnectionMessage>,
+ list_state: ListState,
+ connection: Weak<acp::ClientSideConnection>,
+ incoming_request_methods: HashMap<i32, Arc<str>>,
+ outgoing_request_methods: HashMap<i32, Arc<str>>,
+ _task: Task<()>,
+}
+
+impl AcpTools {
+ fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
+ let connection_registry = AcpConnectionRegistry::default_global(cx);
+
+ let subscription = cx.observe(&connection_registry, |this, _, cx| {
+ this.update_connection(cx);
+ cx.notify();
+ });
+
+ let mut this = Self {
+ project,
+ focus_handle: cx.focus_handle(),
+ expanded: HashSet::default(),
+ watched_connection: None,
+ connection_registry,
+ _subscription: subscription,
+ };
+ this.update_connection(cx);
+ this
+ }
+
+ fn update_connection(&mut self, cx: &mut Context<Self>) {
+ let active_connection = self.connection_registry.read(cx).active_connection.borrow();
+ let Some(active_connection) = active_connection.as_ref() else {
+ return;
+ };
+
+ if let Some(watched_connection) = self.watched_connection.as_ref() {
+ if Weak::ptr_eq(
+ &watched_connection.connection,
+ &active_connection.connection,
+ ) {
+ return;
+ }
+ }
+
+ if let Some(connection) = active_connection.connection.upgrade() {
+ let mut receiver = connection.subscribe();
+ let task = cx.spawn(async move |this, cx| {
+ while let Ok(message) = receiver.recv().await {
+ this.update(cx, |this, cx| {
+ this.push_stream_message(message, cx);
+ })
+ .ok();
+ }
+ });
+
+ self.watched_connection = Some(WatchedConnection {
+ server_name: active_connection.server_name.clone(),
+ messages: vec![],
+ list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),
+ connection: active_connection.connection.clone(),
+ incoming_request_methods: HashMap::default(),
+ outgoing_request_methods: HashMap::default(),
+ _task: task,
+ });
+ }
+ }
+
+ fn push_stream_message(&mut self, stream_message: acp::StreamMessage, cx: &mut Context<Self>) {
+ let Some(connection) = self.watched_connection.as_mut() else {
+ return;
+ };
+ let language_registry = self.project.read(cx).languages().clone();
+ let index = connection.messages.len();
+
+ let (request_id, method, message_type, params) = match stream_message.message {
+ acp::StreamMessageContent::Request { id, method, params } => {
+ let method_map = match stream_message.direction {
+ acp::StreamMessageDirection::Incoming => {
+ &mut connection.incoming_request_methods
+ }
+ acp::StreamMessageDirection::Outgoing => {
+ &mut connection.outgoing_request_methods
+ }
+ };
+
+ method_map.insert(id, method.clone());
+ (Some(id), method.into(), MessageType::Request, Ok(params))
+ }
+ acp::StreamMessageContent::Response { id, result } => {
+ let method_map = match stream_message.direction {
+ acp::StreamMessageDirection::Incoming => {
+ &mut connection.outgoing_request_methods
+ }
+ acp::StreamMessageDirection::Outgoing => {
+ &mut connection.incoming_request_methods
+ }
+ };
+
+ if let Some(method) = method_map.remove(&id) {
+ (Some(id), method.into(), MessageType::Response, result)
+ } else {
+ (
+ Some(id),
+ "[unrecognized response]".into(),
+ MessageType::Response,
+ result,
+ )
+ }
+ }
+ acp::StreamMessageContent::Notification { method, params } => {
+ (None, method.into(), MessageType::Notification, Ok(params))
+ }
+ };
+
+ let message = WatchedConnectionMessage {
+ name: method,
+ message_type,
+ request_id,
+ direction: stream_message.direction,
+ collapsed_params_md: match params.as_ref() {
+ Ok(params) => params
+ .as_ref()
+ .map(|params| collapsed_params_md(params, &language_registry, cx)),
+ Err(err) => {
+ if let Ok(err) = &serde_json::to_value(err) {
+ Some(collapsed_params_md(&err, &language_registry, cx))
+ } else {
+ None
+ }
+ }
+ },
+
+ expanded_params_md: None,
+ params,
+ };
+
+ connection.messages.push(message);
+ connection.list_state.splice(index..index, 1);
+ cx.notify();
+ }
+
+ fn render_message(
+ &mut self,
+ index: usize,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> AnyElement {
+ let Some(connection) = self.watched_connection.as_ref() else {
+ return Empty.into_any();
+ };
+
+ let Some(message) = connection.messages.get(index) else {
+ return Empty.into_any();
+ };
+
+ let base_size = TextSize::Editor.rems(cx);
+
+ let theme_settings = ThemeSettings::get_global(cx);
+ let text_style = window.text_style();
+
+ let colors = cx.theme().colors();
+ let expanded = self.expanded.contains(&index);
+
+ v_flex()
+ .w_full()
+ .px_4()
+ .py_3()
+ .border_color(colors.border)
+ .border_b_1()
+ .gap_2()
+ .items_start()
+ .font_buffer(cx)
+ .text_size(base_size)
+ .id(index)
+ .group("message")
+ .hover(|this| this.bg(colors.element_background.opacity(0.5)))
+ .on_click(cx.listener(move |this, _, _, cx| {
+ if this.expanded.contains(&index) {
+ this.expanded.remove(&index);
+ } else {
+ this.expanded.insert(index);
+ let Some(connection) = &mut this.watched_connection else {
+ return;
+ };
+ let Some(message) = connection.messages.get_mut(index) else {
+ return;
+ };
+ message.expanded(this.project.read(cx).languages().clone(), cx);
+ connection.list_state.scroll_to_reveal_item(index);
+ }
+ cx.notify()
+ }))
+ .child(
+ h_flex()
+ .w_full()
+ .gap_2()
+ .items_center()
+ .flex_shrink_0()
+ .child(match message.direction {
+ acp::StreamMessageDirection::Incoming => {
+ ui::Icon::new(ui::IconName::ArrowDown).color(Color::Error)
+ }
+ acp::StreamMessageDirection::Outgoing => {
+ ui::Icon::new(ui::IconName::ArrowUp).color(Color::Success)
+ }
+ })
+ .child(
+ Label::new(message.name.clone())
+ .buffer_font(cx)
+ .color(Color::Muted),
+ )
+ .child(div().flex_1())
+ .child(
+ div()
+ .child(ui::Chip::new(message.message_type.to_string()))
+ .visible_on_hover("message"),
+ )
+ .children(
+ message
+ .request_id
+ .map(|req_id| div().child(ui::Chip::new(req_id.to_string()))),
+ ),
+ )
+ // I'm aware using markdown is a hack. Trying to get something working for the demo.
+ // Will clean up soon!
+ .when_some(
+ if expanded {
+ message.expanded_params_md.clone()
+ } else {
+ message.collapsed_params_md.clone()
+ },
+ |this, params| {
+ this.child(
+ div().pl_6().w_full().child(
+ MarkdownElement::new(
+ params,
+ MarkdownStyle {
+ base_text_style: text_style,
+ selection_background_color: colors.element_selection_background,
+ syntax: cx.theme().syntax().clone(),
+ code_block_overflow_x_scroll: true,
+ code_block: StyleRefinement {
+ text: Some(TextStyleRefinement {
+ font_family: Some(
+ theme_settings.buffer_font.family.clone(),
+ ),
+ font_size: Some((base_size * 0.8).into()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ )
+ .code_block_renderer(
+ CodeBlockRenderer::Default {
+ copy_button: false,
+ copy_button_on_hover: expanded,
+ border: false,
+ },
+ ),
+ ),
+ )
+ },
+ )
+ .into_any()
+ }
+}
+
+struct WatchedConnectionMessage {
+ name: SharedString,
+ request_id: Option<i32>,
+ direction: acp::StreamMessageDirection,
+ message_type: MessageType,
+ params: Result<Option<serde_json::Value>, acp::Error>,
+ collapsed_params_md: Option<Entity<Markdown>>,
+ expanded_params_md: Option<Entity<Markdown>>,
+}
+
+impl WatchedConnectionMessage {
+ fn expanded(&mut self, language_registry: Arc<LanguageRegistry>, cx: &mut App) {
+ let params_md = match &self.params {
+ Ok(Some(params)) => Some(expanded_params_md(params, &language_registry, cx)),
+ Err(err) => {
+ if let Some(err) = &serde_json::to_value(err).log_err() {
+ Some(expanded_params_md(&err, &language_registry, cx))
+ } else {
+ None
+ }
+ }
+ _ => None,
+ };
+ self.expanded_params_md = params_md;
+ }
+}
+
+fn collapsed_params_md(
+ params: &serde_json::Value,
+ language_registry: &Arc<LanguageRegistry>,
+ cx: &mut App,
+) -> Entity<Markdown> {
+ let params_json = serde_json::to_string(params).unwrap_or_default();
+ let mut spaced_out_json = String::with_capacity(params_json.len() + params_json.len() / 4);
+
+ for ch in params_json.chars() {
+ match ch {
+ '{' => spaced_out_json.push_str("{ "),
+ '}' => spaced_out_json.push_str(" }"),
+ ':' => spaced_out_json.push_str(": "),
+ ',' => spaced_out_json.push_str(", "),
+ c => spaced_out_json.push(c),
+ }
+ }
+
+ let params_md = format!("```json\n{}\n```", spaced_out_json);
+ cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
+}
+
+fn expanded_params_md(
+ params: &serde_json::Value,
+ language_registry: &Arc<LanguageRegistry>,
+ cx: &mut App,
+) -> Entity<Markdown> {
+ let params_json = serde_json::to_string_pretty(params).unwrap_or_default();
+ let params_md = format!("```json\n{}\n```", params_json);
+ cx.new(|cx| Markdown::new(params_md.into(), Some(language_registry.clone()), None, cx))
+}
+
+enum MessageType {
+ Request,
+ Response,
+ Notification,
+}
+
+impl Display for MessageType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ MessageType::Request => write!(f, "Request"),
+ MessageType::Response => write!(f, "Response"),
+ MessageType::Notification => write!(f, "Notification"),
+ }
+ }
+}
+
+enum AcpToolsEvent {}
+
+impl EventEmitter<AcpToolsEvent> for AcpTools {}
+
+impl Item for AcpTools {
+ type Event = AcpToolsEvent;
+
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
+ format!(
+ "ACP: {}",
+ self.watched_connection
+ .as_ref()
+ .map_or("Disconnected", |connection| &connection.server_name)
+ )
+ .into()
+ }
+
+ fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
+ Some(ui::Icon::new(IconName::Thread))
+ }
+}
+
+impl Focusable for AcpTools {
+ fn focus_handle(&self, _cx: &App) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl Render for AcpTools {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ v_flex()
+ .track_focus(&self.focus_handle)
+ .size_full()
+ .bg(cx.theme().colors().editor_background)
+ .child(match self.watched_connection.as_ref() {
+ Some(connection) => {
+ if connection.messages.is_empty() {
+ h_flex()
+ .size_full()
+ .justify_center()
+ .items_center()
+ .child("No messages recorded yet")
+ .into_any()
+ } else {
+ list(
+ connection.list_state.clone(),
+ cx.processor(Self::render_message),
+ )
+ .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
+ .flex_grow()
+ .into_any()
+ }
+ }
+ None => h_flex()
+ .size_full()
+ .justify_center()
+ .items_center()
+ .child("No active connection")
+ .into_any(),
+ })
+ }
+}
@@ -116,7 +116,7 @@ impl ActionLog {
} else if buffer
.read(cx)
.file()
- .map_or(false, |file| file.disk_state().exists())
+ .is_some_and(|file| file.disk_state().exists())
{
TrackedBufferStatus::Created {
existing_file_content: Some(buffer.read(cx).as_rope().clone()),
@@ -161,7 +161,7 @@ impl ActionLog {
diff_base,
last_seen_base,
unreviewed_edits,
- snapshot: text_snapshot.clone(),
+ snapshot: text_snapshot,
status,
version: buffer.read(cx).version(),
diff,
@@ -190,7 +190,7 @@ impl ActionLog {
cx: &mut Context<Self>,
) {
match event {
- BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx),
+ BufferEvent::Edited => self.handle_buffer_edited(buffer, cx),
BufferEvent::FileHandleChanged => {
self.handle_buffer_file_changed(buffer, cx);
}
@@ -215,7 +215,7 @@ impl ActionLog {
if buffer
.read(cx)
.file()
- .map_or(false, |file| file.disk_state() == DiskState::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.
@@ -227,7 +227,7 @@ impl ActionLog {
if buffer
.read(cx)
.file()
- .map_or(false, |file| file.disk_state() != DiskState::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
@@ -264,15 +264,14 @@ impl ActionLog {
if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) {
cx.update(|cx| {
let mut old_head = buffer_repo.read(cx).head_commit.clone();
- Some(cx.subscribe(git_diff, move |_, event, cx| match event {
- buffer_diff::BufferDiffEvent::DiffChanged { .. } => {
+ Some(cx.subscribe(git_diff, move |_, event, cx| {
+ if let buffer_diff::BufferDiffEvent::DiffChanged { .. } = event {
let new_head = buffer_repo.read(cx).head_commit.clone();
if new_head != old_head {
old_head = new_head;
git_diff_updates_tx.send(()).ok();
}
}
- _ => {}
}))
})?
} else {
@@ -290,7 +289,7 @@ impl ActionLog {
}
_ = git_diff_updates_rx.changed().fuse() => {
if let Some(git_diff) = git_diff.as_ref() {
- Self::keep_committed_edits(&this, &buffer, &git_diff, cx).await?;
+ Self::keep_committed_edits(&this, &buffer, git_diff, cx).await?;
}
}
}
@@ -462,7 +461,7 @@ impl ActionLog {
anyhow::Ok((
tracked_buffer.diff.clone(),
buffer.read(cx).language().cloned(),
- buffer.read(cx).language_registry().clone(),
+ buffer.read(cx).language_registry(),
))
})??;
let diff_snapshot = BufferDiff::update_diff(
@@ -498,7 +497,7 @@ impl ActionLog {
new: new_range,
},
&new_diff_base,
- &buffer_snapshot.as_rope(),
+ buffer_snapshot.as_rope(),
));
}
unreviewed_edits
@@ -530,12 +529,12 @@ impl ActionLog {
/// Mark a buffer as created by agent, so we can refresh it in the context
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
- self.track_buffer_internal(buffer.clone(), true, cx);
+ self.track_buffer_internal(buffer, true, cx);
}
/// Mark a buffer as edited by agent, so we can refresh it in the context
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
- let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx);
+ let tracked_buffer = self.track_buffer_internal(buffer, false, cx);
if let TrackedBufferStatus::Deleted = tracked_buffer.status {
tracked_buffer.status = TrackedBufferStatus::Modified;
}
@@ -614,10 +613,10 @@ impl ActionLog {
false
}
});
- if tracked_buffer.unreviewed_edits.is_empty() {
- if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
- tracked_buffer.status = TrackedBufferStatus::Modified;
- }
+ if tracked_buffer.unreviewed_edits.is_empty()
+ && let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status
+ {
+ tracked_buffer.status = TrackedBufferStatus::Modified;
}
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
}
@@ -811,7 +810,7 @@ impl ActionLog {
tracked.version != buffer.version
&& buffer
.file()
- .map_or(false, |file| file.disk_state() != DiskState::Deleted)
+ .is_some_and(|file| file.disk_state() != DiskState::Deleted)
})
.map(|(buffer, _)| buffer)
}
@@ -847,7 +846,7 @@ fn apply_non_conflicting_edits(
conflict = true;
if new_edits
.peek()
- .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new))
+ .is_some_and(|next_edit| next_edit.old.overlaps(&old_edit.new))
{
new_edit = new_edits.next().unwrap();
} else {
@@ -964,7 +963,7 @@ impl TrackedBuffer {
fn has_edits(&self, cx: &App) -> bool {
self.diff
.read(cx)
- .hunks(&self.buffer.read(cx), cx)
+ .hunks(self.buffer.read(cx), cx)
.next()
.is_some()
}
@@ -2219,7 +2218,7 @@ mod tests {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
for _ in 0..operations {
- match rng.gen_range(0..100) {
+ match rng.random_range(0..100) {
0..25 => {
action_log.update(cx, |log, cx| {
let range = buffer.read(cx).random_byte_range(0, &mut rng);
@@ -2238,7 +2237,7 @@ mod tests {
.unwrap();
}
_ => {
- let is_agent_edit = rng.gen_bool(0.5);
+ let is_agent_edit = rng.random_bool(0.5);
if is_agent_edit {
log::info!("agent edit");
} else {
@@ -2253,7 +2252,7 @@ mod tests {
}
}
- if rng.gen_bool(0.2) {
+ if rng.random_bool(0.2) {
quiesce(&action_log, &buffer, cx);
}
}
@@ -2268,7 +2267,7 @@ mod tests {
log::info!("quiescing...");
cx.run_until_parked();
action_log.update(cx, |log, cx| {
- let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap();
+ let tracked_buffer = log.tracked_buffers.get(buffer).unwrap();
let mut old_text = tracked_buffer.diff_base.clone();
let new_text = buffer.read(cx).as_rope();
for edit in tracked_buffer.unreviewed_edits.edits() {
@@ -2426,7 +2425,7 @@ mod tests {
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
- buffer.clone(),
+ buffer,
vec![
HunkStatus {
range: Point::new(6, 0)..Point::new(7, 0),
@@ -1,11 +1,10 @@
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
use editor::Editor;
-use extension_host::ExtensionStore;
+use extension_host::{ExtensionOperation, ExtensionStore};
use futures::StreamExt;
use gpui::{
- Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter,
- InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
- Styled, Transformation, Window, actions, percentage,
+ App, Context, CursorStyle, Entity, EventEmitter, InteractiveElement as _, ParentElement as _,
+ Render, SharedString, StatefulInteractiveElement, Styled, Window, actions,
};
use language::{
BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
@@ -25,7 +24,10 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
-use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
+use ui::{
+ ButtonLike, CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip,
+ prelude::*,
+};
use util::truncate_and_trailoff;
use workspace::{StatusItemView, Workspace, item::ItemHandle};
@@ -82,7 +84,6 @@ impl ActivityIndicator {
) -> Entity<ActivityIndicator> {
let project = workspace.project().clone();
let auto_updater = AutoUpdater::get(cx);
- let workspace_handle = cx.entity();
let this = cx.new(|cx| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(async move |this, cx| {
@@ -100,29 +101,10 @@ impl ActivityIndicator {
})
.detach();
- cx.subscribe_in(
- &workspace_handle,
- window,
- |activity_indicator, _, event, window, cx| match event {
- workspace::Event::ClearActivityIndicator { .. } => {
- if activity_indicator.statuses.pop().is_some() {
- activity_indicator.dismiss_error_message(
- &DismissErrorMessage,
- window,
- cx,
- );
- cx.notify();
- }
- }
- _ => {}
- },
- )
- .detach();
-
cx.subscribe(
&project.read(cx).lsp_store(),
- |activity_indicator, _, event, cx| match event {
- LspStoreEvent::LanguageServerUpdate { name, message, .. } => {
+ |activity_indicator, _, event, cx| {
+ if let LspStoreEvent::LanguageServerUpdate { name, message, .. } = event {
if let proto::update_language_server::Variant::StatusUpdate(status_update) =
message
{
@@ -191,7 +173,6 @@ impl ActivityIndicator {
}
cx.notify()
}
- _ => {}
},
)
.detach();
@@ -206,9 +187,10 @@ impl ActivityIndicator {
cx.subscribe(
&project.read(cx).git_store().clone(),
- |_, _, event: &GitStoreEvent, cx| match event {
- project::git_store::GitStoreEvent::JobsUpdated => cx.notify(),
- _ => {}
+ |_, _, event: &GitStoreEvent, cx| {
+ if let project::git_store::GitStoreEvent::JobsUpdated = event {
+ cx.notify()
+ }
},
)
.detach();
@@ -230,7 +212,8 @@ impl ActivityIndicator {
server_name,
status,
} => {
- let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
+ let create_buffer =
+ project.update(cx, |project, cx| project.create_buffer(false, cx));
let status = status.clone();
let server_name = server_name.clone();
cx.spawn_in(window, async move |workspace, cx| {
@@ -410,13 +393,7 @@ impl ActivityIndicator {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(percentage(delta)))
- },
- )
+ .with_rotate_animation(2)
.into_any_element(),
),
message,
@@ -438,11 +415,7 @@ impl ActivityIndicator {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- )
+ .with_rotate_animation(2)
.into_any_element(),
),
message: format!("Debug: {}", session.read(cx).adapter()),
@@ -458,26 +431,20 @@ impl ActivityIndicator {
.map(|r| r.read(cx))
.and_then(Repository::current_job);
// Show any long-running git command
- if let Some(job_info) = current_job {
- if Instant::now() - job_info.start >= GIT_OPERATION_DELAY {
- return Some(Content {
- icon: Some(
- Icon::new(IconName::ArrowCircle)
- .size(IconSize::Small)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(percentage(delta)))
- },
- )
- .into_any_element(),
- ),
- message: job_info.message.into(),
- on_click: None,
- tooltip_message: None,
- });
- }
+ if let Some(job_info) = current_job
+ && Instant::now() - job_info.start >= GIT_OPERATION_DELAY
+ {
+ return Some(Content {
+ icon: Some(
+ Icon::new(IconName::ArrowCircle)
+ .size(IconSize::Small)
+ .with_rotate_animation(2)
+ .into_any_element(),
+ ),
+ message: job_info.message.into(),
+ on_click: None,
+ tooltip_message: None,
+ });
}
// Show any language server installation info.
@@ -678,8 +645,9 @@ impl ActivityIndicator {
}
// Show any application auto-update info.
- if let Some(updater) = &self.auto_updater {
- return match &updater.read(cx).status() {
+ self.auto_updater
+ .as_ref()
+ .and_then(|updater| match &updater.read(cx).status() {
AutoUpdateStatus::Checking => Some(Content {
icon: Some(
Icon::new(IconName::Download)
@@ -702,7 +670,7 @@ impl ActivityIndicator {
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
- tooltip_message: Some(Self::version_tooltip_message(&version)),
+ tooltip_message: Some(Self::version_tooltip_message(version)),
}),
AutoUpdateStatus::Installing { version } => Some(Content {
icon: Some(
@@ -714,13 +682,13 @@ impl ActivityIndicator {
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
- tooltip_message: Some(Self::version_tooltip_message(&version)),
+ tooltip_message: Some(Self::version_tooltip_message(version)),
}),
AutoUpdateStatus::Updated { version } => Some(Content {
icon: None,
message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
- tooltip_message: Some(Self::version_tooltip_message(&version)),
+ tooltip_message: Some(Self::version_tooltip_message(version)),
}),
AutoUpdateStatus::Errored => Some(Content {
icon: Some(
@@ -735,29 +703,49 @@ impl ActivityIndicator {
tooltip_message: None,
}),
AutoUpdateStatus::Idle => None,
- };
- }
-
- if let Some(extension_store) =
- ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
- {
- if let Some(extension_id) = extension_store.outstanding_operations().keys().next() {
- return Some(Content {
- icon: Some(
- Icon::new(IconName::Download)
- .size(IconSize::Small)
- .into_any_element(),
- ),
- message: format!("Updating {extension_id} extension…"),
- on_click: Some(Arc::new(|this, window, cx| {
- this.dismiss_error_message(&DismissErrorMessage, window, cx)
- })),
- tooltip_message: None,
- });
- }
- }
+ })
+ .or_else(|| {
+ if let Some(extension_store) =
+ ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
+ && let Some((extension_id, operation)) =
+ extension_store.outstanding_operations().iter().next()
+ {
+ let (message, icon, rotate) = match operation {
+ ExtensionOperation::Install => (
+ format!("Installing {extension_id} extension…"),
+ IconName::LoadCircle,
+ true,
+ ),
+ ExtensionOperation::Upgrade => (
+ format!("Updating {extension_id} extension…"),
+ IconName::Download,
+ false,
+ ),
+ ExtensionOperation::Remove => (
+ format!("Removing {extension_id} extension…"),
+ IconName::LoadCircle,
+ true,
+ ),
+ };
- None
+ Some(Content {
+ icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| {
+ if rotate {
+ this.with_rotate_animation(3).into_any_element()
+ } else {
+ this.into_any_element()
+ }
+ })),
+ message,
+ on_click: Some(Arc::new(|this, window, cx| {
+ this.dismiss_error_message(&Default::default(), window, cx)
+ })),
+ tooltip_message: None,
+ })
+ } else {
+ None
+ }
+ })
}
fn version_tooltip_message(version: &VersionCheckType) -> String {
@@ -31,7 +31,6 @@ collections.workspace = true
component.workspace = true
context_server.workspace = true
convert_case.workspace = true
-feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
git.workspace = true
@@ -64,6 +63,7 @@ time.workspace = true
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
+zed_env_vars.workspace = true
zstd.workspace = true
[dev-dependencies]
@@ -90,7 +90,7 @@ impl AgentProfile {
return false;
};
- return Self::is_enabled(settings, source, tool_name);
+ Self::is_enabled(settings, source, tool_name)
}
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
@@ -132,7 +132,7 @@ mod tests {
});
let tool_set = default_tool_set(cx);
- let profile = AgentProfile::new(id.clone(), tool_set);
+ let profile = AgentProfile::new(id, tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
@@ -169,7 +169,7 @@ mod tests {
});
let tool_set = default_tool_set(cx);
- let profile = AgentProfile::new(id.clone(), tool_set);
+ let profile = AgentProfile::new(id, tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
@@ -202,7 +202,7 @@ mod tests {
});
let tool_set = default_tool_set(cx);
- let profile = AgentProfile::new(id.clone(), tool_set);
+ let profile = AgentProfile::new(id, tool_set);
let mut enabled_tools = cx
.read(|cx| profile.enabled_tools(cx))
@@ -202,23 +202,22 @@ impl FileContextHandle {
}
if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot()) {
- if let Some(outline) = snapshot.outline(None) {
- let items = outline
- .items
- .into_iter()
- .map(|item| item.to_point(&snapshot));
-
- if let Ok(outline_text) =
- outline::render_outline(items, None, 0, usize::MAX).await
- {
- let context = AgentContext::File(FileContext {
- handle: self,
- full_path,
- text: outline_text.into(),
- is_outline: true,
- });
- return Some((context, vec![buffer]));
- }
+ let items = snapshot
+ .outline(None)
+ .items
+ .into_iter()
+ .map(|item| item.to_point(&snapshot));
+
+ if let Ok(outline_text) =
+ outline::render_outline(items, None, 0, usize::MAX).await
+ {
+ let context = AgentContext::File(FileContext {
+ handle: self,
+ full_path,
+ text: outline_text.into(),
+ is_outline: true,
+ });
+ return Some((context, vec![buffer]));
}
}
}
@@ -362,7 +361,7 @@ impl Display for DirectoryContext {
let mut is_first = true;
for descendant in &self.descendants {
if !is_first {
- write!(f, "\n")?;
+ writeln!(f)?;
} else {
is_first = false;
}
@@ -650,7 +649,7 @@ impl TextThreadContextHandle {
impl Display for TextThreadContext {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
// TODO: escape title?
- write!(f, "<text_thread title=\"{}\">\n", self.title)?;
+ writeln!(f, "<text_thread title=\"{}\">", self.title)?;
write!(f, "{}", self.text.trim())?;
write!(f, "\n</text_thread>")
}
@@ -716,7 +715,7 @@ impl RulesContextHandle {
impl Display for RulesContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(title) = &self.title {
- write!(f, "Rules title: {}\n", title)?;
+ writeln!(f, "Rules title: {}", title)?;
}
let code_block = MarkdownCodeBlock {
tag: "",
@@ -86,15 +86,13 @@ impl Tool for ContextServerTool {
) -> ToolResult {
if let Some(server) = self.store.read(cx).get_running_server(&self.server_id) {
let tool_name = self.tool.name.clone();
- let server_clone = server.clone();
- let input_clone = input.clone();
cx.spawn(async move |_cx| {
- let Some(protocol) = server_clone.client() else {
+ let Some(protocol) = server.client() else {
bail!("Context server not initialized");
};
- let arguments = if let serde_json::Value::Object(map) = input_clone {
+ let arguments = if let serde_json::Value::Object(map) = input {
Some(map.into_iter().collect())
} else {
None
@@ -338,11 +338,9 @@ impl ContextStore {
image_task,
context_id: self.next_context_id.post_inc(),
});
- if self.has_context(&context) {
- if remove_if_exists {
- self.remove_context(&context, cx);
- return None;
- }
+ if self.has_context(&context) && remove_if_exists {
+ self.remove_context(&context, cx);
+ return None;
}
self.insert_context(context.clone(), cx);
@@ -254,10 +254,9 @@ impl HistoryStore {
}
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
- self.recently_opened_entries.retain(|entry| match entry {
- HistoryEntryId::Thread(thread_id) if thread_id == &id => false,
- _ => true,
- });
+ self.recently_opened_entries.retain(
+ |entry| !matches!(entry, HistoryEntryId::Thread(thread_id) if thread_id == &id),
+ );
self.save_recently_opened_entries(cx);
}
@@ -9,14 +9,16 @@ use crate::{
tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState},
};
use action_log::ActionLog;
-use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT};
+use agent_settings::{
+ AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT,
+ SUMMARIZE_THREAD_PROMPT,
+};
use anyhow::{Result, anyhow};
use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage};
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
use collections::HashMap;
-use feature_flags::{self, FeatureFlagAppExt};
use futures::{FutureExt, StreamExt as _, future::Shared};
use git::repository::DiffType;
use gpui::{
@@ -108,7 +110,7 @@ impl std::fmt::Display for PromptId {
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
-pub struct MessageId(pub(crate) usize);
+pub struct MessageId(pub usize);
impl MessageId {
fn post_inc(&mut self) -> Self {
@@ -179,7 +181,7 @@ impl Message {
}
}
- pub fn to_string(&self) -> String {
+ pub fn to_message_content(&self) -> String {
let mut result = String::new();
if !self.loaded_context.text.is_empty() {
@@ -385,10 +387,8 @@ pub struct Thread {
cumulative_token_usage: TokenUsage,
exceeded_window_error: Option<ExceededWindowError>,
tool_use_limit_reached: bool,
- feedback: Option<ThreadFeedback>,
retry_state: Option<RetryState>,
message_feedback: HashMap<MessageId, ThreadFeedback>,
- last_auto_capture_at: Option<Instant>,
last_received_chunk_at: Option<Instant>,
request_callback: Option<
Box<dyn FnMut(&LanguageModelRequest, &[Result<LanguageModelCompletionEvent, String>])>,
@@ -486,15 +486,13 @@ impl Thread {
cumulative_token_usage: TokenUsage::default(),
exceeded_window_error: None,
tool_use_limit_reached: false,
- feedback: None,
retry_state: None,
message_feedback: HashMap::default(),
- last_auto_capture_at: None,
last_error_context: None,
last_received_chunk_at: None,
request_callback: None,
remaining_turns: u32::MAX,
- configured_model: configured_model.clone(),
+ configured_model,
profile: AgentProfile::new(profile_id, tools),
}
}
@@ -532,7 +530,7 @@ impl Thread {
.and_then(|model| {
let model = SelectedModel {
provider: model.provider.clone().into(),
- model: model.model.clone().into(),
+ model: model.model.into(),
};
registry.select_model(&model, cx)
})
@@ -612,9 +610,7 @@ impl Thread {
cumulative_token_usage: serialized.cumulative_token_usage,
exceeded_window_error: None,
tool_use_limit_reached: serialized.tool_use_limit_reached,
- feedback: None,
message_feedback: HashMap::default(),
- last_auto_capture_at: None,
last_error_context: None,
last_received_chunk_at: None,
request_callback: None,
@@ -844,11 +840,17 @@ impl Thread {
.await
.unwrap_or(false);
- if !equal {
- this.update(cx, |this, cx| {
- this.insert_checkpoint(pending_checkpoint, cx)
- })?;
- }
+ this.update(cx, |this, cx| {
+ this.pending_checkpoint = if equal {
+ Some(pending_checkpoint)
+ } else {
+ this.insert_checkpoint(pending_checkpoint, cx);
+ Some(ThreadCheckpoint {
+ message_id: this.next_message_id,
+ git_checkpoint: final_checkpoint,
+ })
+ }
+ })?;
Ok(())
}
@@ -1027,8 +1029,6 @@ impl Thread {
});
}
- self.auto_capture_telemetry(cx);
-
message_id
}
@@ -1643,17 +1643,15 @@ impl Thread {
};
self.tool_use
- .request_tool_use(tool_message_id, tool_use, tool_use_metadata.clone(), cx);
+ .request_tool_use(tool_message_id, tool_use, tool_use_metadata, cx);
- let pending_tool_use = self.tool_use.insert_tool_output(
- tool_use_id.clone(),
+ self.tool_use.insert_tool_output(
+ tool_use_id,
tool_name,
tool_output,
self.configured_model.as_ref(),
self.completion_mode,
- );
-
- pending_tool_use
+ )
}
pub fn stream_completion(
@@ -1686,7 +1684,7 @@ impl Thread {
self.last_received_chunk_at = Some(Instant::now());
let task = cx.spawn(async move |thread, cx| {
- let stream_completion_future = model.stream_completion(request, &cx);
+ let stream_completion_future = model.stream_completion(request, cx);
let initial_token_usage =
thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage);
let stream_completion = async {
@@ -1818,7 +1816,7 @@ impl Thread {
let streamed_input = if tool_use.is_input_complete {
None
} else {
- Some((&tool_use.input).clone())
+ Some(tool_use.input.clone())
};
let ui_text = thread.tool_use.request_tool_use(
@@ -1900,7 +1898,6 @@ impl Thread {
cx.emit(ThreadEvent::StreamedCompletion);
cx.notify();
- thread.auto_capture_telemetry(cx);
Ok(())
})??;
@@ -1968,11 +1965,9 @@ impl Thread {
if let Some(prev_message) =
thread.messages.get(ix - 1)
- {
- if prev_message.role == Role::Assistant {
+ && prev_message.role == Role::Assistant {
break;
}
- }
}
}
@@ -2045,7 +2040,7 @@ impl Thread {
retry_scheduled = thread
.handle_retryable_error_with_delay(
- &completion_error,
+ completion_error,
Some(retry_strategy),
model.clone(),
intent,
@@ -2075,8 +2070,6 @@ impl Thread {
request_callback(request, response_events);
}
- thread.auto_capture_telemetry(cx);
-
if let Ok(initial_usage) = initial_token_usage {
let usage = thread.cumulative_token_usage - initial_usage;
@@ -2124,7 +2117,7 @@ impl Thread {
self.pending_summary = cx.spawn(async move |this, cx| {
let result = async {
- let mut messages = model.model.stream_completion(request, &cx).await?;
+ let mut messages = model.model.stream_completion(request, cx).await?;
let mut new_summary = String::new();
while let Some(event) = messages.next().await {
@@ -2432,12 +2425,10 @@ impl Thread {
return;
}
- let added_user_message = include_str!("./prompts/summarize_thread_detailed_prompt.txt");
-
let request = self.to_summarize_request(
&model,
CompletionIntent::ThreadContextSummarization,
- added_user_message.into(),
+ SUMMARIZE_THREAD_DETAILED_PROMPT.into(),
cx,
);
@@ -2450,7 +2441,7 @@ impl Thread {
// which result to prefer (the old task could complete after the new one, resulting in a
// stale summary).
self.detailed_summary_task = cx.spawn(async move |thread, cx| {
- let stream = model.stream_completion_text(request, &cx);
+ let stream = model.stream_completion_text(request, cx);
let Some(mut messages) = stream.await.log_err() else {
thread
.update(cx, |thread, _cx| {
@@ -2479,13 +2470,13 @@ impl Thread {
.ok()?;
// Save thread so its summary can be reused later
- if let Some(thread) = thread.upgrade() {
- if let Ok(Ok(save_task)) = cx.update(|cx| {
+ if let Some(thread) = thread.upgrade()
+ && let Ok(Ok(save_task)) = cx.update(|cx| {
thread_store
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
- }) {
- save_task.await.log_err();
- }
+ })
+ {
+ save_task.await.log_err();
}
Some(())
@@ -2530,7 +2521,6 @@ impl Thread {
model: Arc<dyn LanguageModel>,
cx: &mut Context<Self>,
) -> Vec<PendingToolUse> {
- self.auto_capture_telemetry(cx);
let request =
Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx));
let pending_tool_uses = self
@@ -2734,13 +2724,11 @@ impl Thread {
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) {
- if self.all_tools_finished() {
- if let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() {
- if !canceled {
- self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx);
- }
- self.auto_capture_telemetry(cx);
- }
+ if self.all_tools_finished()
+ && let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref()
+ && !canceled
+ {
+ self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx);
}
cx.emit(ThreadEvent::ToolFinished {
@@ -2796,10 +2784,6 @@ impl Thread {
cx.emit(ThreadEvent::CancelEditing);
}
- pub fn feedback(&self) -> Option<ThreadFeedback> {
- self.feedback
- }
-
pub fn message_feedback(&self, message_id: MessageId) -> Option<ThreadFeedback> {
self.message_feedback.get(&message_id).copied()
}
@@ -2832,7 +2816,7 @@ impl Thread {
let message_content = self
.message(message_id)
- .map(|msg| msg.to_string())
+ .map(|msg| msg.to_message_content())
.unwrap_or_default();
cx.background_spawn(async move {
@@ -2861,52 +2845,6 @@ impl Thread {
})
}
- pub fn report_feedback(
- &mut self,
- feedback: ThreadFeedback,
- cx: &mut Context<Self>,
- ) -> Task<Result<()>> {
- let last_assistant_message_id = self
- .messages
- .iter()
- .rev()
- .find(|msg| msg.role == Role::Assistant)
- .map(|msg| msg.id);
-
- if let Some(message_id) = last_assistant_message_id {
- self.report_message_feedback(message_id, feedback, cx)
- } else {
- let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx);
- let serialized_thread = self.serialize(cx);
- let thread_id = self.id().clone();
- let client = self.project.read(cx).client();
- self.feedback = Some(feedback);
- cx.notify();
-
- cx.background_spawn(async move {
- let final_project_snapshot = final_project_snapshot.await;
- let serialized_thread = serialized_thread.await?;
- let thread_data = serde_json::to_value(serialized_thread)
- .unwrap_or_else(|_| serde_json::Value::Null);
-
- let rating = match feedback {
- ThreadFeedback::Positive => "positive",
- ThreadFeedback::Negative => "negative",
- };
- telemetry::event!(
- "Assistant Thread Rated",
- rating,
- thread_id,
- thread_data,
- final_project_snapshot
- );
- client.telemetry().flush_events().await;
-
- Ok(())
- })
- }
- }
-
/// Create a snapshot of the current project state including git information and unsaved buffers.
fn project_snapshot(
project: Entity<Project>,
@@ -2927,11 +2865,11 @@ impl Thread {
let buffer_store = project.read(app_cx).buffer_store();
for buffer_handle in buffer_store.read(app_cx).buffers() {
let buffer = buffer_handle.read(app_cx);
- if buffer.is_dirty() {
- if let Some(file) = buffer.file() {
- let path = file.path().to_string_lossy().to_string();
- unsaved_buffers.push(path);
- }
+ if buffer.is_dirty()
+ && let Some(file) = buffer.file()
+ {
+ let path = file.path().to_string_lossy().to_string();
+ unsaved_buffers.push(path);
}
}
})
@@ -3141,50 +3079,6 @@ impl Thread {
&self.project
}
- pub fn auto_capture_telemetry(&mut self, cx: &mut Context<Self>) {
- if !cx.has_flag::<feature_flags::ThreadAutoCaptureFeatureFlag>() {
- return;
- }
-
- let now = Instant::now();
- if let Some(last) = self.last_auto_capture_at {
- if now.duration_since(last).as_secs() < 10 {
- return;
- }
- }
-
- self.last_auto_capture_at = Some(now);
-
- let thread_id = self.id().clone();
- let github_login = self
- .project
- .read(cx)
- .user_store()
- .read(cx)
- .current_user()
- .map(|user| user.github_login.clone());
- let client = self.project.read(cx).client();
- let serialize_task = self.serialize(cx);
-
- cx.background_executor()
- .spawn(async move {
- if let Ok(serialized_thread) = serialize_task.await {
- if let Ok(thread_data) = serde_json::to_value(serialized_thread) {
- telemetry::event!(
- "Agent Thread Auto-Captured",
- thread_id = thread_id.to_string(),
- thread_data = thread_data,
- auto_capture_reason = "tracked_user",
- github_login = github_login
- );
-
- client.telemetry().flush_events().await;
- }
- }
- })
- .detach();
- }
-
pub fn cumulative_token_usage(&self) -> TokenUsage {
self.cumulative_token_usage
}
@@ -3227,13 +3121,13 @@ impl Thread {
.model
.max_token_count_for_mode(self.completion_mode().into());
- if let Some(exceeded_error) = &self.exceeded_window_error {
- if model.model.id() == exceeded_error.model_id {
- return Some(TotalTokenUsage {
- total: exceeded_error.token_count,
- max,
- });
- }
+ if let Some(exceeded_error) = &self.exceeded_window_error
+ && model.model.id() == exceeded_error.model_id
+ {
+ return Some(TotalTokenUsage {
+ total: exceeded_error.token_count,
+ max,
+ });
}
let total = self
@@ -3294,7 +3188,7 @@ impl Thread {
self.configured_model.as_ref(),
self.completion_mode,
);
- self.tool_finished(tool_use_id.clone(), None, true, window, cx);
+ self.tool_finished(tool_use_id, None, true, window, cx);
}
}
@@ -3926,7 +3820,7 @@ fn main() {{
AgentSettings {
model_parameters: vec![LanguageModelParameters {
provider: Some(model.provider_id().0.to_string().into()),
- model: Some(model.id().0.clone()),
+ model: Some(model.id().0),
temperature: Some(0.66),
}],
..AgentSettings::get_global(cx).clone()
@@ -3946,7 +3840,7 @@ fn main() {{
AgentSettings {
model_parameters: vec![LanguageModelParameters {
provider: None,
- model: Some(model.id().0.clone()),
+ model: Some(model.id().0),
temperature: Some(0.66),
}],
..AgentSettings::get_global(cx).clone()
@@ -3986,7 +3880,7 @@ fn main() {{
AgentSettings {
model_parameters: vec![LanguageModelParameters {
provider: Some("anthropic".into()),
- model: Some(model.id().0.clone()),
+ model: Some(model.id().0),
temperature: Some(0.66),
}],
..AgentSettings::get_global(cx).clone()
@@ -4037,7 +3931,7 @@ fn main() {{
});
let fake_model = model.as_fake();
- simulate_successful_response(&fake_model, cx);
+ simulate_successful_response(fake_model, cx);
// Should start generating summary when there are >= 2 messages
thread.read_with(cx, |thread, _| {
@@ -4132,7 +4026,7 @@ fn main() {{
});
let fake_model = model.as_fake();
- simulate_successful_response(&fake_model, cx);
+ simulate_successful_response(fake_model, cx);
thread.read_with(cx, |thread, _| {
// State is still Error, not Generating
@@ -5331,7 +5225,7 @@ fn main() {{
}
#[gpui::test]
- async fn test_retry_cancelled_on_stop(cx: &mut TestAppContext) {
+ async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(cx, json!({})).await;
@@ -5387,7 +5281,7 @@ fn main() {{
"Should have no pending completions after cancellation"
);
- // Verify the retry was cancelled by checking retry state
+ // Verify the retry was canceled by checking retry state
thread.read_with(cx, |thread, _| {
if let Some(retry_state) = &thread.retry_state {
panic!(
@@ -5414,7 +5308,7 @@ fn main() {{
});
let fake_model = model.as_fake();
- simulate_successful_response(&fake_model, cx);
+ simulate_successful_response(fake_model, cx);
thread.read_with(cx, |thread, _| {
assert!(matches!(thread.summary(), ThreadSummary::Generating));
@@ -41,8 +41,7 @@ use std::{
};
use util::ResultExt as _;
-pub static ZED_STATELESS: std::sync::LazyLock<bool> =
- std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
+use zed_env_vars::ZED_STATELESS;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {
@@ -74,7 +73,7 @@ impl Column for DataType {
}
}
-const RULES_FILE_NAMES: [&'static str; 9] = [
+const RULES_FILE_NAMES: [&str; 9] = [
".rules",
".cursorrules",
".windsurfrules",
@@ -581,33 +580,32 @@ impl ThreadStore {
return;
};
- if protocol.capable(context_server::protocol::ServerCapability::Tools) {
- if let Some(response) = protocol
+ if protocol.capable(context_server::protocol::ServerCapability::Tools)
+ && let Some(response) = protocol
.request::<context_server::types::requests::ListTools>(())
.await
.log_err()
- {
- let tool_ids = tool_working_set
- .update(cx, |tool_working_set, cx| {
- tool_working_set.extend(
- response.tools.into_iter().map(|tool| {
- Arc::new(ContextServerTool::new(
- context_server_store.clone(),
- server.id(),
- tool,
- )) as Arc<dyn Tool>
- }),
- cx,
- )
- })
- .log_err();
-
- if let Some(tool_ids) = tool_ids {
- this.update(cx, |this, _| {
- this.context_server_tool_ids.insert(server_id, tool_ids);
- })
- .log_err();
- }
+ {
+ let tool_ids = tool_working_set
+ .update(cx, |tool_working_set, cx| {
+ tool_working_set.extend(
+ response.tools.into_iter().map(|tool| {
+ Arc::new(ContextServerTool::new(
+ context_server_store.clone(),
+ server.id(),
+ tool,
+ )) as Arc<dyn Tool>
+ }),
+ cx,
+ )
+ })
+ .log_err();
+
+ if let Some(tool_ids) = tool_ids {
+ this.update(cx, |this, _| {
+ this.context_server_tool_ids.insert(server_id, tool_ids);
+ })
+ .log_err();
}
}
})
@@ -697,13 +695,14 @@ impl SerializedThreadV0_1_0 {
let mut messages: Vec<SerializedMessage> = Vec::with_capacity(self.0.messages.len());
for message in self.0.messages {
- if message.role == Role::User && !message.tool_results.is_empty() {
- if let Some(last_message) = messages.last_mut() {
- debug_assert!(last_message.role == Role::Assistant);
-
- last_message.tool_results = message.tool_results;
- continue;
- }
+ if message.role == Role::User
+ && !message.tool_results.is_empty()
+ && let Some(last_message) = messages.last_mut()
+ {
+ debug_assert!(last_message.role == Role::Assistant);
+
+ last_message.tool_results = message.tool_results;
+ continue;
}
messages.push(message);
@@ -895,6 +894,17 @@ impl ThreadsDatabase {
let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
+ } else if cfg!(any(feature = "test-support", test)) {
+ // rust stores the name of the test on the current thread.
+ // We use this to automatically create a database that will
+ // be shared within the test (for the test_retrieve_old_thread)
+ // but not with concurrent tests.
+ let thread = std::thread::current();
+ let test_name = thread.name();
+ Connection::open_memory(Some(&format!(
+ "THREAD_FALLBACK_{}",
+ test_name.unwrap_or_default()
+ )))
} else {
Connection::open_file(&sqlite_path.to_string_lossy())
};
@@ -112,19 +112,13 @@ impl ToolUseState {
},
);
- if let Some(window) = &mut window {
- if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
- if let Some(output) = tool_result.output.clone() {
- if let Some(card) = tool.deserialize_card(
- output,
- project.clone(),
- window,
- cx,
- ) {
- this.tool_result_cards.insert(tool_use_id, card);
- }
- }
- }
+ if let Some(window) = &mut window
+ && let Some(tool) = this.tools.read(cx).tool(tool_use, cx)
+ && let Some(output) = tool_result.output.clone()
+ && let Some(card) =
+ tool.deserialize_card(output, project.clone(), window, cx)
+ {
+ this.tool_result_cards.insert(tool_use_id, card);
}
}
}
@@ -137,7 +131,7 @@ impl ToolUseState {
}
pub fn cancel_pending(&mut self) -> Vec<PendingToolUse> {
- let mut cancelled_tool_uses = Vec::new();
+ let mut canceled_tool_uses = Vec::new();
self.pending_tool_uses_by_id
.retain(|tool_use_id, tool_use| {
if matches!(tool_use.status, PendingToolUseStatus::Error { .. }) {
@@ -155,10 +149,10 @@ impl ToolUseState {
is_error: true,
},
);
- cancelled_tool_uses.push(tool_use.clone());
+ canceled_tool_uses.push(tool_use.clone());
false
});
- cancelled_tool_uses
+ canceled_tool_uses
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
@@ -281,7 +275,7 @@ impl ToolUseState {
pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool {
self.tool_uses_by_assistant_message
.get(&assistant_message_id)
- .map_or(false, |results| !results.is_empty())
+ .is_some_and(|results| !results.is_empty())
}
pub fn tool_result(
@@ -8,24 +8,33 @@ license = "GPL-3.0-or-later"
[lib]
path = "src/agent2.rs"
+[features]
+test-support = ["db/test-support"]
+e2e = []
+
[lints]
workspace = true
[dependencies]
acp_thread.workspace = true
action_log.workspace = true
+agent.workspace = true
agent-client-protocol.workspace = true
agent_servers.workspace = true
agent_settings.workspace = true
anyhow.workspace = true
+assistant_context.workspace = true
assistant_tool.workspace = true
assistant_tools.workspace = true
chrono.workspace = true
+client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
context_server.workspace = true
+db.workspace = true
fs.workspace = true
futures.workspace = true
+git.workspace = true
gpui.workspace = true
handlebars = { workspace = true, features = ["rust-embed"] }
html_to_markdown.workspace = true
@@ -37,8 +46,8 @@ language_model.workspace = true
language_models.workspace = true
log.workspace = true
open.workspace = true
+parking_lot.workspace = true
paths.workspace = true
-portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
rust-embed.workspace = true
@@ -47,25 +56,34 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
+sqlez.workspace = true
task.workspace = true
+telemetry.workspace = true
terminal.workspace = true
+thiserror.workspace = true
text.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
web_search.workspace = true
-which.workspace = true
workspace-hack.workspace = true
+zed_env_vars.workspace = true
+zstd.workspace = true
[dev-dependencies]
+agent = { workspace = true, "features" = ["test-support"] }
+agent_servers = { workspace = true, "features" = ["test-support"] }
+assistant_context = { workspace = true, "features" = ["test-support"] }
ctor.workspace = true
client = { workspace = true, "features" = ["test-support"] }
clock = { workspace = true, "features" = ["test-support"] }
context_server = { workspace = true, "features" = ["test-support"] }
+db = { workspace = true, "features" = ["test-support"] }
editor = { workspace = true, "features" = ["test-support"] }
env_logger.workspace = true
fs = { workspace = true, "features" = ["test-support"] }
+git = { workspace = true, "features" = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }
gpui_tokio.workspace = true
language = { workspace = true, "features" = ["test-support"] }
@@ -1,16 +1,17 @@
-use crate::{AgentResponseEvent, Thread, templates::Templates};
use crate::{
- ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, DiagnosticsTool,
- EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool,
- OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, UserMessageContent,
- WebSearchTool,
+ ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization,
+ UserMessageContent, templates::Templates,
};
-use acp_thread::AgentModelSelector;
+use crate::{HistoryStore, TerminalHandle, ThreadEnvironment, TitleUpdated, TokenUsageUpdated};
+use acp_thread::{AcpThread, AgentModelSelector};
+use action_log::ActionLog;
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result, anyhow};
use collections::{HashSet, IndexMap};
use fs::Fs;
+use futures::channel::{mpsc, oneshot};
+use futures::future::Shared;
use futures::{StreamExt, future};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
@@ -21,14 +22,14 @@ use prompt_store::{
ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext,
};
use settings::update_settings_file;
-use std::cell::RefCell;
+use std::any::Any;
use std::collections::HashMap;
-use std::path::Path;
+use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use util::ResultExt;
-const RULES_FILE_NAMES: [&'static str; 9] = [
+const RULES_FILE_NAMES: [&str; 9] = [
".rules",
".cursorrules",
".windsurfrules",
@@ -50,7 +51,8 @@ struct Session {
thread: Entity<Thread>,
/// The ACP thread that handles protocol communication
acp_thread: WeakEntity<acp_thread::AcpThread>,
- _subscription: Subscription,
+ pending_save: Task<()>,
+ _subscriptions: Vec<Subscription>,
}
pub struct LanguageModels {
@@ -60,16 +62,19 @@ pub struct LanguageModels {
model_list: acp_thread::AgentModelList,
refresh_models_rx: watch::Receiver<()>,
refresh_models_tx: watch::Sender<()>,
+ _authenticate_all_providers_task: Task<()>,
}
impl LanguageModels {
- fn new(cx: &App) -> Self {
+ fn new(cx: &mut App) -> Self {
let (refresh_models_tx, refresh_models_rx) = watch::channel(());
+
let mut this = Self {
models: HashMap::default(),
model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()),
refresh_models_rx,
refresh_models_tx,
+ _authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx),
};
this.refresh_list(cx);
this
@@ -89,8 +94,8 @@ impl LanguageModels {
let mut recommended = Vec::new();
for provider in &providers {
for model in provider.recommended_models(cx) {
- recommended_models.insert(model.id());
- recommended.push(Self::map_language_model_to_info(&model, &provider));
+ recommended_models.insert((model.provider_id(), model.id()));
+ recommended.push(Self::map_language_model_to_info(&model, provider));
}
}
if !recommended.is_empty() {
@@ -106,7 +111,7 @@ impl LanguageModels {
for model in provider.provided_models(cx) {
let model_info = Self::map_language_model_to_info(&model, &provider);
let model_id = model_info.id.clone();
- if !recommended_models.contains(&model.id()) {
+ if !recommended_models.contains(&(model.provider_id(), model.id())) {
provider_models.push(model_info);
}
models.insert(model_id, model);
@@ -149,13 +154,60 @@ impl LanguageModels {
fn model_id(model: &Arc<dyn LanguageModel>) -> acp_thread::AgentModelId {
acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
}
+
+ fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
+ let authenticate_all_providers = LanguageModelRegistry::global(cx)
+ .read(cx)
+ .providers()
+ .iter()
+ .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
+ .collect::<Vec<_>>();
+
+ cx.background_spawn(async move {
+ for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
+ if let Err(err) = authenticate_task.await {
+ if matches!(err, language_model::AuthenticateError::CredentialsNotFound) {
+ // Since we're authenticating these providers in the
+ // background for the purposes of populating the
+ // language selector, we don't care about providers
+ // where the credentials are not found.
+ } else {
+ // Some providers have noisy failure states that we
+ // don't want to spam the logs with every time the
+ // language model selector is initialized.
+ //
+ // Ideally these should have more clear failure modes
+ // that we know are safe to ignore here, like what we do
+ // with `CredentialsNotFound` above.
+ match provider_id.0.as_ref() {
+ "lmstudio" | "ollama" => {
+ // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
+ //
+ // These fail noisily, so we don't log them.
+ }
+ "copilot_chat" => {
+ // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
+ }
+ _ => {
+ log::error!(
+ "Failed to authenticate provider: {}: {err}",
+ provider_name.0
+ );
+ }
+ }
+ }
+ }
+ }
+ })
+ }
}
pub struct NativeAgent {
/// Session ID -> Session mapping
sessions: HashMap<acp::SessionId, Session>,
+ history: Entity<HistoryStore>,
/// Shared project context for all threads
- project_context: Rc<RefCell<ProjectContext>>,
+ project_context: Entity<ProjectContext>,
project_context_needs_refresh: watch::Sender<()>,
_maintain_project_context: Task<Result<()>>,
context_server_registry: Entity<ContextServerRegistry>,
@@ -172,12 +224,13 @@ pub struct NativeAgent {
impl NativeAgent {
pub async fn new(
project: Entity<Project>,
+ history: Entity<HistoryStore>,
templates: Arc<Templates>,
prompt_store: Option<Entity<PromptStore>>,
fs: Arc<dyn Fs>,
cx: &mut AsyncApp,
) -> Result<Entity<NativeAgent>> {
- log::info!("Creating new NativeAgent");
+ log::debug!("Creating new NativeAgent");
let project_context = cx
.update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))?
@@ -199,7 +252,8 @@ impl NativeAgent {
watch::channel(());
Self {
sessions: HashMap::new(),
- project_context: Rc::new(RefCell::new(project_context)),
+ history,
+ project_context: cx.new(|_| project_context),
project_context_needs_refresh: project_context_needs_refresh_tx,
_maintain_project_context: cx.spawn(async move |this, cx| {
Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await
@@ -217,6 +271,67 @@ impl NativeAgent {
})
}
+ fn register_session(
+ &mut self,
+ thread_handle: Entity<Thread>,
+ cx: &mut Context<Self>,
+ ) -> Entity<AcpThread> {
+ let connection = Rc::new(NativeAgentConnection(cx.entity()));
+
+ let thread = thread_handle.read(cx);
+ let session_id = thread.id().clone();
+ let title = thread.title();
+ let project = thread.project.clone();
+ let action_log = thread.action_log.clone();
+ let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone();
+ let acp_thread = cx.new(|cx| {
+ acp_thread::AcpThread::new(
+ title,
+ connection,
+ project.clone(),
+ action_log.clone(),
+ session_id.clone(),
+ prompt_capabilities_rx,
+ cx,
+ )
+ });
+
+ let registry = LanguageModelRegistry::read_global(cx);
+ let summarization_model = registry.thread_summary_model().map(|c| c.model);
+
+ thread_handle.update(cx, |thread, cx| {
+ thread.set_summarization_model(summarization_model, cx);
+ thread.add_default_tools(
+ Rc::new(AcpThreadEnvironment {
+ acp_thread: acp_thread.downgrade(),
+ }) as _,
+ cx,
+ )
+ });
+
+ let subscriptions = vec![
+ cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
+ this.sessions.remove(acp_thread.session_id());
+ }),
+ cx.subscribe(&thread_handle, Self::handle_thread_title_updated),
+ cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated),
+ cx.observe(&thread_handle, move |this, thread, cx| {
+ this.save_thread(thread, cx)
+ }),
+ ];
+
+ self.sessions.insert(
+ session_id,
+ Session {
+ thread: thread_handle,
+ acp_thread: acp_thread.downgrade(),
+ _subscriptions: subscriptions,
+ pending_save: Task::ready(()),
+ },
+ );
+ acp_thread
+ }
+
pub fn models(&self) -> &LanguageModels {
&self.models
}
@@ -232,7 +347,9 @@ impl NativeAgent {
Self::build_project_context(&this.project, this.prompt_store.as_ref(), cx)
})?
.await;
- this.update(cx, |this, _| this.project_context.replace(project_context))?;
+ this.update(cx, |this, cx| {
+ this.project_context = cx.new(|_| project_context);
+ })?;
}
Ok(())
@@ -385,6 +502,43 @@ impl NativeAgent {
})
}
+ fn handle_thread_title_updated(
+ &mut self,
+ thread: Entity<Thread>,
+ _: &TitleUpdated,
+ cx: &mut Context<Self>,
+ ) {
+ let session_id = thread.read(cx).id();
+ let Some(session) = self.sessions.get(session_id) else {
+ return;
+ };
+ let thread = thread.downgrade();
+ let acp_thread = session.acp_thread.clone();
+ cx.spawn(async move |_, cx| {
+ let title = thread.read_with(cx, |thread, _| thread.title())?;
+ let task = acp_thread.update(cx, |acp_thread, cx| acp_thread.set_title(title, cx))?;
+ task.await
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn handle_thread_token_usage_updated(
+ &mut self,
+ thread: Entity<Thread>,
+ usage: &TokenUsageUpdated,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(session) = self.sessions.get(thread.read(cx).id()) else {
+ return;
+ };
+ session
+ .acp_thread
+ .update(cx, |acp_thread, cx| {
+ acp_thread.update_token_usage(usage.0.clone(), cx);
+ })
+ .ok();
+ }
+
fn handle_project_event(
&mut self,
_project: Entity<Project>,
@@ -424,21 +578,251 @@ impl NativeAgent {
cx: &mut Context<Self>,
) {
self.models.refresh_list(cx);
+
+ let registry = LanguageModelRegistry::read_global(cx);
+ let default_model = registry.default_model().map(|m| m.model);
+ let summarization_model = registry.thread_summary_model().map(|m| m.model);
+
for session in self.sessions.values_mut() {
- session.thread.update(cx, |thread, _| {
- let model_id = LanguageModels::model_id(&thread.selected_model);
- if let Some(model) = self.models.model_from_id(&model_id) {
- thread.selected_model = model.clone();
+ session.thread.update(cx, |thread, cx| {
+ if thread.model().is_none()
+ && let Some(model) = default_model.clone()
+ {
+ thread.set_model(model, cx);
+ cx.notify();
}
+ thread.set_summarization_model(summarization_model.clone(), cx);
});
}
}
+
+ pub fn open_thread(
+ &mut self,
+ id: acp::SessionId,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Entity<AcpThread>>> {
+ let database_future = ThreadsDatabase::connect(cx);
+ cx.spawn(async move |this, cx| {
+ let database = database_future.await.map_err(|err| anyhow!(err))?;
+ let db_thread = database
+ .load_thread(id.clone())
+ .await?
+ .with_context(|| format!("no thread found with ID: {id:?}"))?;
+
+ let thread = this.update(cx, |this, cx| {
+ let action_log = cx.new(|_cx| ActionLog::new(this.project.clone()));
+ cx.new(|cx| {
+ Thread::from_db(
+ id.clone(),
+ db_thread,
+ this.project.clone(),
+ this.project_context.clone(),
+ this.context_server_registry.clone(),
+ action_log.clone(),
+ this.templates.clone(),
+ cx,
+ )
+ })
+ })?;
+ let acp_thread =
+ this.update(cx, |this, cx| this.register_session(thread.clone(), cx))?;
+ let events = thread.update(cx, |thread, cx| thread.replay(cx))?;
+ cx.update(|cx| {
+ NativeAgentConnection::handle_thread_events(events, acp_thread.downgrade(), cx)
+ })?
+ .await?;
+ Ok(acp_thread)
+ })
+ }
+
+ pub fn thread_summary(
+ &mut self,
+ id: acp::SessionId,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<SharedString>> {
+ let thread = self.open_thread(id.clone(), cx);
+ cx.spawn(async move |this, cx| {
+ let acp_thread = thread.await?;
+ let result = this
+ .update(cx, |this, cx| {
+ this.sessions
+ .get(&id)
+ .unwrap()
+ .thread
+ .update(cx, |thread, cx| thread.summary(cx))
+ })?
+ .await?;
+ drop(acp_thread);
+ Ok(result)
+ })
+ }
+
+ fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
+ if thread.read(cx).is_empty() {
+ return;
+ }
+
+ let database_future = ThreadsDatabase::connect(cx);
+ let (id, db_thread) =
+ thread.update(cx, |thread, cx| (thread.id().clone(), thread.to_db(cx)));
+ let Some(session) = self.sessions.get_mut(&id) else {
+ return;
+ };
+ let history = self.history.clone();
+ session.pending_save = cx.spawn(async move |_, cx| {
+ let Some(database) = database_future.await.map_err(|err| anyhow!(err)).log_err() else {
+ return;
+ };
+ let db_thread = db_thread.await;
+ database.save_thread(id, db_thread).await.log_err();
+ history.update(cx, |history, cx| history.reload(cx)).ok();
+ });
+ }
}
/// Wrapper struct that implements the AgentConnection trait
#[derive(Clone)]
pub struct NativeAgentConnection(pub Entity<NativeAgent>);
+impl NativeAgentConnection {
+ pub fn thread(&self, session_id: &acp::SessionId, cx: &App) -> Option<Entity<Thread>> {
+ self.0
+ .read(cx)
+ .sessions
+ .get(session_id)
+ .map(|session| session.thread.clone())
+ }
+
+ fn run_turn(
+ &self,
+ session_id: acp::SessionId,
+ cx: &mut App,
+ f: impl 'static
+ + FnOnce(Entity<Thread>, &mut App) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>,
+ ) -> Task<Result<acp::PromptResponse>> {
+ let Some((thread, acp_thread)) = self.0.update(cx, |agent, _cx| {
+ agent
+ .sessions
+ .get_mut(&session_id)
+ .map(|s| (s.thread.clone(), s.acp_thread.clone()))
+ }) else {
+ return Task::ready(Err(anyhow!("Session not found")));
+ };
+ log::debug!("Found session for: {}", session_id);
+
+ let response_stream = match f(thread, cx) {
+ Ok(stream) => stream,
+ Err(err) => return Task::ready(Err(err)),
+ };
+ Self::handle_thread_events(response_stream, acp_thread, cx)
+ }
+
+ fn handle_thread_events(
+ mut events: mpsc::UnboundedReceiver<Result<ThreadEvent>>,
+ acp_thread: WeakEntity<AcpThread>,
+ cx: &App,
+ ) -> Task<Result<acp::PromptResponse>> {
+ cx.spawn(async move |cx| {
+ // Handle response stream and forward to session.acp_thread
+ while let Some(result) = events.next().await {
+ match result {
+ Ok(event) => {
+ log::trace!("Received completion event: {:?}", event);
+
+ match event {
+ ThreadEvent::UserMessage(message) => {
+ acp_thread.update(cx, |thread, cx| {
+ for content in message.content {
+ thread.push_user_content_block(
+ Some(message.id.clone()),
+ content.into(),
+ cx,
+ );
+ }
+ })?;
+ }
+ ThreadEvent::AgentText(text) => {
+ acp_thread.update(cx, |thread, cx| {
+ thread.push_assistant_content_block(
+ acp::ContentBlock::Text(acp::TextContent {
+ text,
+ annotations: None,
+ }),
+ false,
+ cx,
+ )
+ })?;
+ }
+ ThreadEvent::AgentThinking(text) => {
+ acp_thread.update(cx, |thread, cx| {
+ thread.push_assistant_content_block(
+ acp::ContentBlock::Text(acp::TextContent {
+ text,
+ annotations: None,
+ }),
+ true,
+ cx,
+ )
+ })?;
+ }
+ ThreadEvent::ToolCallAuthorization(ToolCallAuthorization {
+ tool_call,
+ options,
+ response,
+ }) => {
+ let outcome_task = acp_thread.update(cx, |thread, cx| {
+ thread.request_tool_call_authorization(
+ tool_call, options, true, cx,
+ )
+ })??;
+ cx.background_spawn(async move {
+ if let acp::RequestPermissionOutcome::Selected { option_id } =
+ outcome_task.await
+ {
+ response
+ .send(option_id)
+ .map(|_| anyhow!("authorization receiver was dropped"))
+ .log_err();
+ }
+ })
+ .detach();
+ }
+ ThreadEvent::ToolCall(tool_call) => {
+ acp_thread.update(cx, |thread, cx| {
+ thread.upsert_tool_call(tool_call, cx)
+ })??;
+ }
+ ThreadEvent::ToolCallUpdate(update) => {
+ acp_thread.update(cx, |thread, cx| {
+ thread.update_tool_call(update, cx)
+ })??;
+ }
+ ThreadEvent::Retry(status) => {
+ acp_thread.update(cx, |thread, cx| {
+ thread.update_retry_status(status, cx)
+ })?;
+ }
+ ThreadEvent::Stop(stop_reason) => {
+ log::debug!("Assistant message complete: {:?}", stop_reason);
+ return Ok(acp::PromptResponse { stop_reason });
+ }
+ }
+ }
+ Err(e) => {
+ log::error!("Error in model response stream: {:?}", e);
+ return Err(e);
+ }
+ }
+ }
+
+ log::debug!("Response stream completed");
+ anyhow::Ok(acp::PromptResponse {
+ stop_reason: acp::StopReason::EndTurn,
+ })
+ })
+ }
+}
+
impl AgentModelSelector for NativeAgentConnection {
fn list_models(&self, cx: &mut App) -> Task<Result<acp_thread::AgentModelList>> {
log::debug!("NativeAgentConnection::list_models called");
@@ -456,7 +840,7 @@ impl AgentModelSelector for NativeAgentConnection {
model_id: acp_thread::AgentModelId,
cx: &mut App,
) -> Task<Result<()>> {
- log::info!("Setting model for session {}: {}", session_id, model_id);
+ log::debug!("Setting model for session {}: {}", session_id, model_id);
let Some(thread) = self
.0
.read(cx)
@@ -471,8 +855,8 @@ impl AgentModelSelector for NativeAgentConnection {
return Task::ready(Err(anyhow!("Invalid model ID {}", model_id)));
};
- thread.update(cx, |thread, _cx| {
- thread.selected_model = model.clone();
+ thread.update(cx, |thread, cx| {
+ thread.set_model(model.clone(), cx);
});
update_settings_file::<AgentSettings>(
@@ -502,13 +886,15 @@ impl AgentModelSelector for NativeAgentConnection {
else {
return Task::ready(Err(anyhow!("Session not found")));
};
- let model = thread.read(cx).selected_model.clone();
+ let Some(model) = thread.read(cx).model() else {
+ return Task::ready(Err(anyhow!("Model not found")));
+ };
let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id())
else {
return Task::ready(Err(anyhow!("Provider not found")));
};
Task::ready(Ok(LanguageModels::map_language_model_to_info(
- &model, &provider,
+ model, &provider,
)))
}
@@ -522,105 +908,42 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
- cx: &mut AsyncApp,
+ cx: &mut App,
) -> Task<Result<Entity<acp_thread::AcpThread>>> {
let agent = self.0.clone();
- log::info!("Creating new thread for project at: {:?}", cwd);
+ log::debug!("Creating new thread for project at: {:?}", cwd);
cx.spawn(async move |cx| {
log::debug!("Starting thread creation in async context");
- // Generate session ID
- let session_id = acp::SessionId(uuid::Uuid::new_v4().to_string().into());
- log::info!("Created session with ID: {}", session_id);
-
- // Create AcpThread
- let acp_thread = cx.update(|cx| {
- cx.new(|cx| {
- acp_thread::AcpThread::new(
- "agent2",
- self.clone(),
- project.clone(),
- session_id.clone(),
- cx,
- )
- })
- })?;
- let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?;
-
// Create Thread
let thread = agent.update(
cx,
|agent, cx: &mut gpui::Context<NativeAgent>| -> Result<_> {
// Fetch default model from registry settings
let registry = LanguageModelRegistry::read_global(cx);
-
// Log available models for debugging
let available_count = registry.available_models(cx).count();
log::debug!("Total available models: {}", available_count);
- let default_model = registry
- .default_model()
- .and_then(|default_model| {
- agent
- .models
- .model_from_id(&LanguageModels::model_id(&default_model.model))
- })
- .ok_or_else(|| {
- log::warn!("No default model configured in settings");
- anyhow!(
- "No default model. Please configure a default model in settings."
- )
- })?;
-
- let thread = cx.new(|cx| {
- let mut thread = Thread::new(
+ let default_model = registry.default_model().and_then(|default_model| {
+ agent
+ .models
+ .model_from_id(&LanguageModels::model_id(&default_model.model))
+ });
+ Ok(cx.new(|cx| {
+ Thread::new(
project.clone(),
agent.project_context.clone(),
agent.context_server_registry.clone(),
- action_log.clone(),
agent.templates.clone(),
default_model,
cx,
- );
- thread.add_tool(CopyPathTool::new(project.clone()));
- thread.add_tool(CreateDirectoryTool::new(project.clone()));
- thread.add_tool(DeletePathTool::new(project.clone(), action_log.clone()));
- thread.add_tool(DiagnosticsTool::new(project.clone()));
- thread.add_tool(EditFileTool::new(cx.entity()));
- thread.add_tool(FetchTool::new(project.read(cx).client().http_client()));
- thread.add_tool(FindPathTool::new(project.clone()));
- thread.add_tool(GrepTool::new(project.clone()));
- thread.add_tool(ListDirectoryTool::new(project.clone()));
- thread.add_tool(MovePathTool::new(project.clone()));
- thread.add_tool(NowTool);
- thread.add_tool(OpenTool::new(project.clone()));
- thread.add_tool(ReadFileTool::new(project.clone(), action_log));
- thread.add_tool(TerminalTool::new(project.clone(), cx));
- thread.add_tool(ThinkingTool);
- thread.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model.
- thread
- });
-
- Ok(thread)
+ )
+ }))
},
)??;
-
- // Store the session
- agent.update(cx, |agent, cx| {
- agent.sessions.insert(
- session_id,
- Session {
- thread,
- acp_thread: acp_thread.downgrade(),
- _subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
- this.sessions.remove(acp_thread.session_id());
- }),
- },
- );
- })?;
-
- Ok(acp_thread)
+ agent.update(cx, |agent, cx| agent.register_session(thread, cx))
})
}
@@ -644,166 +967,228 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
) -> Task<Result<acp::PromptResponse>> {
let id = id.expect("UserMessageId is required");
let session_id = params.session_id.clone();
- let agent = self.0.clone();
log::info!("Received prompt request for session: {}", session_id);
log::debug!("Prompt blocks count: {}", params.prompt.len());
- cx.spawn(async move |cx| {
- // Get session
- let (thread, acp_thread) = agent
- .update(cx, |agent, _| {
- agent
- .sessions
- .get_mut(&session_id)
- .map(|s| (s.thread.clone(), s.acp_thread.clone()))
- })?
- .ok_or_else(|| {
- log::error!("Session not found: {}", session_id);
- anyhow::anyhow!("Session not found")
- })?;
- log::debug!("Found session for: {}", session_id);
-
+ self.run_turn(session_id, cx, |thread, cx| {
let content: Vec<UserMessageContent> = params
.prompt
.into_iter()
.map(Into::into)
.collect::<Vec<_>>();
- log::info!("Converted prompt to message: {} chars", content.len());
+ log::debug!("Converted prompt to message: {} chars", content.len());
log::debug!("Message id: {:?}", id);
log::debug!("Message content: {:?}", content);
- // Get model using the ModelSelector capability (always available for agent2)
- // Get the selected model from the thread directly
- let model = thread.read_with(cx, |thread, _| thread.selected_model.clone())?;
-
- // Send to thread
- log::info!("Sending message to thread with model: {:?}", model.name());
- let mut response_stream =
- thread.update(cx, |thread, cx| thread.send(id, content, cx))?;
-
- // Handle response stream and forward to session.acp_thread
- while let Some(result) = response_stream.next().await {
- match result {
- Ok(event) => {
- log::trace!("Received completion event: {:?}", event);
-
- match event {
- AgentResponseEvent::Text(text) => {
- acp_thread.update(cx, |thread, cx| {
- thread.push_assistant_content_block(
- acp::ContentBlock::Text(acp::TextContent {
- text,
- annotations: None,
- }),
- false,
- cx,
- )
- })?;
- }
- AgentResponseEvent::Thinking(text) => {
- acp_thread.update(cx, |thread, cx| {
- thread.push_assistant_content_block(
- acp::ContentBlock::Text(acp::TextContent {
- text,
- annotations: None,
- }),
- true,
- cx,
- )
- })?;
- }
- AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization {
- tool_call,
- options,
- response,
- }) => {
- let recv = acp_thread.update(cx, |thread, cx| {
- thread.request_tool_call_authorization(tool_call, options, cx)
- })?;
- cx.background_spawn(async move {
- if let Some(option) = recv
- .await
- .context("authorization sender was dropped")
- .log_err()
- {
- response
- .send(option)
- .map(|_| anyhow!("authorization receiver was dropped"))
- .log_err();
- }
- })
- .detach();
- }
- AgentResponseEvent::ToolCall(tool_call) => {
- acp_thread.update(cx, |thread, cx| {
- thread.upsert_tool_call(tool_call, cx)
- })?;
- }
- AgentResponseEvent::ToolCallUpdate(update) => {
- acp_thread.update(cx, |thread, cx| {
- thread.update_tool_call(update, cx)
- })??;
- }
- AgentResponseEvent::Stop(stop_reason) => {
- log::debug!("Assistant message complete: {:?}", stop_reason);
- return Ok(acp::PromptResponse { stop_reason });
- }
- }
- }
- Err(e) => {
- log::error!("Error in model response stream: {:?}", e);
- // TODO: Consider sending an error message to the UI
- break;
- }
- }
- }
-
- log::info!("Response stream completed");
- anyhow::Ok(acp::PromptResponse {
- stop_reason: acp::StopReason::EndTurn,
- })
+ thread.update(cx, |thread, cx| thread.send(id, content, cx))
})
}
+ fn resume(
+ &self,
+ session_id: &acp::SessionId,
+ _cx: &App,
+ ) -> Option<Rc<dyn acp_thread::AgentSessionResume>> {
+ Some(Rc::new(NativeAgentSessionResume {
+ connection: self.clone(),
+ session_id: session_id.clone(),
+ }) as _)
+ }
+
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
log::info!("Cancelling on session: {}", session_id);
self.0.update(cx, |agent, cx| {
if let Some(agent) = agent.sessions.get(session_id) {
- agent.thread.update(cx, |thread, _cx| thread.cancel());
+ agent.thread.update(cx, |thread, cx| thread.cancel(cx));
}
});
}
- fn session_editor(
+ fn truncate(
&self,
session_id: &agent_client_protocol::SessionId,
+ cx: &App,
+ ) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
+ self.0.read_with(cx, |agent, _cx| {
+ agent.sessions.get(session_id).map(|session| {
+ Rc::new(NativeAgentSessionTruncate {
+ thread: session.thread.clone(),
+ acp_thread: session.acp_thread.clone(),
+ }) as _
+ })
+ })
+ }
+
+ fn set_title(
+ &self,
+ session_id: &acp::SessionId,
+ _cx: &App,
+ ) -> Option<Rc<dyn acp_thread::AgentSessionSetTitle>> {
+ Some(Rc::new(NativeAgentSessionSetTitle {
+ connection: self.clone(),
+ session_id: session_id.clone(),
+ }) as _)
+ }
+
+ fn telemetry(&self) -> Option<Rc<dyn acp_thread::AgentTelemetry>> {
+ Some(Rc::new(self.clone()) as Rc<dyn acp_thread::AgentTelemetry>)
+ }
+
+ fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+ self
+ }
+}
+
+impl acp_thread::AgentTelemetry for NativeAgentConnection {
+ fn agent_name(&self) -> String {
+ "Zed".into()
+ }
+
+ fn thread_data(
+ &self,
+ session_id: &acp::SessionId,
cx: &mut App,
- ) -> Option<Rc<dyn acp_thread::AgentSessionEditor>> {
- self.0.update(cx, |agent, _cx| {
- agent
- .sessions
- .get(session_id)
- .map(|session| Rc::new(NativeAgentSessionEditor(session.thread.clone())) as _)
+ ) -> Task<Result<serde_json::Value>> {
+ let Some(session) = self.0.read(cx).sessions.get(session_id) else {
+ return Task::ready(Err(anyhow!("Session not found")));
+ };
+
+ let task = session.thread.read(cx).to_db(cx);
+ cx.background_spawn(async move {
+ serde_json::to_value(task.await).context("Failed to serialize thread")
})
}
}
-struct NativeAgentSessionEditor(Entity<Thread>);
+struct NativeAgentSessionTruncate {
+ thread: Entity<Thread>,
+ acp_thread: WeakEntity<AcpThread>,
+}
-impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor {
- fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
- Task::ready(self.0.update(cx, |thread, _cx| thread.truncate(message_id)))
+impl acp_thread::AgentSessionTruncate for NativeAgentSessionTruncate {
+ fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
+ match self.thread.update(cx, |thread, cx| {
+ thread.truncate(message_id.clone(), cx)?;
+ Ok(thread.latest_token_usage())
+ }) {
+ Ok(usage) => {
+ self.acp_thread
+ .update(cx, |thread, cx| {
+ thread.update_token_usage(usage, cx);
+ })
+ .ok();
+ Task::ready(Ok(()))
+ }
+ Err(error) => Task::ready(Err(error)),
+ }
+ }
+}
+
+struct NativeAgentSessionResume {
+ connection: NativeAgentConnection,
+ session_id: acp::SessionId,
+}
+
+impl acp_thread::AgentSessionResume for NativeAgentSessionResume {
+ fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>> {
+ self.connection
+ .run_turn(self.session_id.clone(), cx, |thread, cx| {
+ thread.update(cx, |thread, cx| thread.resume(cx))
+ })
+ }
+}
+
+struct NativeAgentSessionSetTitle {
+ connection: NativeAgentConnection,
+ session_id: acp::SessionId,
+}
+
+impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle {
+ fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>> {
+ let Some(session) = self.connection.0.read(cx).sessions.get(&self.session_id) else {
+ return Task::ready(Err(anyhow!("session not found")));
+ };
+ let thread = session.thread.clone();
+ thread.update(cx, |thread, cx| thread.set_title(title, cx));
+ Task::ready(Ok(()))
+ }
+}
+
+pub struct AcpThreadEnvironment {
+ acp_thread: WeakEntity<AcpThread>,
+}
+
+impl ThreadEnvironment for AcpThreadEnvironment {
+ fn create_terminal(
+ &self,
+ command: String,
+ cwd: Option<PathBuf>,
+ output_byte_limit: Option<u64>,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<Rc<dyn TerminalHandle>>> {
+ let task = self.acp_thread.update(cx, |thread, cx| {
+ thread.create_terminal(command, vec![], vec![], cwd, output_byte_limit, cx)
+ });
+
+ let acp_thread = self.acp_thread.clone();
+ cx.spawn(async move |cx| {
+ let terminal = task?.await?;
+
+ let (drop_tx, drop_rx) = oneshot::channel();
+ let terminal_id = terminal.read_with(cx, |terminal, _cx| terminal.id().clone())?;
+
+ cx.spawn(async move |cx| {
+ drop_rx.await.ok();
+ acp_thread.update(cx, |thread, cx| thread.release_terminal(terminal_id, cx))
+ })
+ .detach();
+
+ let handle = AcpTerminalHandle {
+ terminal,
+ _drop_tx: Some(drop_tx),
+ };
+
+ Ok(Rc::new(handle) as _)
+ })
+ }
+}
+
+pub struct AcpTerminalHandle {
+ terminal: Entity<acp_thread::Terminal>,
+ _drop_tx: Option<oneshot::Sender<()>>,
+}
+
+impl TerminalHandle for AcpTerminalHandle {
+ fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId> {
+ self.terminal.read_with(cx, |term, _cx| term.id().clone())
+ }
+
+ fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>> {
+ self.terminal
+ .read_with(cx, |term, _cx| term.wait_for_exit())
+ }
+
+ fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse> {
+ self.terminal
+ .read_with(cx, |term, cx| term.current_output(cx))
}
}
#[cfg(test)]
mod tests {
+ use crate::HistoryEntryId;
+
use super::*;
- use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo};
+ use acp_thread::{
+ AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo, MentionUri,
+ };
use fs::FakeFs;
use gpui::TestAppContext;
+ use indoc::indoc;
+ use language_model::fake_provider::FakeLanguageModel;
use serde_json::json;
use settings::SettingsStore;
+ use util::path;
#[gpui::test]
async fn test_maintaining_project_context(cx: &mut TestAppContext) {
@@ -1,13 +1,18 @@
mod agent;
+mod db;
+mod history_store;
mod native_agent_server;
mod templates;
mod thread;
+mod tool_schema;
mod tools;
#[cfg(test)]
mod tests;
pub use agent::*;
+pub use db::*;
+pub use history_store::*;
pub use native_agent_server::NativeAgentServer;
pub use templates::*;
pub use thread::*;
@@ -0,0 +1,497 @@
+use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent};
+use acp_thread::UserMessageId;
+use agent::{thread::DetailedSummaryState, thread_store};
+use agent_client_protocol as acp;
+use agent_settings::{AgentProfileId, CompletionMode};
+use anyhow::{Result, anyhow};
+use chrono::{DateTime, Utc};
+use collections::{HashMap, IndexMap};
+use futures::{FutureExt, future::Shared};
+use gpui::{BackgroundExecutor, Global, Task};
+use indoc::indoc;
+use parking_lot::Mutex;
+use serde::{Deserialize, Serialize};
+use sqlez::{
+ bindable::{Bind, Column},
+ connection::Connection,
+ statement::Statement,
+};
+use std::sync::Arc;
+use ui::{App, SharedString};
+use zed_env_vars::ZED_STATELESS;
+
+pub type DbMessage = crate::Message;
+pub type DbSummary = DetailedSummaryState;
+pub type DbLanguageModel = thread_store::SerializedLanguageModel;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DbThreadMetadata {
+ pub id: acp::SessionId,
+ #[serde(alias = "summary")]
+ pub title: SharedString,
+ pub updated_at: DateTime<Utc>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct DbThread {
+ pub title: SharedString,
+ pub messages: Vec<DbMessage>,
+ pub updated_at: DateTime<Utc>,
+ #[serde(default)]
+ pub detailed_summary: Option<SharedString>,
+ #[serde(default)]
+ pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>,
+ #[serde(default)]
+ pub cumulative_token_usage: language_model::TokenUsage,
+ #[serde(default)]
+ pub request_token_usage: HashMap<acp_thread::UserMessageId, language_model::TokenUsage>,
+ #[serde(default)]
+ pub model: Option<DbLanguageModel>,
+ #[serde(default)]
+ pub completion_mode: Option<CompletionMode>,
+ #[serde(default)]
+ pub profile: Option<AgentProfileId>,
+}
+
+impl DbThread {
+ pub const VERSION: &'static str = "0.3.0";
+
+ pub fn from_json(json: &[u8]) -> Result<Self> {
+ let saved_thread_json = serde_json::from_slice::<serde_json::Value>(json)?;
+ match saved_thread_json.get("version") {
+ Some(serde_json::Value::String(version)) => match version.as_str() {
+ Self::VERSION => Ok(serde_json::from_value(saved_thread_json)?),
+ _ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?),
+ },
+ _ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?),
+ }
+ }
+
+ fn upgrade_from_agent_1(thread: agent::SerializedThread) -> Result<Self> {
+ let mut messages = Vec::new();
+ let mut request_token_usage = HashMap::default();
+
+ let mut last_user_message_id = None;
+ for (ix, msg) in thread.messages.into_iter().enumerate() {
+ let message = match msg.role {
+ language_model::Role::User => {
+ let mut content = Vec::new();
+
+ // Convert segments to content
+ for segment in msg.segments {
+ match segment {
+ thread_store::SerializedMessageSegment::Text { text } => {
+ content.push(UserMessageContent::Text(text));
+ }
+ thread_store::SerializedMessageSegment::Thinking { text, .. } => {
+ // User messages don't have thinking segments, but handle gracefully
+ content.push(UserMessageContent::Text(text));
+ }
+ thread_store::SerializedMessageSegment::RedactedThinking { .. } => {
+ // User messages don't have redacted thinking, skip.
+ }
+ }
+ }
+
+ // If no content was added, add context as text if available
+ if content.is_empty() && !msg.context.is_empty() {
+ content.push(UserMessageContent::Text(msg.context));
+ }
+
+ let id = UserMessageId::new();
+ last_user_message_id = Some(id.clone());
+
+ crate::Message::User(UserMessage {
+ // MessageId from old format can't be meaningfully converted, so generate a new one
+ id,
+ content,
+ })
+ }
+ language_model::Role::Assistant => {
+ let mut content = Vec::new();
+
+ // Convert segments to content
+ for segment in msg.segments {
+ match segment {
+ thread_store::SerializedMessageSegment::Text { text } => {
+ content.push(AgentMessageContent::Text(text));
+ }
+ thread_store::SerializedMessageSegment::Thinking {
+ text,
+ signature,
+ } => {
+ content.push(AgentMessageContent::Thinking { text, signature });
+ }
+ thread_store::SerializedMessageSegment::RedactedThinking { data } => {
+ content.push(AgentMessageContent::RedactedThinking(data));
+ }
+ }
+ }
+
+ // Convert tool uses
+ let mut tool_names_by_id = HashMap::default();
+ for tool_use in msg.tool_uses {
+ tool_names_by_id.insert(tool_use.id.clone(), tool_use.name.clone());
+ content.push(AgentMessageContent::ToolUse(
+ language_model::LanguageModelToolUse {
+ id: tool_use.id,
+ name: tool_use.name.into(),
+ raw_input: serde_json::to_string(&tool_use.input)
+ .unwrap_or_default(),
+ input: tool_use.input,
+ is_input_complete: true,
+ },
+ ));
+ }
+
+ // Convert tool results
+ let mut tool_results = IndexMap::default();
+ for tool_result in msg.tool_results {
+ let name = tool_names_by_id
+ .remove(&tool_result.tool_use_id)
+ .unwrap_or_else(|| SharedString::from("unknown"));
+ tool_results.insert(
+ tool_result.tool_use_id.clone(),
+ language_model::LanguageModelToolResult {
+ tool_use_id: tool_result.tool_use_id,
+ tool_name: name.into(),
+ is_error: tool_result.is_error,
+ content: tool_result.content,
+ output: tool_result.output,
+ },
+ );
+ }
+
+ if let Some(last_user_message_id) = &last_user_message_id
+ && let Some(token_usage) = thread.request_token_usage.get(ix).copied()
+ {
+ request_token_usage.insert(last_user_message_id.clone(), token_usage);
+ }
+
+ crate::Message::Agent(AgentMessage {
+ content,
+ tool_results,
+ })
+ }
+ language_model::Role::System => {
+ // Skip system messages as they're not supported in the new format
+ continue;
+ }
+ };
+
+ messages.push(message);
+ }
+
+ Ok(Self {
+ title: thread.summary,
+ messages,
+ updated_at: thread.updated_at,
+ detailed_summary: match thread.detailed_summary_state {
+ DetailedSummaryState::NotGenerated | DetailedSummaryState::Generating { .. } => {
+ None
+ }
+ DetailedSummaryState::Generated { text, .. } => Some(text),
+ },
+ initial_project_snapshot: thread.initial_project_snapshot,
+ cumulative_token_usage: thread.cumulative_token_usage,
+ request_token_usage,
+ model: thread.model,
+ completion_mode: thread.completion_mode,
+ profile: thread.profile,
+ })
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum DataType {
+ #[serde(rename = "json")]
+ Json,
+ #[serde(rename = "zstd")]
+ Zstd,
+}
+
+impl Bind for DataType {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let value = match self {
+ DataType::Json => "json",
+ DataType::Zstd => "zstd",
+ };
+ value.bind(statement, start_index)
+ }
+}
+
+impl Column for DataType {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (value, next_index) = String::column(statement, start_index)?;
+ let data_type = match value.as_str() {
+ "json" => DataType::Json,
+ "zstd" => DataType::Zstd,
+ _ => anyhow::bail!("Unknown data type: {}", value),
+ };
+ Ok((data_type, next_index))
+ }
+}
+
+pub(crate) struct ThreadsDatabase {
+ executor: BackgroundExecutor,
+ connection: Arc<Mutex<Connection>>,
+}
+
+struct GlobalThreadsDatabase(Shared<Task<Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>>);
+
+impl Global for GlobalThreadsDatabase {}
+
+impl ThreadsDatabase {
+ pub fn connect(cx: &mut App) -> Shared<Task<Result<Arc<ThreadsDatabase>, Arc<anyhow::Error>>>> {
+ if cx.has_global::<GlobalThreadsDatabase>() {
+ return cx.global::<GlobalThreadsDatabase>().0.clone();
+ }
+ let executor = cx.background_executor().clone();
+ let task = executor
+ .spawn({
+ let executor = executor.clone();
+ async move {
+ match ThreadsDatabase::new(executor) {
+ Ok(db) => Ok(Arc::new(db)),
+ Err(err) => Err(Arc::new(err)),
+ }
+ }
+ })
+ .shared();
+
+ cx.set_global(GlobalThreadsDatabase(task.clone()));
+ task
+ }
+
+ pub fn new(executor: BackgroundExecutor) -> Result<Self> {
+ let connection = if *ZED_STATELESS {
+ Connection::open_memory(Some("THREAD_FALLBACK_DB"))
+ } else if cfg!(any(feature = "test-support", test)) {
+ // rust stores the name of the test on the current thread.
+ // We use this to automatically create a database that will
+ // be shared within the test (for the test_retrieve_old_thread)
+ // but not with concurrent tests.
+ let thread = std::thread::current();
+ let test_name = thread.name();
+ Connection::open_memory(Some(&format!(
+ "THREAD_FALLBACK_{}",
+ test_name.unwrap_or_default()
+ )))
+ } else {
+ let threads_dir = paths::data_dir().join("threads");
+ std::fs::create_dir_all(&threads_dir)?;
+ let sqlite_path = threads_dir.join("threads.db");
+ Connection::open_file(&sqlite_path.to_string_lossy())
+ };
+
+ connection.exec(indoc! {"
+ CREATE TABLE IF NOT EXISTS threads (
+ id TEXT PRIMARY KEY,
+ summary TEXT NOT NULL,
+ updated_at TEXT NOT NULL,
+ data_type TEXT NOT NULL,
+ data BLOB NOT NULL
+ )
+ "})?()
+ .map_err(|e| anyhow!("Failed to create threads table: {}", e))?;
+
+ let db = Self {
+ executor,
+ connection: Arc::new(Mutex::new(connection)),
+ };
+
+ Ok(db)
+ }
+
+ fn save_thread_sync(
+ connection: &Arc<Mutex<Connection>>,
+ id: acp::SessionId,
+ thread: DbThread,
+ ) -> Result<()> {
+ const COMPRESSION_LEVEL: i32 = 3;
+
+ #[derive(Serialize)]
+ struct SerializedThread {
+ #[serde(flatten)]
+ thread: DbThread,
+ version: &'static str,
+ }
+
+ let title = thread.title.to_string();
+ let updated_at = thread.updated_at.to_rfc3339();
+ let json_data = serde_json::to_string(&SerializedThread {
+ thread,
+ version: DbThread::VERSION,
+ })?;
+
+ let connection = connection.lock();
+
+ let compressed = zstd::encode_all(json_data.as_bytes(), COMPRESSION_LEVEL)?;
+ let data_type = DataType::Zstd;
+ let data = compressed;
+
+ let mut insert = connection.exec_bound::<(Arc<str>, String, String, DataType, Vec<u8>)>(indoc! {"
+ INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?)
+ "})?;
+
+ insert((id.0, title, updated_at, data_type, data))?;
+
+ Ok(())
+ }
+
+ pub fn list_threads(&self) -> Task<Result<Vec<DbThreadMetadata>>> {
+ let connection = self.connection.clone();
+
+ self.executor.spawn(async move {
+ let connection = connection.lock();
+
+ let mut select =
+ connection.select_bound::<(), (Arc<str>, String, String)>(indoc! {"
+ SELECT id, summary, updated_at FROM threads ORDER BY updated_at DESC
+ "})?;
+
+ let rows = select(())?;
+ let mut threads = Vec::new();
+
+ for (id, summary, updated_at) in rows {
+ threads.push(DbThreadMetadata {
+ id: acp::SessionId(id),
+ title: summary.into(),
+ updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc),
+ });
+ }
+
+ Ok(threads)
+ })
+ }
+
+ pub fn load_thread(&self, id: acp::SessionId) -> Task<Result<Option<DbThread>>> {
+ let connection = self.connection.clone();
+
+ self.executor.spawn(async move {
+ let connection = connection.lock();
+ let mut select = connection.select_bound::<Arc<str>, (DataType, Vec<u8>)>(indoc! {"
+ SELECT data_type, data FROM threads WHERE id = ? LIMIT 1
+ "})?;
+
+ let rows = select(id.0)?;
+ if let Some((data_type, data)) = rows.into_iter().next() {
+ let json_data = match data_type {
+ DataType::Zstd => {
+ let decompressed = zstd::decode_all(&data[..])?;
+ String::from_utf8(decompressed)?
+ }
+ DataType::Json => String::from_utf8(data)?,
+ };
+ let thread = DbThread::from_json(json_data.as_bytes())?;
+ Ok(Some(thread))
+ } else {
+ Ok(None)
+ }
+ })
+ }
+
+ pub fn save_thread(&self, id: acp::SessionId, thread: DbThread) -> Task<Result<()>> {
+ let connection = self.connection.clone();
+
+ self.executor
+ .spawn(async move { Self::save_thread_sync(&connection, id, thread) })
+ }
+
+ pub fn delete_thread(&self, id: acp::SessionId) -> Task<Result<()>> {
+ let connection = self.connection.clone();
+
+ self.executor.spawn(async move {
+ let connection = connection.lock();
+
+ let mut delete = connection.exec_bound::<Arc<str>>(indoc! {"
+ DELETE FROM threads WHERE id = ?
+ "})?;
+
+ delete(id.0)?;
+
+ Ok(())
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::*;
+ use agent::MessageSegment;
+ use agent::context::LoadedContext;
+ use client::Client;
+ use fs::FakeFs;
+ use gpui::AppContext;
+ use gpui::TestAppContext;
+ use http_client::FakeHttpClient;
+ use language_model::Role;
+ use project::Project;
+ use settings::SettingsStore;
+
+ fn init_test(cx: &mut TestAppContext) {
+ env_logger::try_init().ok();
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ Project::init_settings(cx);
+ language::init(cx);
+
+ let http_client = FakeHttpClient::with_404_response();
+ let clock = Arc::new(clock::FakeSystemClock::new());
+ let client = Client::new(clock, http_client, cx);
+ agent::init(cx);
+ agent_settings::init(cx);
+ language_model::init(client, cx);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, [], cx).await;
+
+ // Save a thread using the old agent.
+ let thread_store = cx.new(|cx| agent::ThreadStore::fake(project, cx));
+ let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx));
+ thread.update(cx, |thread, cx| {
+ thread.insert_message(
+ Role::User,
+ vec![MessageSegment::Text("Hey!".into())],
+ LoadedContext::default(),
+ vec![],
+ false,
+ cx,
+ );
+ thread.insert_message(
+ Role::Assistant,
+ vec![MessageSegment::Text("How're you doing?".into())],
+ LoadedContext::default(),
+ vec![],
+ false,
+ cx,
+ )
+ });
+ thread_store
+ .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
+ .await
+ .unwrap();
+
+ // Open that same thread using the new agent.
+ let db = cx.update(ThreadsDatabase::connect).await.unwrap();
+ let threads = db.list_threads().await.unwrap();
+ assert_eq!(threads.len(), 1);
+ let thread = db
+ .load_thread(threads[0].id.clone())
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(thread.messages[0].to_markdown(), "## User\n\nHey!\n");
+ assert_eq!(
+ thread.messages[1].to_markdown(),
+ "## Assistant\n\nHow're you doing?\n"
+ );
+ }
+}
@@ -0,0 +1,357 @@
+use crate::{DbThreadMetadata, ThreadsDatabase};
+use acp_thread::MentionUri;
+use agent_client_protocol as acp;
+use anyhow::{Context as _, Result, anyhow};
+use assistant_context::{AssistantContext, SavedContextMetadata};
+use chrono::{DateTime, Utc};
+use db::kvp::KEY_VALUE_STORE;
+use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*};
+use itertools::Itertools;
+use paths::contexts_dir;
+use serde::{Deserialize, Serialize};
+use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
+use ui::ElementId;
+use util::ResultExt as _;
+
+const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
+const RECENTLY_OPENED_THREADS_KEY: &str = "recent-agent-threads";
+const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
+
+const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread");
+
+#[derive(Clone, Debug)]
+pub enum HistoryEntry {
+ AcpThread(DbThreadMetadata),
+ TextThread(SavedContextMetadata),
+}
+
+impl HistoryEntry {
+ pub fn updated_at(&self) -> DateTime<Utc> {
+ match self {
+ HistoryEntry::AcpThread(thread) => thread.updated_at,
+ HistoryEntry::TextThread(context) => context.mtime.to_utc(),
+ }
+ }
+
+ pub fn id(&self) -> HistoryEntryId {
+ match self {
+ HistoryEntry::AcpThread(thread) => HistoryEntryId::AcpThread(thread.id.clone()),
+ HistoryEntry::TextThread(context) => HistoryEntryId::TextThread(context.path.clone()),
+ }
+ }
+
+ pub fn mention_uri(&self) -> MentionUri {
+ match self {
+ HistoryEntry::AcpThread(thread) => MentionUri::Thread {
+ id: thread.id.clone(),
+ name: thread.title.to_string(),
+ },
+ HistoryEntry::TextThread(context) => MentionUri::TextThread {
+ path: context.path.as_ref().to_owned(),
+ name: context.title.to_string(),
+ },
+ }
+ }
+
+ pub fn title(&self) -> &SharedString {
+ match self {
+ HistoryEntry::AcpThread(thread) if thread.title.is_empty() => DEFAULT_TITLE,
+ HistoryEntry::AcpThread(thread) => &thread.title,
+ HistoryEntry::TextThread(context) => &context.title,
+ }
+ }
+}
+
+/// Generic identifier for a history entry.
+#[derive(Clone, PartialEq, Eq, Debug, Hash)]
+pub enum HistoryEntryId {
+ AcpThread(acp::SessionId),
+ TextThread(Arc<Path>),
+}
+
+impl Into<ElementId> for HistoryEntryId {
+ fn into(self) -> ElementId {
+ match self {
+ HistoryEntryId::AcpThread(session_id) => ElementId::Name(session_id.0.into()),
+ HistoryEntryId::TextThread(path) => ElementId::Path(path),
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+enum SerializedRecentOpen {
+ AcpThread(String),
+ TextThread(String),
+}
+
+pub struct HistoryStore {
+ threads: Vec<DbThreadMetadata>,
+ entries: Vec<HistoryEntry>,
+ context_store: Entity<assistant_context::ContextStore>,
+ recently_opened_entries: VecDeque<HistoryEntryId>,
+ _subscriptions: Vec<gpui::Subscription>,
+ _save_recently_opened_entries_task: Task<()>,
+}
+
+impl HistoryStore {
+ pub fn new(
+ context_store: Entity<assistant_context::ContextStore>,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))];
+
+ cx.spawn(async move |this, cx| {
+ let entries = Self::load_recently_opened_entries(cx).await;
+ this.update(cx, |this, cx| {
+ if let Some(entries) = entries.log_err() {
+ this.recently_opened_entries = entries;
+ }
+
+ this.reload(cx);
+ })
+ .ok();
+ })
+ .detach();
+
+ Self {
+ context_store,
+ recently_opened_entries: VecDeque::default(),
+ threads: Vec::default(),
+ entries: Vec::default(),
+ _subscriptions: subscriptions,
+ _save_recently_opened_entries_task: Task::ready(()),
+ }
+ }
+
+ pub fn thread_from_session_id(&self, session_id: &acp::SessionId) -> Option<&DbThreadMetadata> {
+ self.threads.iter().find(|thread| &thread.id == session_id)
+ }
+
+ pub fn delete_thread(
+ &mut self,
+ id: acp::SessionId,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ let database_future = ThreadsDatabase::connect(cx);
+ cx.spawn(async move |this, cx| {
+ let database = database_future.await.map_err(|err| anyhow!(err))?;
+ database.delete_thread(id.clone()).await?;
+ this.update(cx, |this, cx| this.reload(cx))
+ })
+ }
+
+ pub fn delete_text_thread(
+ &mut self,
+ path: Arc<Path>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ self.context_store.update(cx, |context_store, cx| {
+ context_store.delete_local_context(path, cx)
+ })
+ }
+
+ pub fn load_text_thread(
+ &self,
+ path: Arc<Path>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Entity<AssistantContext>>> {
+ self.context_store.update(cx, |context_store, cx| {
+ context_store.open_local_context(path, cx)
+ })
+ }
+
+ pub fn reload(&self, cx: &mut Context<Self>) {
+ let database_future = ThreadsDatabase::connect(cx);
+ cx.spawn(async move |this, cx| {
+ let threads = database_future
+ .await
+ .map_err(|err| anyhow!(err))?
+ .list_threads()
+ .await?;
+
+ this.update(cx, |this, cx| {
+ if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES {
+ for thread in threads
+ .iter()
+ .take(MAX_RECENTLY_OPENED_ENTRIES - this.recently_opened_entries.len())
+ .rev()
+ {
+ this.push_recently_opened_entry(
+ HistoryEntryId::AcpThread(thread.id.clone()),
+ cx,
+ )
+ }
+ }
+ this.threads = threads;
+ this.update_entries(cx);
+ })
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn update_entries(&mut self, cx: &mut Context<Self>) {
+ #[cfg(debug_assertions)]
+ if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
+ return;
+ }
+ let mut history_entries = Vec::new();
+ history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread));
+ history_entries.extend(
+ self.context_store
+ .read(cx)
+ .unordered_contexts()
+ .cloned()
+ .map(HistoryEntry::TextThread),
+ );
+
+ history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
+ self.entries = history_entries;
+ cx.notify()
+ }
+
+ pub fn is_empty(&self, _cx: &App) -> bool {
+ self.entries.is_empty()
+ }
+
+ pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
+ #[cfg(debug_assertions)]
+ if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
+ return Vec::new();
+ }
+
+ let thread_entries = self.threads.iter().flat_map(|thread| {
+ self.recently_opened_entries
+ .iter()
+ .enumerate()
+ .flat_map(|(index, entry)| match entry {
+ HistoryEntryId::AcpThread(id) if &thread.id == id => {
+ Some((index, HistoryEntry::AcpThread(thread.clone())))
+ }
+ _ => None,
+ })
+ });
+
+ let context_entries =
+ self.context_store
+ .read(cx)
+ .unordered_contexts()
+ .flat_map(|context| {
+ self.recently_opened_entries
+ .iter()
+ .enumerate()
+ .flat_map(|(index, entry)| match entry {
+ HistoryEntryId::TextThread(path) if &context.path == path => {
+ Some((index, HistoryEntry::TextThread(context.clone())))
+ }
+ _ => None,
+ })
+ });
+
+ thread_entries
+ .chain(context_entries)
+ // optimization to halt iteration early
+ .take(self.recently_opened_entries.len())
+ .sorted_unstable_by_key(|(index, _)| *index)
+ .map(|(_, entry)| entry)
+ .collect()
+ }
+
+ fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
+ let serialized_entries = self
+ .recently_opened_entries
+ .iter()
+ .filter_map(|entry| match entry {
+ HistoryEntryId::TextThread(path) => path.file_name().map(|file| {
+ SerializedRecentOpen::TextThread(file.to_string_lossy().to_string())
+ }),
+ HistoryEntryId::AcpThread(id) => {
+ Some(SerializedRecentOpen::AcpThread(id.to_string()))
+ }
+ })
+ .collect::<Vec<_>>();
+
+ self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
+ let content = serde_json::to_string(&serialized_entries).unwrap();
+ cx.background_executor()
+ .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
+ .await;
+
+ if cfg!(any(feature = "test-support", test)) {
+ return;
+ }
+ KEY_VALUE_STORE
+ .write_kvp(RECENTLY_OPENED_THREADS_KEY.to_owned(), content)
+ .await
+ .log_err();
+ });
+ }
+
+ fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
+ cx.background_spawn(async move {
+ if cfg!(any(feature = "test-support", test)) {
+ anyhow::bail!("history store does not persist in tests");
+ }
+ let json = KEY_VALUE_STORE
+ .read_kvp(RECENTLY_OPENED_THREADS_KEY)?
+ .unwrap_or("[]".to_string());
+ let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&json)
+ .context("deserializing persisted agent panel navigation history")?
+ .into_iter()
+ .take(MAX_RECENTLY_OPENED_ENTRIES)
+ .flat_map(|entry| match entry {
+ SerializedRecentOpen::AcpThread(id) => Some(HistoryEntryId::AcpThread(
+ acp::SessionId(id.as_str().into()),
+ )),
+ SerializedRecentOpen::TextThread(file_name) => Some(
+ HistoryEntryId::TextThread(contexts_dir().join(file_name).into()),
+ ),
+ })
+ .collect();
+ Ok(entries)
+ })
+ }
+
+ pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
+ self.recently_opened_entries
+ .retain(|old_entry| old_entry != &entry);
+ self.recently_opened_entries.push_front(entry);
+ self.recently_opened_entries
+ .truncate(MAX_RECENTLY_OPENED_ENTRIES);
+ self.save_recently_opened_entries(cx);
+ }
+
+ pub fn remove_recently_opened_thread(&mut self, id: acp::SessionId, cx: &mut Context<Self>) {
+ self.recently_opened_entries.retain(
+ |entry| !matches!(entry, HistoryEntryId::AcpThread(thread_id) if thread_id == &id),
+ );
+ self.save_recently_opened_entries(cx);
+ }
+
+ pub fn replace_recently_opened_text_thread(
+ &mut self,
+ old_path: &Path,
+ new_path: &Arc<Path>,
+ cx: &mut Context<Self>,
+ ) {
+ for entry in &mut self.recently_opened_entries {
+ match entry {
+ HistoryEntryId::TextThread(path) if path.as_ref() == old_path => {
+ *entry = HistoryEntryId::TextThread(new_path.clone());
+ break;
+ }
+ _ => {}
+ }
+ }
+ self.save_recently_opened_entries(cx);
+ }
+
+ pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
+ self.recently_opened_entries
+ .retain(|old_entry| old_entry != entry);
+ self.save_recently_opened_entries(cx);
+ }
+
+ pub fn entries(&self) -> impl Iterator<Item = HistoryEntry> {
+ self.entries.iter().cloned()
+ }
+}
@@ -1,55 +1,56 @@
-use std::{path::Path, rc::Rc, sync::Arc};
+use std::{any::Any, path::Path, rc::Rc, sync::Arc};
-use agent_servers::AgentServer;
+use agent_servers::{AgentServer, AgentServerDelegate};
use anyhow::Result;
use fs::Fs;
-use gpui::{App, Entity, Task};
-use project::Project;
+use gpui::{App, Entity, SharedString, Task};
use prompt_store::PromptStore;
-use crate::{NativeAgent, NativeAgentConnection, templates::Templates};
+use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates};
#[derive(Clone)]
pub struct NativeAgentServer {
fs: Arc<dyn Fs>,
+ history: Entity<HistoryStore>,
}
impl NativeAgentServer {
- pub fn new(fs: Arc<dyn Fs>) -> Self {
- Self { fs }
+ pub fn new(fs: Arc<dyn Fs>, history: Entity<HistoryStore>) -> Self {
+ Self { fs, history }
}
}
impl AgentServer for NativeAgentServer {
- fn name(&self) -> &'static str {
- "Native Agent"
+ fn telemetry_id(&self) -> &'static str {
+ "zed"
}
- fn empty_state_headline(&self) -> &'static str {
- "Native Agent"
- }
-
- fn empty_state_message(&self) -> &'static str {
- "How can I help you today?"
+ fn name(&self) -> SharedString {
+ "Zed Agent".into()
}
fn logo(&self) -> ui::IconName {
- // Using the ZedAssistant icon as it's the native built-in agent
- ui::IconName::ZedAssistant
+ ui::IconName::ZedAgent
}
fn connect(
&self,
- _root_dir: &Path,
- project: &Entity<Project>,
+ _root_dir: Option<&Path>,
+ delegate: AgentServerDelegate,
cx: &mut App,
- ) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
- log::info!(
+ ) -> Task<
+ Result<(
+ Rc<dyn acp_thread::AgentConnection>,
+ Option<task::SpawnInTerminal>,
+ )>,
+ > {
+ log::debug!(
"NativeAgentServer::connect called for path: {:?}",
_root_dir
);
- let project = project.clone();
+ let project = delegate.project().clone();
let fs = self.fs.clone();
+ let history = self.history.clone();
let prompt_store = PromptStore::global(cx);
cx.spawn(async move |cx| {
log::debug!("Creating templates for native agent");
@@ -57,13 +58,70 @@ impl AgentServer for NativeAgentServer {
let prompt_store = prompt_store.await?;
log::debug!("Creating native agent entity");
- let agent = NativeAgent::new(project, templates, Some(prompt_store), fs, cx).await?;
+ let agent =
+ NativeAgent::new(project, history, templates, Some(prompt_store), fs, cx).await?;
// Create the connection wrapper
let connection = NativeAgentConnection(agent);
- log::info!("NativeAgentServer connection established successfully");
+ log::debug!("NativeAgentServer connection established successfully");
- Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
+ Ok((
+ Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>,
+ None,
+ ))
})
}
+
+ fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+ self
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use assistant_context::ContextStore;
+ use gpui::AppContext;
+
+ agent_servers::e2e_tests::common_e2e_tests!(
+ async |fs, project, cx| {
+ let auth = cx.update(|cx| {
+ prompt_store::init(cx);
+ terminal::init(cx);
+
+ let registry = language_model::LanguageModelRegistry::read_global(cx);
+ let auth = registry
+ .provider(&language_model::ANTHROPIC_PROVIDER_ID)
+ .unwrap()
+ .authenticate(cx);
+
+ cx.spawn(async move |_| auth.await)
+ });
+
+ auth.await.unwrap();
+
+ cx.update(|cx| {
+ let registry = language_model::LanguageModelRegistry::global(cx);
+
+ registry.update(cx, |registry, cx| {
+ registry.select_default_model(
+ Some(&language_model::SelectedModel {
+ provider: language_model::ANTHROPIC_PROVIDER_ID,
+ model: language_model::LanguageModelId("claude-sonnet-4-latest".into()),
+ }),
+ cx,
+ );
+ });
+ });
+
+ let history = cx.update(|cx| {
+ let context_store = cx.new(move |cx| ContextStore::fake(project.clone(), cx));
+ cx.new(move |cx| HistoryStore::new(context_store, cx))
+ });
+
+ NativeAgentServer::new(fs.clone(), history)
+ },
+ allow_option_id = "allow"
+ );
}
@@ -62,7 +62,7 @@ fn contains(
handlebars::RenderError::new("contains: missing or invalid query parameter")
})?;
- if list.contains(&query) {
+ if list.contains(query) {
out.write("true")?;
}
@@ -1,46 +1,63 @@
use super::*;
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId};
-use action_log::ActionLog;
use agent_client_protocol::{self as acp};
use agent_settings::AgentProfileId;
use anyhow::Result;
use client::{Client, UserStore};
+use cloud_llm_client::CompletionIntent;
+use collections::IndexMap;
+use context_server::{ContextServer, ContextServerCommand, ContextServerId};
use fs::{FakeFs, Fs};
-use futures::channel::mpsc::UnboundedReceiver;
+use futures::{
+ StreamExt,
+ channel::{
+ mpsc::{self, UnboundedReceiver},
+ oneshot,
+ },
+};
use gpui::{
App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient,
};
use indoc::indoc;
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
- LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, Role, StopReason,
- fake_provider::FakeLanguageModel,
+ LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequest,
+ LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolSchemaFormat,
+ LanguageModelToolUse, MessageContent, Role, StopReason, fake_provider::FakeLanguageModel,
+};
+use pretty_assertions::assert_eq;
+use project::{
+ Project, context_server_store::ContextServerStore, project_settings::ProjectSettings,
};
-use project::Project;
use prompt_store::ProjectContext;
use reqwest_client::ReqwestClient;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
-use settings::SettingsStore;
-use smol::stream::StreamExt;
-use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc, time::Duration};
+use settings::{Settings, SettingsStore};
+use std::{path::Path, rc::Rc, sync::Arc, time::Duration};
use util::path;
mod test_tools;
use test_tools::*;
#[gpui::test]
-#[ignore = "can't run on CI yet"]
async fn test_echo(cx: &mut TestAppContext) {
- let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
+ let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+ let fake_model = model.as_fake();
let events = thread
.update(cx, |thread, cx| {
thread.send(UserMessageId::new(), ["Testing: Reply with 'Hello'"], cx)
})
- .collect()
- .await;
+ .unwrap();
+ cx.run_until_parked();
+ fake_model.send_last_completion_stream_text_chunk("Hello");
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
+ fake_model.end_last_completion_stream();
+
+ let events = events.collect().await;
thread.update(cx, |thread, _cx| {
assert_eq!(
thread.last_message().unwrap().to_markdown(),
@@ -55,9 +72,9 @@ async fn test_echo(cx: &mut TestAppContext) {
}
#[gpui::test]
-#[ignore = "can't run on CI yet"]
async fn test_thinking(cx: &mut TestAppContext) {
- let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await;
+ let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+ let fake_model = model.as_fake();
let events = thread
.update(cx, |thread, cx| {
@@ -72,8 +89,18 @@ async fn test_thinking(cx: &mut TestAppContext) {
cx,
)
})
- .collect()
- .await;
+ .unwrap();
+ cx.run_until_parked();
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Thinking {
+ text: "Think".to_string(),
+ signature: None,
+ });
+ fake_model.send_last_completion_stream_text_chunk("Hello");
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn));
+ fake_model.end_last_completion_stream();
+
+ let events = events.collect().await;
thread.update(cx, |thread, _cx| {
assert_eq!(
thread.last_message().unwrap().to_markdown(),
@@ -98,11 +125,15 @@ async fn test_system_prompt(cx: &mut TestAppContext) {
} = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
- project_context.borrow_mut().shell = "test-shell".into();
- thread.update(cx, |thread, _| thread.add_tool(EchoTool));
- thread.update(cx, |thread, cx| {
- thread.send(UserMessageId::new(), ["abc"], cx)
+ project_context.update(cx, |project_context, _cx| {
+ project_context.shell = "test-shell".into()
});
+ thread.update(cx, |thread, _| thread.add_tool(EchoTool));
+ thread
+ .update(cx, |thread, cx| {
+ thread.send(UserMessageId::new(), ["abc"], cx)
+ })
+ .unwrap();
cx.run_until_parked();
let mut pending_completions = fake_model.pending_completions();
assert_eq!(
@@ -130,7 +161,141 @@ async fn test_system_prompt(cx: &mut TestAppContext) {
}
#[gpui::test]
-#[ignore = "can't run on CI yet"]
+async fn test_prompt_caching(cx: &mut TestAppContext) {
+ let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+ let fake_model = model.as_fake();
+
+ // Send initial user message and verify it's cached
+ thread
+ .update(cx, |thread, cx| {
+ thread.send(UserMessageId::new(), ["Message 1"], cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+
+ let completion = fake_model.pending_completions().pop().unwrap();
+ assert_eq!(
+ completion.messages[1..],
+ vec![LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["Message 1".into()],
+ cache: true
+ }]
+ );
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
+ "Response to Message 1".into(),
+ ));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ // Send another user message and verify only the latest is cached
+ thread
+ .update(cx, |thread, cx| {
+ thread.send(UserMessageId::new(), ["Message 2"], cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+
+ let completion = fake_model.pending_completions().pop().unwrap();
+ assert_eq!(
+ completion.messages[1..],
+ vec![
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["Message 1".into()],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::Assistant,
+ content: vec!["Response to Message 1".into()],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["Message 2".into()],
+ cache: true
+ }
+ ]
+ );
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
+ "Response to Message 2".into(),
+ ));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ // Simulate a tool call and verify that the latest tool result is cached
+ thread.update(cx, |thread, _| thread.add_tool(EchoTool));
+ thread
+ .update(cx, |thread, cx| {
+ thread.send(UserMessageId::new(), ["Use the echo tool"], cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+
+ let tool_use = LanguageModelToolUse {
+ id: "tool_1".into(),
+ name: EchoTool::name().into(),
+ raw_input: json!({"text": "test"}).to_string(),
+ input: json!({"text": "test"}),
+ is_input_complete: true,
+ };
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ let completion = fake_model.pending_completions().pop().unwrap();
+ let tool_result = LanguageModelToolResult {
+ tool_use_id: "tool_1".into(),
+ tool_name: EchoTool::name().into(),
+ is_error: false,
+ content: "test".into(),
+ output: Some("test".into()),
+ };
+ assert_eq!(
+ completion.messages[1..],
+ vec![
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["Message 1".into()],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::Assistant,
+ content: vec!["Response to Message 1".into()],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["Message 2".into()],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::Assistant,
+ content: vec!["Response to Message 2".into()],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["Use the echo tool".into()],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::Assistant,
+ content: vec![MessageContent::ToolUse(tool_use)],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec![MessageContent::ToolResult(tool_result)],
+ cache: true
+ }
+ ]
+ );
+}
+
+#[gpui::test]
+#[cfg_attr(not(feature = "e2e"), ignore)]
async fn test_basic_tool_calls(cx: &mut TestAppContext) {
let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
@@ -144,6 +309,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
cx,
)
})
+ .unwrap()
.collect()
.await;
assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
@@ -151,7 +317,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
// Test a tool calls that's likely to complete *after* streaming stops.
let events = thread
.update(cx, |thread, cx| {
- thread.remove_tool(&AgentTool::name(&EchoTool));
+ thread.remove_tool(&EchoTool::name());
thread.add_tool(DelayTool);
thread.send(
UserMessageId::new(),
@@ -162,6 +328,7 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
cx,
)
})
+ .unwrap()
.collect()
.await;
assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
@@ -188,19 +355,21 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
}
#[gpui::test]
-#[ignore = "can't run on CI yet"]
+#[cfg_attr(not(feature = "e2e"), ignore)]
async fn test_streaming_tool_calls(cx: &mut TestAppContext) {
let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
// Test a tool call that's likely to complete *before* streaming stops.
- let mut events = thread.update(cx, |thread, cx| {
- thread.add_tool(WordListTool);
- thread.send(UserMessageId::new(), ["Test the word_list tool."], cx)
- });
+ let mut events = thread
+ .update(cx, |thread, cx| {
+ thread.add_tool(WordListTool);
+ thread.send(UserMessageId::new(), ["Test the word_list tool."], cx)
+ })
+ .unwrap();
let mut saw_partial_tool_use = false;
while let Some(event) = events.next().await {
- if let Ok(AgentResponseEvent::ToolCall(tool_call)) = event {
+ if let Ok(ThreadEvent::ToolCall(tool_call)) = event {
thread.update(cx, |thread, _cx| {
// Look for a tool use in the thread's last message
let message = thread.last_message().unwrap();
@@ -242,15 +411,17 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
- let mut events = thread.update(cx, |thread, cx| {
- thread.add_tool(ToolRequiringPermission);
- thread.send(UserMessageId::new(), ["abc"], cx)
- });
+ let mut events = thread
+ .update(cx, |thread, cx| {
+ thread.add_tool(ToolRequiringPermission);
+ thread.send(UserMessageId::new(), ["abc"], cx)
+ })
+ .unwrap();
cx.run_until_parked();
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "tool_id_1".into(),
- name: ToolRequiringPermission.name().into(),
+ name: ToolRequiringPermission::name().into(),
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
@@ -259,7 +430,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "tool_id_2".into(),
- name: ToolRequiringPermission.name().into(),
+ name: ToolRequiringPermission::name().into(),
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
@@ -290,17 +461,17 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
vec![
language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
- tool_name: ToolRequiringPermission.name().into(),
+ tool_name: ToolRequiringPermission::name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
}),
language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
- tool_name: ToolRequiringPermission.name().into(),
+ tool_name: ToolRequiringPermission::name().into(),
is_error: true,
content: "Permission to run tool denied by user".into(),
- output: None
+ output: Some("Permission to run tool denied by user".into())
})
]
);
@@ -309,7 +480,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "tool_id_3".into(),
- name: ToolRequiringPermission.name().into(),
+ name: ToolRequiringPermission::name().into(),
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
@@ -331,7 +502,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
vec![language_model::MessageContent::ToolResult(
LanguageModelToolResult {
tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
- tool_name: ToolRequiringPermission.name().into(),
+ tool_name: ToolRequiringPermission::name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
@@ -343,7 +514,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "tool_id_4".into(),
- name: ToolRequiringPermission.name().into(),
+ name: ToolRequiringPermission::name().into(),
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
@@ -358,7 +529,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
vec![language_model::MessageContent::ToolResult(
LanguageModelToolResult {
tool_use_id: "tool_id_4".into(),
- tool_name: ToolRequiringPermission.name().into(),
+ tool_name: ToolRequiringPermission::name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
@@ -372,9 +543,11 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
- let mut events = thread.update(cx, |thread, cx| {
- thread.send(UserMessageId::new(), ["abc"], cx)
- });
+ let mut events = thread
+ .update(cx, |thread, cx| {
+ thread.send(UserMessageId::new(), ["abc"], cx)
+ })
+ .unwrap();
cx.run_until_parked();
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
@@ -394,16 +567,197 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed));
}
-async fn expect_tool_call(
- events: &mut UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>>,
-) -> acp::ToolCall {
+#[gpui::test]
+async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
+ let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+ let fake_model = model.as_fake();
+
+ let events = thread
+ .update(cx, |thread, cx| {
+ thread.add_tool(EchoTool);
+ thread.send(UserMessageId::new(), ["abc"], cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+ let tool_use = LanguageModelToolUse {
+ id: "tool_id_1".into(),
+ name: EchoTool::name().into(),
+ raw_input: "{}".into(),
+ input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
+ is_input_complete: true,
+ };
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
+ fake_model.end_last_completion_stream();
+
+ cx.run_until_parked();
+ let completion = fake_model.pending_completions().pop().unwrap();
+ let tool_result = LanguageModelToolResult {
+ tool_use_id: "tool_id_1".into(),
+ tool_name: EchoTool::name().into(),
+ is_error: false,
+ content: "def".into(),
+ output: Some("def".into()),
+ };
+ assert_eq!(
+ completion.messages[1..],
+ vec![
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["abc".into()],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::Assistant,
+ content: vec![MessageContent::ToolUse(tool_use.clone())],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec![MessageContent::ToolResult(tool_result.clone())],
+ cache: true
+ },
+ ]
+ );
+
+ // Simulate reaching tool use limit.
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
+ cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
+ ));
+ fake_model.end_last_completion_stream();
+ let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
+ assert!(
+ last_event
+ .unwrap_err()
+ .is::<language_model::ToolUseLimitReachedError>()
+ );
+
+ let events = thread.update(cx, |thread, cx| thread.resume(cx)).unwrap();
+ cx.run_until_parked();
+ let completion = fake_model.pending_completions().pop().unwrap();
+ assert_eq!(
+ completion.messages[1..],
+ vec![
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["abc".into()],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::Assistant,
+ content: vec![MessageContent::ToolUse(tool_use)],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec![MessageContent::ToolResult(tool_result)],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["Continue where you left off".into()],
+ cache: true
+ }
+ ]
+ );
+
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text("Done".into()));
+ fake_model.end_last_completion_stream();
+ events.collect::<Vec<_>>().await;
+ thread.read_with(cx, |thread, _cx| {
+ assert_eq!(
+ thread.last_message().unwrap().to_markdown(),
+ indoc! {"
+ ## Assistant
+
+ Done
+ "}
+ )
+ });
+}
+
+#[gpui::test]
+async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
+ let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
+ let fake_model = model.as_fake();
+
+ let events = thread
+ .update(cx, |thread, cx| {
+ thread.add_tool(EchoTool);
+ thread.send(UserMessageId::new(), ["abc"], cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+
+ let tool_use = LanguageModelToolUse {
+ id: "tool_id_1".into(),
+ name: EchoTool::name().into(),
+ raw_input: "{}".into(),
+ input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
+ is_input_complete: true,
+ };
+ let tool_result = LanguageModelToolResult {
+ tool_use_id: "tool_id_1".into(),
+ tool_name: EchoTool::name().into(),
+ is_error: false,
+ content: "def".into(),
+ output: Some("def".into()),
+ };
+ fake_model
+ .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
+ cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
+ ));
+ fake_model.end_last_completion_stream();
+ let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
+ assert!(
+ last_event
+ .unwrap_err()
+ .is::<language_model::ToolUseLimitReachedError>()
+ );
+
+ thread
+ .update(cx, |thread, cx| {
+ thread.send(UserMessageId::new(), vec!["ghi"], cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+ let completion = fake_model.pending_completions().pop().unwrap();
+ assert_eq!(
+ completion.messages[1..],
+ vec![
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["abc".into()],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::Assistant,
+ content: vec![MessageContent::ToolUse(tool_use)],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec![MessageContent::ToolResult(tool_result)],
+ cache: false
+ },
+ LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["ghi".into()],
+ cache: true
+ }
+ ]
+ );
+}
+
+async fn expect_tool_call(events: &mut UnboundedReceiver<Result<ThreadEvent>>) -> acp::ToolCall {
let event = events
.next()
.await
.expect("no tool call authorization event received")
.unwrap();
match event {
- AgentResponseEvent::ToolCall(tool_call) => return tool_call,
+ ThreadEvent::ToolCall(tool_call) => tool_call,
event => {
panic!("Unexpected event {event:?}");
}
@@ -411,7 +765,7 @@ async fn expect_tool_call(
}
async fn expect_tool_call_update_fields(
- events: &mut UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>>,
+ events: &mut UnboundedReceiver<Result<ThreadEvent>>,
) -> acp::ToolCallUpdate {
let event = events
.next()
@@ -419,9 +773,7 @@ async fn expect_tool_call_update_fields(
.expect("no tool call authorization event received")
.unwrap();
match event {
- AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => {
- return update;
- }
+ ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => update,
event => {
panic!("Unexpected event {event:?}");
}
@@ -429,7 +781,7 @@ async fn expect_tool_call_update_fields(
}
async fn next_tool_call_authorization(
- events: &mut UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>>,
+ events: &mut UnboundedReceiver<Result<ThreadEvent>>,
) -> ToolCallAuthorization {
loop {
let event = events
@@ -437,7 +789,7 @@ async fn next_tool_call_authorization(
.await
.expect("no tool call authorization event received")
.unwrap();
- if let AgentResponseEvent::ToolCallAuthorization(tool_call_authorization) = event {
+ if let ThreadEvent::ToolCallAuthorization(tool_call_authorization) = event {
let permission_kinds = tool_call_authorization
.options
.iter()
@@ -457,7 +809,7 @@ async fn next_tool_call_authorization(
}
#[gpui::test]
-#[ignore = "can't run on CI yet"]
+#[cfg_attr(not(feature = "e2e"), ignore)]
async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
@@ -475,6 +827,7 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
cx,
)
})
+ .unwrap()
.collect()
.await;
@@ -522,14 +875,14 @@ async fn test_profiles(cx: &mut TestAppContext) {
"test-1": {
"name": "Test Profile 1",
"tools": {
- EchoTool.name(): true,
- DelayTool.name(): true,
+ EchoTool::name(): true,
+ DelayTool::name(): true,
}
},
"test-2": {
"name": "Test Profile 2",
"tools": {
- InfiniteTool.name(): true,
+ InfiniteTool::name(): true,
}
}
}
@@ -542,10 +895,12 @@ async fn test_profiles(cx: &mut TestAppContext) {
cx.run_until_parked();
// Test that test-1 profile (default) has echo and delay tools
- thread.update(cx, |thread, cx| {
- thread.set_profile(AgentProfileId("test-1".into()));
- thread.send(UserMessageId::new(), ["test"], cx);
- });
+ thread
+ .update(cx, |thread, cx| {
+ thread.set_profile(AgentProfileId("test-1".into()));
+ thread.send(UserMessageId::new(), ["test"], cx)
+ })
+ .unwrap();
cx.run_until_parked();
let mut pending_completions = fake_model.pending_completions();
@@ -556,14 +911,16 @@ async fn test_profiles(cx: &mut TestAppContext) {
.iter()
.map(|tool| tool.name.clone())
.collect();
- assert_eq!(tool_names, vec![DelayTool.name(), EchoTool.name()]);
+ assert_eq!(tool_names, vec![DelayTool::name(), EchoTool::name()]);
fake_model.end_last_completion_stream();
// Switch to test-2 profile, and verify that it has only the infinite tool.
- thread.update(cx, |thread, cx| {
- thread.set_profile(AgentProfileId("test-2".into()));
- thread.send(UserMessageId::new(), ["test2"], cx)
- });
+ thread
+ .update(cx, |thread, cx| {
+ thread.set_profile(AgentProfileId("test-2".into()));
+ thread.send(UserMessageId::new(), ["test2"], cx)
+ })
+ .unwrap();
cx.run_until_parked();
let mut pending_completions = fake_model.pending_completions();
assert_eq!(pending_completions.len(), 1);
@@ -573,60 +930,399 @@ async fn test_profiles(cx: &mut TestAppContext) {
.iter()
.map(|tool| tool.name.clone())
.collect();
- assert_eq!(tool_names, vec![InfiniteTool.name()]);
+ assert_eq!(tool_names, vec![InfiniteTool::name()]);
}
#[gpui::test]
-#[ignore = "can't run on CI yet"]
-async fn test_cancellation(cx: &mut TestAppContext) {
- let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
-
- let mut events = thread.update(cx, |thread, cx| {
- thread.add_tool(InfiniteTool);
- thread.add_tool(EchoTool);
- thread.send(
- UserMessageId::new(),
- ["Call the echo tool, then call the infinite tool, then explain their output"],
- cx,
- )
- });
+async fn test_mcp_tools(cx: &mut TestAppContext) {
+ let ThreadTest {
+ model,
+ thread,
+ context_server_store,
+ fs,
+ ..
+ } = setup(cx, TestModel::Fake).await;
+ let fake_model = model.as_fake();
- // Wait until both tools are called.
- let mut expected_tools = vec!["Echo", "Infinite Tool"];
- let mut echo_id = None;
- let mut echo_completed = false;
- while let Some(event) = events.next().await {
- match event.unwrap() {
- AgentResponseEvent::ToolCall(tool_call) => {
- assert_eq!(tool_call.title, expected_tools.remove(0));
- if tool_call.title == "Echo" {
- echo_id = Some(tool_call.id);
+ // Override profiles and wait for settings to be loaded.
+ fs.insert_file(
+ paths::settings_file(),
+ json!({
+ "agent": {
+ "always_allow_tool_actions": true,
+ "profiles": {
+ "test": {
+ "name": "Test Profile",
+ "enable_all_context_servers": true,
+ "tools": {
+ EchoTool::name(): true,
+ }
+ },
}
}
- AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
- acp::ToolCallUpdate {
- id,
- fields:
- acp::ToolCallUpdateFields {
- status: Some(acp::ToolCallStatus::Completed),
- ..
- },
- },
- )) if Some(&id) == echo_id.as_ref() => {
- echo_completed = true;
- }
- _ => {}
- }
-
- if expected_tools.is_empty() && echo_completed {
- break;
+ })
+ .to_string()
+ .into_bytes(),
+ )
+ .await;
+ cx.run_until_parked();
+ thread.update(cx, |thread, _| {
+ thread.set_profile(AgentProfileId("test".into()))
+ });
+
+ let mut mcp_tool_calls = setup_context_server(
+ "test_server",
+ vec![context_server::types::Tool {
+ name: "echo".into(),
+ description: None,
+ input_schema: serde_json::to_value(
+ EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
+ )
+ .unwrap(),
+ output_schema: None,
+ annotations: None,
+ }],
+ &context_server_store,
+ cx,
+ );
+
+ let events = thread.update(cx, |thread, cx| {
+ thread.send(UserMessageId::new(), ["Hey"], cx).unwrap()
+ });
+ cx.run_until_parked();
+
+ // Simulate the model calling the MCP tool.
+ let completion = fake_model.pending_completions().pop().unwrap();
+ assert_eq!(tool_names_for_completion(&completion), vec!["echo"]);
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
+ LanguageModelToolUse {
+ id: "tool_1".into(),
+ name: "echo".into(),
+ raw_input: json!({"text": "test"}).to_string(),
+ input: json!({"text": "test"}),
+ is_input_complete: true,
+ },
+ ));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap();
+ assert_eq!(tool_call_params.name, "echo");
+ assert_eq!(tool_call_params.arguments, Some(json!({"text": "test"})));
+ tool_call_response
+ .send(context_server::types::CallToolResponse {
+ content: vec![context_server::types::ToolResponseContent::Text {
+ text: "test".into(),
+ }],
+ is_error: None,
+ meta: None,
+ structured_content: None,
+ })
+ .unwrap();
+ cx.run_until_parked();
+
+ assert_eq!(tool_names_for_completion(&completion), vec!["echo"]);
+ fake_model.send_last_completion_stream_text_chunk("Done!");
+ fake_model.end_last_completion_stream();
+ events.collect::<Vec<_>>().await;
+
+ // Send again after adding the echo tool, ensuring the name collision is resolved.
+ let events = thread.update(cx, |thread, cx| {
+ thread.add_tool(EchoTool);
+ thread.send(UserMessageId::new(), ["Go"], cx).unwrap()
+ });
+ cx.run_until_parked();
+ let completion = fake_model.pending_completions().pop().unwrap();
+ assert_eq!(
+ tool_names_for_completion(&completion),
+ vec!["echo", "test_server_echo"]
+ );
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
+ LanguageModelToolUse {
+ id: "tool_2".into(),
+ name: "test_server_echo".into(),
+ raw_input: json!({"text": "mcp"}).to_string(),
+ input: json!({"text": "mcp"}),
+ is_input_complete: true,
+ },
+ ));
+ fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
+ LanguageModelToolUse {
+ id: "tool_3".into(),
+ name: "echo".into(),
+ raw_input: json!({"text": "native"}).to_string(),
+ input: json!({"text": "native"}),
+ is_input_complete: true,
+ },
+ ));
+ fake_model.end_last_completion_stream();
+ cx.run_until_parked();
+
+ let (tool_call_params, tool_call_response) = mcp_tool_calls.next().await.unwrap();
+ assert_eq!(tool_call_params.name, "echo");
+ assert_eq!(tool_call_params.arguments, Some(json!({"text": "mcp"})));
+ tool_call_response
+ .send(context_server::types::CallToolResponse {
+ content: vec![context_server::types::ToolResponseContent::Text { text: "mcp".into() }],
+ is_error: None,
+ meta: None,
+ structured_content: None,
+ })
+ .unwrap();
+ cx.run_until_parked();
+
+ // Ensure the tool results were inserted with the correct names.
+ let completion = fake_model.pending_completions().pop().unwrap();
+ assert_eq!(
+ completion.messages.last().unwrap().content,
+ vec![
+ MessageContent::ToolResult(LanguageModelToolResult {
+ tool_use_id: "tool_3".into(),
+ tool_name: "echo".into(),
+ is_error: false,
+ content: "native".into(),
+ output: Some("native".into()),
+ },),
+ MessageContent::ToolResult(LanguageModelToolResult {
+ tool_use_id: "tool_2".into(),
+ tool_name: "test_server_echo".into(),
+ is_error: false,
+ content: "mcp".into(),
+ output: Some("mcp".into()),
+ },),
+ ]
+ );
+ fake_model.end_last_completion_stream();
+ events.collect::<Vec<_>>().await;
+}
+
+#[gpui::test]
+async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
+ let ThreadTest {
+ model,
+ thread,
+ context_server_store,
+ fs,
+ ..
+ } = setup(cx, TestModel::Fake).await;
+ let fake_model = model.as_fake();
+
+ // Set up a profile with all tools enabled
+ fs.insert_file(
+ paths::settings_file(),
+ json!({
+ "agent": {
+ "profiles": {
+ "test": {
+ "name": "Test Profile",
+ "enable_all_context_servers": true,
+ "tools": {
+ EchoTool::name(): true,
+ DelayTool::name(): true,
+ WordListTool::name(): true,
+ ToolRequiringPermission::name(): true,
+ InfiniteTool::name(): true,
+ }
+ },
+ }
+ }
+ })
+ .to_string()
+ .into_bytes(),
+ )
+ .await;
+ cx.run_until_parked();
+
+ thread.update(cx, |thread, _| {
+ thread.set_profile(AgentProfileId("test".into()));
+ thread.add_tool(EchoTool);
+ thread.add_tool(DelayTool);
+ thread.add_tool(WordListTool);
+ thread.add_tool(ToolRequiringPermission);
+ thread.add_tool(InfiniteTool);
+ });
+
+ // Set up multiple context servers with some overlapping tool names
+ let _server1_calls = setup_context_server(
+ "xxx",
+ vec![
+ context_server::types::Tool {
+ name: "echo".into(), // Conflicts with native EchoTool
+ description: None,
+ input_schema: serde_json::to_value(
+ EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
+ )
+ .unwrap(),
+ output_schema: None,
+ annotations: None,
+ },
+ context_server::types::Tool {
+ name: "unique_tool_1".into(),
+ description: None,
+ input_schema: json!({"type": "object", "properties": {}}),
+ output_schema: None,
+ annotations: None,
+ },
+ ],
+ &context_server_store,
+ cx,
+ );
+
+ let _server2_calls = setup_context_server(
+ "yyy",
+ vec![
+ context_server::types::Tool {
+ name: "echo".into(), // Also conflicts with native EchoTool
+ description: None,
+ input_schema: serde_json::to_value(
+ EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema),
+ )
+ .unwrap(),
+ output_schema: None,
+ annotations: None,
+ },
+ context_server::types::Tool {
+ name: "unique_tool_2".into(),
+ description: None,
+ input_schema: json!({"type": "object", "properties": {}}),
+ output_schema: None,
+ annotations: None,
+ },
+ context_server::types::Tool {
+ name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2),
+ description: None,
+ input_schema: json!({"type": "object", "properties": {}}),
+ output_schema: None,
+ annotations: None,
+ },
+ context_server::types::Tool {
+ name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1),
+ description: None,
+ input_schema: json!({"type": "object", "properties": {}}),
+ output_schema: None,
+ annotations: None,
+ },
+ ],
+ &context_server_store,
+ cx,
+ );
+ let _server3_calls = setup_context_server(
+ "zzz",
+ vec![
+ context_server::types::Tool {
+ name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2),
+ description: None,
+ input_schema: json!({"type": "object", "properties": {}}),
+ output_schema: None,
+ annotations: None,
+ },
+ context_server::types::Tool {
+ name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1),
+ description: None,
+ input_schema: json!({"type": "object", "properties": {}}),
+ output_schema: None,
+ annotations: None,
+ },
+ context_server::types::Tool {
+ name: "c".repeat(MAX_TOOL_NAME_LENGTH + 1),
+ description: None,
+ input_schema: json!({"type": "object", "properties": {}}),
+ output_schema: None,
+ annotations: None,
+ },
+ ],
+ &context_server_store,
+ cx,
+ );
+
+ thread
+ .update(cx, |thread, cx| {
+ thread.send(UserMessageId::new(), ["Go"], cx)
+ })
+ .unwrap();
+ cx.run_until_parked();
+ let completion = fake_model.pending_completions().pop().unwrap();
+ assert_eq!(
+ tool_names_for_completion(&completion),
+ vec![
+ "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
+ "delay",
+ "echo",
+ "infinite",
+ "tool_requiring_permission",
+ "unique_tool_1",
+ "unique_tool_2",
+ "word_list",
+ "xxx_echo",
+ "y_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "yyy_echo",
+ "z_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ ]
+ );
+}
+
+#[gpui::test]
+#[cfg_attr(not(feature = "e2e"), ignore)]
+async fn test_cancellation(cx: &mut TestAppContext) {
+ let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
+
+ let mut events = thread
+ .update(cx, |thread, cx| {
+ thread.add_tool(InfiniteTool);
+ thread.add_tool(EchoTool);
+ thread.send(
+ UserMessageId::new(),
+ ["Call the echo tool, then call the infinite tool, then explain their output"],
+ cx,
+ )
+ })
+ .unwrap();
+
+ // Wait until both tools are called.
+ let mut expected_tools = vec!["Echo", "Infinite Tool"];
+ let mut echo_id = None;
+ let mut echo_completed = false;
+ while let Some(event) = events.next().await {
+ match event.unwrap() {
+ ThreadEvent::ToolCall(tool_call) => {
+ assert_eq!(tool_call.title, expected_tools.remove(0));
+ if tool_call.title == "Echo" {
+ echo_id = Some(tool_call.id);
+ }
+ }
+ ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
+ acp::ToolCallUpdate {
+ id,
+ fields:
+ acp::ToolCallUpdateFields {
+ status: Some(acp::ToolCallStatus::Completed),
+ ..
+ },
+ },
+ )) if Some(&id) == echo_id.as_ref() => {
+ echo_completed = true;
+ }
+ _ => {}
+ }
+
+ if expected_tools.is_empty() && echo_completed {
+ break;
}
}
// Cancel the current send and ensure that the event stream is closed, even
// if one of the tools is still running.
- thread.update(cx, |thread, _cx| thread.cancel());
- events.collect::<Vec<_>>().await;
+ thread.update(cx, |thread, cx| thread.cancel(cx));
+ let events = events.collect::<Vec<_>>().await;
+ let last_event = events.last();
+ assert!(
+ matches!(
+ last_event,
+ Some(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled)))
+ ),
+ "unexpected event {last_event:?}"
+ );
// Ensure we can still send a new message after cancellation.
let events = thread
@@ -7,7 +7,7 @@ use std::future;
#[derive(JsonSchema, Serialize, Deserialize)]
pub struct EchoToolInput {
/// The text to echo.
- text: String,
+ pub text: String,
}
pub struct EchoTool;
@@ -16,15 +16,19 @@ impl AgentTool for EchoTool {
type Input = EchoToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "echo".into()
+ fn name() -> &'static str {
+ "echo"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Other
}
- fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ _input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
"Echo".into()
}
@@ -51,11 +55,15 @@ impl AgentTool for DelayTool {
type Input = DelayToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "delay".into()
+ fn name() -> &'static str {
+ "delay"
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
if let Ok(input) = input {
format!("Delay {}ms", input.ms).into()
} else {
@@ -63,7 +71,7 @@ impl AgentTool for DelayTool {
}
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Other
}
@@ -92,15 +100,19 @@ impl AgentTool for ToolRequiringPermission {
type Input = ToolRequiringPermissionInput;
type Output = String;
- fn name(&self) -> SharedString {
- "tool_requiring_permission".into()
+ fn name() -> &'static str {
+ "tool_requiring_permission"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Other
}
- fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ _input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
"This tool requires permission".into()
}
@@ -127,15 +139,19 @@ impl AgentTool for InfiniteTool {
type Input = InfiniteToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "infinite".into()
+ fn name() -> &'static str {
+ "infinite"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Other
}
- fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ _input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
"Infinite Tool".into()
}
@@ -178,15 +194,19 @@ impl AgentTool for WordListTool {
type Input = WordListInput;
type Output = String;
- fn name(&self) -> SharedString {
- "word_list".into()
+ fn name() -> &'static str {
+ "word_list"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Other
}
- fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ _input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
"List of random words".into()
}
@@ -1,38 +1,103 @@
-use crate::{ContextServerRegistry, SystemPromptTemplate, Template, Templates};
+use crate::{
+ ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread,
+ DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool,
+ ListDirectoryTool, MovePathTool, NowTool, OpenTool, ReadFileTool, SystemPromptTemplate,
+ Template, Templates, TerminalTool, ThinkingTool, WebSearchTool,
+};
use acp_thread::{MentionUri, UserMessageId};
use action_log::ActionLog;
+use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot};
use agent_client_protocol as acp;
-use agent_settings::{AgentProfileId, AgentSettings};
+use agent_settings::{
+ AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode,
+ SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT,
+};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::adapt_schema_to_format;
-use cloud_llm_client::{CompletionIntent, CompletionMode};
-use collections::IndexMap;
+use chrono::{DateTime, Utc};
+use client::{ModelRequestUsage, RequestUsage};
+use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
+use collections::{HashMap, HashSet, IndexMap};
use fs::Fs;
use futures::{
+ FutureExt,
channel::{mpsc, oneshot},
+ future::Shared,
stream::FuturesUnordered,
};
-use gpui::{App, Context, Entity, SharedString, Task};
+use git::repository::DiffType;
+use gpui::{
+ App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
+};
use language_model::{
- LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelImage,
- LanguageModelProviderId, LanguageModelRequest, LanguageModelRequestMessage,
- LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent,
- LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, StopReason,
+ LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt,
+ LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
+ LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
+ LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
+ LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage,
+};
+use project::{
+ Project,
+ git_store::{GitStore, RepositoryState},
};
-use project::Project;
use prompt_store::ProjectContext;
use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
-use std::{cell::RefCell, collections::BTreeMap, path::Path, rc::Rc, sync::Arc};
-use std::{fmt::Write, ops::Range};
-use util::{ResultExt, markdown::MarkdownCodeBlock};
+use std::{
+ collections::BTreeMap,
+ ops::RangeInclusive,
+ path::Path,
+ rc::Rc,
+ sync::Arc,
+ time::{Duration, Instant},
+};
+use std::{fmt::Write, path::PathBuf};
+use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock};
+use uuid::Uuid;
+
+const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user";
+pub const MAX_TOOL_NAME_LENGTH: usize = 64;
+
+/// The ID of the user prompt that initiated a request.
+///
+/// This equates to the user physically submitting a message to the model (e.g., by pressing the Enter key).
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
+pub struct PromptId(Arc<str>);
+
+impl PromptId {
+ pub fn new() -> Self {
+ Self(Uuid::new_v4().to_string().into())
+ }
+}
+
+impl std::fmt::Display for PromptId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+pub(crate) const MAX_RETRY_ATTEMPTS: u8 = 4;
+pub(crate) const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
+
+#[derive(Debug, Clone)]
+enum RetryStrategy {
+ ExponentialBackoff {
+ initial_delay: Duration,
+ max_attempts: u8,
+ },
+ Fixed {
+ delay: Duration,
+ max_attempts: u8,
+ },
+}
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Message {
User(UserMessage),
Agent(AgentMessage),
+ Resume,
}
impl Message {
@@ -43,21 +108,41 @@ impl Message {
}
}
+ pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
+ match self {
+ Message::User(message) => vec![message.to_request()],
+ Message::Agent(message) => message.to_request(),
+ Message::Resume => vec![LanguageModelRequestMessage {
+ role: Role::User,
+ content: vec!["Continue where you left off".into()],
+ cache: false,
+ }],
+ }
+ }
+
pub fn to_markdown(&self) -> String {
match self {
Message::User(message) => message.to_markdown(),
Message::Agent(message) => message.to_markdown(),
+ Message::Resume => "[resume]\n".into(),
+ }
+ }
+
+ pub fn role(&self) -> Role {
+ match self {
+ Message::User(_) | Message::Resume => Role::User,
+ Message::Agent(_) => Role::Assistant,
}
}
}
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserMessage {
pub id: UserMessageId,
pub content: Vec<UserMessageContent>,
}
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum UserMessageContent {
Text(String),
Mention { uri: MentionUri, content: String },
@@ -79,9 +164,9 @@ impl UserMessage {
}
UserMessageContent::Mention { uri, content } => {
if !content.is_empty() {
- let _ = write!(&mut markdown, "{}\n\n{}\n", uri.as_link(), content);
+ let _ = writeln!(&mut markdown, "{}\n\n{}", uri.as_link(), content);
} else {
- let _ = write!(&mut markdown, "{}\n", uri.as_link());
+ let _ = writeln!(&mut markdown, "{}", uri.as_link());
}
}
}
@@ -102,14 +187,18 @@ impl UserMessage {
They are up-to-date and don't need to be re-read.\n\n";
const OPEN_FILES_TAG: &str = "<files>";
+ const OPEN_DIRECTORIES_TAG: &str = "<directories>";
const OPEN_SYMBOLS_TAG: &str = "<symbols>";
+ const OPEN_SELECTIONS_TAG: &str = "<selections>";
const OPEN_THREADS_TAG: &str = "<threads>";
const OPEN_FETCH_TAG: &str = "<fetched_urls>";
const OPEN_RULES_TAG: &str =
"<rules>\nThe user has specified the following rules that should be applied:\n";
let mut file_context = OPEN_FILES_TAG.to_string();
+ let mut directory_context = OPEN_DIRECTORIES_TAG.to_string();
let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
+ let mut selection_context = OPEN_SELECTIONS_TAG.to_string();
let mut thread_context = OPEN_THREADS_TAG.to_string();
let mut fetch_context = OPEN_FETCH_TAG.to_string();
let mut rules_context = OPEN_RULES_TAG.to_string();
@@ -124,29 +213,52 @@ impl UserMessage {
}
UserMessageContent::Mention { uri, content } => {
match uri {
- MentionUri::File(path) => {
+ MentionUri::File { abs_path } => {
write!(
- &mut symbol_context,
+ &mut file_context,
"\n{}",
MarkdownCodeBlock {
- tag: &codeblock_tag(&path, None),
+ tag: &codeblock_tag(abs_path, None),
text: &content.to_string(),
}
)
.ok();
}
+ MentionUri::PastedImage => {
+ debug_panic!("pasted image URI should not be used in mention content")
+ }
+ MentionUri::Directory { .. } => {
+ write!(&mut directory_context, "\n{}\n", content).ok();
+ }
MentionUri::Symbol {
- path, line_range, ..
+ abs_path: path,
+ line_range,
+ ..
+ } => {
+ write!(
+ &mut symbol_context,
+ "\n{}",
+ MarkdownCodeBlock {
+ tag: &codeblock_tag(path, Some(line_range)),
+ text: content
+ }
+ )
+ .ok();
}
- | MentionUri::Selection {
- path, line_range, ..
+ MentionUri::Selection {
+ abs_path: path,
+ line_range,
+ ..
} => {
write!(
- &mut rules_context,
+ &mut selection_context,
"\n{}",
MarkdownCodeBlock {
- tag: &codeblock_tag(&path, Some(line_range)),
- text: &content
+ tag: &codeblock_tag(
+ path.as_deref().unwrap_or("Untitled".as_ref()),
+ Some(line_range)
+ ),
+ text: content
}
)
.ok();
@@ -163,7 +275,7 @@ impl UserMessage {
"\n{}",
MarkdownCodeBlock {
tag: "",
- text: &content
+ text: content
}
)
.ok();
@@ -189,6 +301,13 @@ impl UserMessage {
.push(language_model::MessageContent::Text(file_context));
}
+ if directory_context.len() > OPEN_DIRECTORIES_TAG.len() {
+ directory_context.push_str("</directories>\n");
+ message
+ .content
+ .push(language_model::MessageContent::Text(directory_context));
+ }
+
if symbol_context.len() > OPEN_SYMBOLS_TAG.len() {
symbol_context.push_str("</symbols>\n");
message
@@ -196,6 +315,13 @@ impl UserMessage {
.push(language_model::MessageContent::Text(symbol_context));
}
+ if selection_context.len() > OPEN_SELECTIONS_TAG.len() {
+ selection_context.push_str("</selections>\n");
+ message
+ .content
+ .push(language_model::MessageContent::Text(selection_context));
+ }
+
if thread_context.len() > OPEN_THREADS_TAG.len() {
thread_context.push_str("</threads>\n");
message
@@ -231,7 +357,7 @@ impl UserMessage {
}
}
-fn codeblock_tag(full_path: &Path, line_range: Option<&Range<u32>>) -> String {
+fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive<u32>>) -> String {
let mut result = String::new();
if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) {
@@ -241,10 +367,10 @@ fn codeblock_tag(full_path: &Path, line_range: Option<&Range<u32>>) -> String {
let _ = write!(result, "{}", full_path.display());
if let Some(range) = line_range {
- if range.start == range.end {
- let _ = write!(result, ":{}", range.start + 1);
+ if range.start() == range.end() {
+ let _ = write!(result, ":{}", range.start() + 1);
} else {
- let _ = write!(result, ":{}-{}", range.start + 1, range.end + 1);
+ let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1);
}
}
@@ -269,9 +395,6 @@ impl AgentMessage {
AgentMessageContent::RedactedThinking(_) => {
markdown.push_str("<redacted_thinking />\n")
}
- AgentMessageContent::Image(_) => {
- markdown.push_str("<image />\n");
- }
AgentMessageContent::ToolUse(tool_use) => {
markdown.push_str(&format!(
"**Tool Use**: {} (ID: {})\n",
@@ -320,62 +443,77 @@ impl AgentMessage {
}
pub fn to_request(&self) -> Vec<LanguageModelRequestMessage> {
- let mut content = Vec::with_capacity(self.content.len());
+ let mut assistant_message = LanguageModelRequestMessage {
+ role: Role::Assistant,
+ content: Vec::with_capacity(self.content.len()),
+ cache: false,
+ };
for chunk in &self.content {
- let chunk = match chunk {
+ match chunk {
AgentMessageContent::Text(text) => {
- language_model::MessageContent::Text(text.clone())
+ assistant_message
+ .content
+ .push(language_model::MessageContent::Text(text.clone()));
}
AgentMessageContent::Thinking { text, signature } => {
- language_model::MessageContent::Thinking {
- text: text.clone(),
- signature: signature.clone(),
- }
+ assistant_message
+ .content
+ .push(language_model::MessageContent::Thinking {
+ text: text.clone(),
+ signature: signature.clone(),
+ });
}
AgentMessageContent::RedactedThinking(value) => {
- language_model::MessageContent::RedactedThinking(value.clone())
- }
- AgentMessageContent::ToolUse(value) => {
- language_model::MessageContent::ToolUse(value.clone())
+ assistant_message.content.push(
+ language_model::MessageContent::RedactedThinking(value.clone()),
+ );
}
- AgentMessageContent::Image(value) => {
- language_model::MessageContent::Image(value.clone())
+ AgentMessageContent::ToolUse(tool_use) => {
+ if self.tool_results.contains_key(&tool_use.id) {
+ assistant_message
+ .content
+ .push(language_model::MessageContent::ToolUse(tool_use.clone()));
+ }
}
};
- content.push(chunk);
}
- let mut messages = vec![LanguageModelRequestMessage {
- role: Role::Assistant,
- content,
+ let mut user_message = LanguageModelRequestMessage {
+ role: Role::User,
+ content: Vec::new(),
cache: false,
- }];
+ };
- if !self.tool_results.is_empty() {
- let mut tool_results = Vec::with_capacity(self.tool_results.len());
- for tool_result in self.tool_results.values() {
- tool_results.push(language_model::MessageContent::ToolResult(
- tool_result.clone(),
- ));
+ for tool_result in self.tool_results.values() {
+ let mut tool_result = tool_result.clone();
+ // Surprisingly, the API fails if we return an empty string here.
+ // It thinks we are sending a tool use without a tool result.
+ if tool_result.content.is_empty() {
+ tool_result.content = "<Tool returned an empty string>".into();
}
- messages.push(LanguageModelRequestMessage {
- role: Role::User,
- content: tool_results,
- cache: false,
- });
+ user_message
+ .content
+ .push(language_model::MessageContent::ToolResult(tool_result));
}
+ let mut messages = Vec::new();
+ if !assistant_message.content.is_empty() {
+ messages.push(assistant_message);
+ }
+ if !user_message.content.is_empty() {
+ messages.push(user_message);
+ }
messages
}
}
-#[derive(Default, Debug, Clone, PartialEq, Eq)]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentMessage {
pub content: Vec<AgentMessageContent>,
pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
}
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AgentMessageContent {
Text(String),
Thinking {
@@ -383,72 +521,470 @@ pub enum AgentMessageContent {
signature: Option<String>,
},
RedactedThinking(String),
- Image(LanguageModelImage),
ToolUse(LanguageModelToolUse),
}
+pub trait TerminalHandle {
+ fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId>;
+ fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse>;
+ fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>>;
+}
+
+pub trait ThreadEnvironment {
+ fn create_terminal(
+ &self,
+ command: String,
+ cwd: Option<PathBuf>,
+ output_byte_limit: Option<u64>,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<Rc<dyn TerminalHandle>>>;
+}
+
#[derive(Debug)]
-pub enum AgentResponseEvent {
- Text(String),
- Thinking(String),
+pub enum ThreadEvent {
+ UserMessage(UserMessage),
+ AgentText(String),
+ AgentThinking(String),
ToolCall(acp::ToolCall),
ToolCallUpdate(acp_thread::ToolCallUpdate),
ToolCallAuthorization(ToolCallAuthorization),
+ Retry(acp_thread::RetryStatus),
Stop(acp::StopReason),
}
+#[derive(Debug)]
+pub struct NewTerminal {
+ pub command: String,
+ pub output_byte_limit: Option<u64>,
+ pub cwd: Option<PathBuf>,
+ pub response: oneshot::Sender<Result<Entity<acp_thread::Terminal>>>,
+}
+
#[derive(Debug)]
pub struct ToolCallAuthorization {
- pub tool_call: acp::ToolCall,
+ pub tool_call: acp::ToolCallUpdate,
pub options: Vec<acp::PermissionOption>,
pub response: oneshot::Sender<acp::PermissionOptionId>,
}
+#[derive(Debug, thiserror::Error)]
+enum CompletionError {
+ #[error("max tokens")]
+ MaxTokens,
+ #[error("refusal")]
+ Refusal,
+ #[error(transparent)]
+ Other(#[from] anyhow::Error),
+}
+
pub struct Thread {
+ id: acp::SessionId,
+ prompt_id: PromptId,
+ updated_at: DateTime<Utc>,
+ title: Option<SharedString>,
+ pending_title_generation: Option<Task<()>>,
+ summary: Option<SharedString>,
messages: Vec<Message>,
completion_mode: CompletionMode,
/// Holds the task that handles agent interaction until the end of the turn.
/// Survives across multiple requests as the model performs tool calls and
/// we run tools, report their results.
- running_turn: Option<Task<()>>,
+ running_turn: Option<RunningTurn>,
pending_message: Option<AgentMessage>,
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
+ tool_use_limit_reached: bool,
+ request_token_usage: HashMap<UserMessageId, language_model::TokenUsage>,
+ #[allow(unused)]
+ cumulative_token_usage: TokenUsage,
+ #[allow(unused)]
+ initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
context_server_registry: Entity<ContextServerRegistry>,
profile_id: AgentProfileId,
- project_context: Rc<RefCell<ProjectContext>>,
+ project_context: Entity<ProjectContext>,
templates: Arc<Templates>,
- pub selected_model: Arc<dyn LanguageModel>,
- project: Entity<Project>,
- action_log: Entity<ActionLog>,
+ model: Option<Arc<dyn LanguageModel>>,
+ summarization_model: Option<Arc<dyn LanguageModel>>,
+ prompt_capabilities_tx: watch::Sender<acp::PromptCapabilities>,
+ pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
+ pub(crate) project: Entity<Project>,
+ pub(crate) action_log: Entity<ActionLog>,
}
impl Thread {
+ fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
+ let image = model.map_or(true, |model| model.supports_images());
+ acp::PromptCapabilities {
+ image,
+ audio: false,
+ embedded_context: true,
+ }
+ }
+
pub fn new(
project: Entity<Project>,
- project_context: Rc<RefCell<ProjectContext>>,
+ project_context: Entity<ProjectContext>,
context_server_registry: Entity<ContextServerRegistry>,
- action_log: Entity<ActionLog>,
templates: Arc<Templates>,
- default_model: Arc<dyn LanguageModel>,
+ model: Option<Arc<dyn LanguageModel>>,
cx: &mut Context<Self>,
) -> Self {
let profile_id = AgentSettings::get_global(cx).default_profile.clone();
+ let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
+ let (prompt_capabilities_tx, prompt_capabilities_rx) =
+ watch::channel(Self::prompt_capabilities(model.as_deref()));
Self {
+ id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
+ prompt_id: PromptId::new(),
+ updated_at: Utc::now(),
+ title: None,
+ pending_title_generation: None,
+ summary: None,
messages: Vec::new(),
- completion_mode: CompletionMode::Normal,
+ completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
+ running_turn: None,
+ pending_message: None,
+ tools: BTreeMap::default(),
+ tool_use_limit_reached: false,
+ request_token_usage: HashMap::default(),
+ cumulative_token_usage: TokenUsage::default(),
+ initial_project_snapshot: {
+ let project_snapshot = Self::project_snapshot(project.clone(), cx);
+ cx.foreground_executor()
+ .spawn(async move { Some(project_snapshot.await) })
+ .shared()
+ },
+ context_server_registry,
+ profile_id,
+ project_context,
+ templates,
+ model,
+ summarization_model: None,
+ prompt_capabilities_tx,
+ prompt_capabilities_rx,
+ project,
+ action_log,
+ }
+ }
+
+ pub fn id(&self) -> &acp::SessionId {
+ &self.id
+ }
+
+ pub fn replay(
+ &mut self,
+ cx: &mut Context<Self>,
+ ) -> mpsc::UnboundedReceiver<Result<ThreadEvent>> {
+ let (tx, rx) = mpsc::unbounded();
+ let stream = ThreadEventStream(tx);
+ for message in &self.messages {
+ match message {
+ Message::User(user_message) => stream.send_user_message(user_message),
+ Message::Agent(assistant_message) => {
+ for content in &assistant_message.content {
+ match content {
+ AgentMessageContent::Text(text) => stream.send_text(text),
+ AgentMessageContent::Thinking { text, .. } => {
+ stream.send_thinking(text)
+ }
+ AgentMessageContent::RedactedThinking(_) => {}
+ AgentMessageContent::ToolUse(tool_use) => {
+ self.replay_tool_call(
+ tool_use,
+ assistant_message.tool_results.get(&tool_use.id),
+ &stream,
+ cx,
+ );
+ }
+ }
+ }
+ }
+ Message::Resume => {}
+ }
+ }
+ rx
+ }
+
+ fn replay_tool_call(
+ &self,
+ tool_use: &LanguageModelToolUse,
+ tool_result: Option<&LanguageModelToolResult>,
+ stream: &ThreadEventStream,
+ cx: &mut Context<Self>,
+ ) {
+ let tool = self.tools.get(tool_use.name.as_ref()).cloned().or_else(|| {
+ self.context_server_registry
+ .read(cx)
+ .servers()
+ .find_map(|(_, tools)| {
+ if let Some(tool) = tools.get(tool_use.name.as_ref()) {
+ Some(tool.clone())
+ } else {
+ None
+ }
+ })
+ });
+
+ let Some(tool) = tool else {
+ stream
+ .0
+ .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
+ id: acp::ToolCallId(tool_use.id.to_string().into()),
+ title: tool_use.name.to_string(),
+ kind: acp::ToolKind::Other,
+ status: acp::ToolCallStatus::Failed,
+ content: Vec::new(),
+ locations: Vec::new(),
+ raw_input: Some(tool_use.input.clone()),
+ raw_output: None,
+ })))
+ .ok();
+ return;
+ };
+
+ let title = tool.initial_title(tool_use.input.clone(), cx);
+ let kind = tool.kind();
+ stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone());
+
+ let output = tool_result
+ .as_ref()
+ .and_then(|result| result.output.clone());
+ if let Some(output) = output.clone() {
+ let tool_event_stream = ToolCallEventStream::new(
+ tool_use.id.clone(),
+ stream.clone(),
+ Some(self.project.read(cx).fs().clone()),
+ );
+ tool.replay(tool_use.input.clone(), output, tool_event_stream, cx)
+ .log_err();
+ }
+
+ stream.update_tool_call_fields(
+ &tool_use.id,
+ acp::ToolCallUpdateFields {
+ status: Some(
+ tool_result
+ .as_ref()
+ .map_or(acp::ToolCallStatus::Failed, |result| {
+ if result.is_error {
+ acp::ToolCallStatus::Failed
+ } else {
+ acp::ToolCallStatus::Completed
+ }
+ }),
+ ),
+ raw_output: output,
+ ..Default::default()
+ },
+ );
+ }
+
+ pub fn from_db(
+ id: acp::SessionId,
+ db_thread: DbThread,
+ project: Entity<Project>,
+ project_context: Entity<ProjectContext>,
+ context_server_registry: Entity<ContextServerRegistry>,
+ action_log: Entity<ActionLog>,
+ templates: Arc<Templates>,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let profile_id = db_thread
+ .profile
+ .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone());
+ let model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
+ db_thread
+ .model
+ .and_then(|model| {
+ let model = SelectedModel {
+ provider: model.provider.clone().into(),
+ model: model.model.into(),
+ };
+ registry.select_model(&model, cx)
+ })
+ .or_else(|| registry.default_model())
+ .map(|model| model.model)
+ });
+ let (prompt_capabilities_tx, prompt_capabilities_rx) =
+ watch::channel(Self::prompt_capabilities(model.as_deref()));
+
+ Self {
+ id,
+ prompt_id: PromptId::new(),
+ title: if db_thread.title.is_empty() {
+ None
+ } else {
+ Some(db_thread.title.clone())
+ },
+ pending_title_generation: None,
+ summary: db_thread.detailed_summary,
+ messages: db_thread.messages,
+ completion_mode: db_thread.completion_mode.unwrap_or_default(),
running_turn: None,
pending_message: None,
tools: BTreeMap::default(),
+ tool_use_limit_reached: false,
+ request_token_usage: db_thread.request_token_usage.clone(),
+ cumulative_token_usage: db_thread.cumulative_token_usage,
+ initial_project_snapshot: Task::ready(db_thread.initial_project_snapshot).shared(),
context_server_registry,
profile_id,
project_context,
templates,
- selected_model: default_model,
+ model,
+ summarization_model: None,
project,
action_log,
+ updated_at: db_thread.updated_at,
+ prompt_capabilities_tx,
+ prompt_capabilities_rx,
}
}
+ pub fn to_db(&self, cx: &App) -> Task<DbThread> {
+ let initial_project_snapshot = self.initial_project_snapshot.clone();
+ let mut thread = DbThread {
+ title: self.title(),
+ messages: self.messages.clone(),
+ updated_at: self.updated_at,
+ detailed_summary: self.summary.clone(),
+ initial_project_snapshot: None,
+ cumulative_token_usage: self.cumulative_token_usage,
+ request_token_usage: self.request_token_usage.clone(),
+ model: self.model.as_ref().map(|model| DbLanguageModel {
+ provider: model.provider_id().to_string(),
+ model: model.name().0.to_string(),
+ }),
+ completion_mode: Some(self.completion_mode),
+ profile: Some(self.profile_id.clone()),
+ };
+
+ cx.background_spawn(async move {
+ let initial_project_snapshot = initial_project_snapshot.await;
+ thread.initial_project_snapshot = initial_project_snapshot;
+ thread
+ })
+ }
+
+ /// Create a snapshot of the current project state including git information and unsaved buffers.
+ fn project_snapshot(
+ project: Entity<Project>,
+ cx: &mut Context<Self>,
+ ) -> Task<Arc<agent::thread::ProjectSnapshot>> {
+ let git_store = project.read(cx).git_store().clone();
+ let worktree_snapshots: Vec<_> = project
+ .read(cx)
+ .visible_worktrees(cx)
+ .map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
+ .collect();
+
+ cx.spawn(async move |_, cx| {
+ let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
+
+ let mut unsaved_buffers = Vec::new();
+ cx.update(|app_cx| {
+ let buffer_store = project.read(app_cx).buffer_store();
+ for buffer_handle in buffer_store.read(app_cx).buffers() {
+ let buffer = buffer_handle.read(app_cx);
+ if buffer.is_dirty()
+ && let Some(file) = buffer.file()
+ {
+ let path = file.path().to_string_lossy().to_string();
+ unsaved_buffers.push(path);
+ }
+ }
+ })
+ .ok();
+
+ Arc::new(ProjectSnapshot {
+ worktree_snapshots,
+ unsaved_buffer_paths: unsaved_buffers,
+ timestamp: Utc::now(),
+ })
+ })
+ }
+
+ fn worktree_snapshot(
+ worktree: Entity<project::Worktree>,
+ git_store: Entity<GitStore>,
+ cx: &App,
+ ) -> Task<agent::thread::WorktreeSnapshot> {
+ cx.spawn(async move |cx| {
+ // Get worktree path and snapshot
+ let worktree_info = cx.update(|app_cx| {
+ let worktree = worktree.read(app_cx);
+ let path = worktree.abs_path().to_string_lossy().to_string();
+ let snapshot = worktree.snapshot();
+ (path, snapshot)
+ });
+
+ let Ok((worktree_path, _snapshot)) = worktree_info else {
+ return WorktreeSnapshot {
+ worktree_path: String::new(),
+ git_state: None,
+ };
+ };
+
+ let git_state = git_store
+ .update(cx, |git_store, cx| {
+ git_store
+ .repositories()
+ .values()
+ .find(|repo| {
+ repo.read(cx)
+ .abs_path_to_repo_path(&worktree.read(cx).abs_path())
+ .is_some()
+ })
+ .cloned()
+ })
+ .ok()
+ .flatten()
+ .map(|repo| {
+ repo.update(cx, |repo, _| {
+ let current_branch =
+ repo.branch.as_ref().map(|branch| branch.name().to_owned());
+ repo.send_job(None, |state, _| async move {
+ let RepositoryState::Local { backend, .. } = state else {
+ return GitState {
+ remote_url: None,
+ head_sha: None,
+ current_branch,
+ diff: None,
+ };
+ };
+
+ let remote_url = backend.remote_url("origin");
+ let head_sha = backend.head_sha().await;
+ let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
+
+ GitState {
+ remote_url,
+ head_sha,
+ current_branch,
+ diff,
+ }
+ })
+ })
+ });
+
+ let git_state = match git_state {
+ Some(git_state) => match git_state.ok() {
+ Some(git_state) => git_state.await.ok(),
+ None => None,
+ },
+ None => None,
+ };
+
+ WorktreeSnapshot {
+ worktree_path,
+ git_state,
+ }
+ })
+ }
+
+ pub fn project_context(&self) -> &Entity<ProjectContext> {
+ &self.project_context
+ }
+
pub fn project(&self) -> &Entity<Project> {
&self.project
}
@@ -457,8 +993,51 @@ impl Thread {
&self.action_log
}
- pub fn set_mode(&mut self, mode: CompletionMode) {
+ pub fn is_empty(&self) -> bool {
+ self.messages.is_empty() && self.title.is_none()
+ }
+
+ pub fn model(&self) -> Option<&Arc<dyn LanguageModel>> {
+ self.model.as_ref()
+ }
+
+ pub fn set_model(&mut self, model: Arc<dyn LanguageModel>, cx: &mut Context<Self>) {
+ let old_usage = self.latest_token_usage();
+ self.model = Some(model);
+ let new_caps = Self::prompt_capabilities(self.model.as_deref());
+ let new_usage = self.latest_token_usage();
+ if old_usage != new_usage {
+ cx.emit(TokenUsageUpdated(new_usage));
+ }
+ self.prompt_capabilities_tx.send(new_caps).log_err();
+ cx.notify()
+ }
+
+ pub fn summarization_model(&self) -> Option<&Arc<dyn LanguageModel>> {
+ self.summarization_model.as_ref()
+ }
+
+ pub fn set_summarization_model(
+ &mut self,
+ model: Option<Arc<dyn LanguageModel>>,
+ cx: &mut Context<Self>,
+ ) {
+ self.summarization_model = model;
+ cx.notify()
+ }
+
+ pub fn completion_mode(&self) -> CompletionMode {
+ self.completion_mode
+ }
+
+ pub fn set_completion_mode(&mut self, mode: CompletionMode, cx: &mut Context<Self>) {
+ let old_usage = self.latest_token_usage();
self.completion_mode = mode;
+ let new_usage = self.latest_token_usage();
+ if old_usage != new_usage {
+ cx.emit(TokenUsageUpdated(new_usage));
+ }
+ cx.notify()
}
#[cfg(any(test, feature = "test-support"))]
@@ -470,181 +1049,352 @@ impl Thread {
}
}
- pub fn add_tool(&mut self, tool: impl AgentTool) {
- self.tools.insert(tool.name(), tool.erase());
+ pub fn add_default_tools(
+ &mut self,
+ environment: Rc<dyn ThreadEnvironment>,
+ cx: &mut Context<Self>,
+ ) {
+ let language_registry = self.project.read(cx).languages().clone();
+ self.add_tool(CopyPathTool::new(self.project.clone()));
+ self.add_tool(CreateDirectoryTool::new(self.project.clone()));
+ self.add_tool(DeletePathTool::new(
+ self.project.clone(),
+ self.action_log.clone(),
+ ));
+ self.add_tool(DiagnosticsTool::new(self.project.clone()));
+ self.add_tool(EditFileTool::new(
+ self.project.clone(),
+ cx.weak_entity(),
+ language_registry,
+ ));
+ self.add_tool(FetchTool::new(self.project.read(cx).client().http_client()));
+ self.add_tool(FindPathTool::new(self.project.clone()));
+ self.add_tool(GrepTool::new(self.project.clone()));
+ self.add_tool(ListDirectoryTool::new(self.project.clone()));
+ self.add_tool(MovePathTool::new(self.project.clone()));
+ self.add_tool(NowTool);
+ self.add_tool(OpenTool::new(self.project.clone()));
+ self.add_tool(ReadFileTool::new(
+ self.project.clone(),
+ self.action_log.clone(),
+ ));
+ self.add_tool(TerminalTool::new(self.project.clone(), environment));
+ self.add_tool(ThinkingTool);
+ self.add_tool(WebSearchTool);
+ }
+
+ pub fn add_tool<T: AgentTool>(&mut self, tool: T) {
+ self.tools.insert(T::name().into(), tool.erase());
}
pub fn remove_tool(&mut self, name: &str) -> bool {
self.tools.remove(name).is_some()
}
+ pub fn profile(&self) -> &AgentProfileId {
+ &self.profile_id
+ }
+
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
self.profile_id = profile_id;
}
- pub fn cancel(&mut self) {
- // TODO: do we need to emit a stop::cancel for ACP?
- self.running_turn.take();
- self.flush_pending_message();
+ pub fn cancel(&mut self, cx: &mut Context<Self>) {
+ if let Some(running_turn) = self.running_turn.take() {
+ running_turn.cancel();
+ }
+ self.flush_pending_message(cx);
+ }
+
+ fn update_token_usage(&mut self, update: language_model::TokenUsage, cx: &mut Context<Self>) {
+ let Some(last_user_message) = self.last_user_message() else {
+ return;
+ };
+
+ self.request_token_usage
+ .insert(last_user_message.id.clone(), update);
+ cx.emit(TokenUsageUpdated(self.latest_token_usage()));
+ cx.notify();
}
- pub fn truncate(&mut self, message_id: UserMessageId) -> Result<()> {
- self.cancel();
+ pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context<Self>) -> Result<()> {
+ self.cancel(cx);
let Some(position) = self.messages.iter().position(
|msg| matches!(msg, Message::User(UserMessage { id, .. }) if id == &message_id),
) else {
return Err(anyhow!("Message not found"));
};
- self.messages.truncate(position);
+
+ for message in self.messages.drain(position..) {
+ match message {
+ Message::User(message) => {
+ self.request_token_usage.remove(&message.id);
+ }
+ Message::Agent(_) | Message::Resume => {}
+ }
+ }
+ self.summary = None;
+ cx.notify();
Ok(())
}
+ pub fn latest_token_usage(&self) -> Option<acp_thread::TokenUsage> {
+ let last_user_message = self.last_user_message()?;
+ let tokens = self.request_token_usage.get(&last_user_message.id)?;
+ let model = self.model.clone()?;
+
+ Some(acp_thread::TokenUsage {
+ max_tokens: model.max_token_count_for_mode(self.completion_mode.into()),
+ used_tokens: tokens.total_tokens(),
+ })
+ }
+
+ pub fn resume(
+ &mut self,
+ cx: &mut Context<Self>,
+ ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
+ self.messages.push(Message::Resume);
+ cx.notify();
+
+ log::debug!("Total messages in thread: {}", self.messages.len());
+ self.run_turn(cx)
+ }
+
/// Sending a message results in the model streaming a response, which could include tool calls.
/// After calling tools, the model will stops and waits for any outstanding tool calls to be completed and their results sent.
/// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn.
pub fn send<T>(
&mut self,
- message_id: UserMessageId,
+ id: UserMessageId,
content: impl IntoIterator<Item = T>,
cx: &mut Context<Self>,
- ) -> mpsc::UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>>
+ ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>
where
T: Into<UserMessageContent>,
{
- let model = self.selected_model.clone();
+ let model = self.model().context("No language model configured")?;
+
+ log::info!("Thread::send called with model: {}", model.name().0);
+ self.advance_prompt_id();
+
let content = content.into_iter().map(Into::into).collect::<Vec<_>>();
- log::info!("Thread::send called with model: {:?}", model.name());
log::debug!("Thread::send content: {:?}", content);
+ self.messages
+ .push(Message::User(UserMessage { id, content }));
cx.notify();
- let (events_tx, events_rx) =
- mpsc::unbounded::<Result<AgentResponseEvent, LanguageModelCompletionError>>();
- let event_stream = AgentResponseEventStream(events_tx);
- self.messages.push(Message::User(UserMessage {
- id: message_id.clone(),
- content,
- }));
- log::info!("Total messages in thread: {}", self.messages.len());
- self.running_turn = Some(cx.spawn(async move |this, cx| {
- log::info!("Starting agent turn execution");
- let turn_result = async {
- let mut completion_intent = CompletionIntent::UserPrompt;
- loop {
- log::debug!(
- "Building completion request with intent: {:?}",
- completion_intent
- );
- let request = this.update(cx, |this, cx| {
- this.build_completion_request(completion_intent, cx)
- })?;
+ log::debug!("Total messages in thread: {}", self.messages.len());
+ self.run_turn(cx)
+ }
- log::info!("Calling model.stream_completion");
- let mut events = model.stream_completion(request, cx).await?;
- log::debug!("Stream completion started successfully");
-
- let mut tool_uses = FuturesUnordered::new();
- while let Some(event) = events.next().await {
- match event? {
- LanguageModelCompletionEvent::Stop(reason) => {
- event_stream.send_stop(reason);
- if reason == StopReason::Refusal {
- this.update(cx, |this, _cx| this.truncate(message_id))??;
- return Ok(());
- }
+ fn run_turn(
+ &mut self,
+ cx: &mut Context<Self>,
+ ) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
+ self.cancel(cx);
+
+ let model = self.model.clone().context("No language model configured")?;
+ let profile = AgentSettings::get_global(cx)
+ .profiles
+ .get(&self.profile_id)
+ .context("Profile not found")?;
+ let (events_tx, events_rx) = mpsc::unbounded::<Result<ThreadEvent>>();
+ let event_stream = ThreadEventStream(events_tx);
+ let message_ix = self.messages.len().saturating_sub(1);
+ self.tool_use_limit_reached = false;
+ self.summary = None;
+ self.running_turn = Some(RunningTurn {
+ event_stream: event_stream.clone(),
+ tools: self.enabled_tools(profile, &model, cx),
+ _task: cx.spawn(async move |this, cx| {
+ log::debug!("Starting agent turn execution");
+
+ let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await;
+ _ = this.update(cx, |this, cx| this.flush_pending_message(cx));
+
+ match turn_result {
+ Ok(()) => {
+ log::debug!("Turn execution completed");
+ event_stream.send_stop(acp::StopReason::EndTurn);
+ }
+ Err(error) => {
+ log::error!("Turn execution failed: {:?}", error);
+ match error.downcast::<CompletionError>() {
+ Ok(CompletionError::Refusal) => {
+ event_stream.send_stop(acp::StopReason::Refusal);
+ _ = this.update(cx, |this, _| this.messages.truncate(message_ix));
}
- event => {
- log::trace!("Received completion event: {:?}", event);
- this.update(cx, |this, cx| {
- tool_uses.extend(this.handle_streamed_completion_event(
- event,
- &event_stream,
- cx,
- ));
- })
- .ok();
+ Ok(CompletionError::MaxTokens) => {
+ event_stream.send_stop(acp::StopReason::MaxTokens);
+ }
+ Ok(CompletionError::Other(error)) | Err(error) => {
+ event_stream.send_error(error);
}
}
}
+ }
- if tool_uses.is_empty() {
- log::info!("No tool uses found, completing turn");
- return Ok(());
+ _ = this.update(cx, |this, _| this.running_turn.take());
+ }),
+ });
+ Ok(events_rx)
+ }
+
+ async fn run_turn_internal(
+ this: &WeakEntity<Self>,
+ model: Arc<dyn LanguageModel>,
+ event_stream: &ThreadEventStream,
+ cx: &mut AsyncApp,
+ ) -> Result<()> {
+ let mut attempt = 0;
+ let mut intent = CompletionIntent::UserPrompt;
+ loop {
+ let request =
+ this.update(cx, |this, cx| this.build_completion_request(intent, cx))??;
+
+ telemetry::event!(
+ "Agent Thread Completion",
+ thread_id = this.read_with(cx, |this, _| this.id.to_string())?,
+ prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?,
+ model = model.telemetry_id(),
+ model_provider = model.provider_id().to_string(),
+ attempt
+ );
+
+ log::debug!("Calling model.stream_completion, attempt {}", attempt);
+ let mut events = model
+ .stream_completion(request, cx)
+ .await
+ .map_err(|error| anyhow!(error))?;
+ let mut tool_results = FuturesUnordered::new();
+ let mut error = None;
+ while let Some(event) = events.next().await {
+ log::trace!("Received completion event: {:?}", event);
+ match event {
+ Ok(event) => {
+ tool_results.extend(this.update(cx, |this, cx| {
+ this.handle_completion_event(event, event_stream, cx)
+ })??);
}
- log::info!("Found {} tool uses to execute", tool_uses.len());
-
- while let Some(tool_result) = tool_uses.next().await {
- log::info!("Tool finished {:?}", tool_result);
-
- event_stream.update_tool_call_fields(
- &tool_result.tool_use_id,
- acp::ToolCallUpdateFields {
- status: Some(if tool_result.is_error {
- acp::ToolCallStatus::Failed
- } else {
- acp::ToolCallStatus::Completed
- }),
- raw_output: tool_result.output.clone(),
- ..Default::default()
- },
- );
- this.update(cx, |this, _cx| {
- this.pending_message()
- .tool_results
- .insert(tool_result.tool_use_id.clone(), tool_result);
- })
- .ok();
+ Err(err) => {
+ error = Some(err);
+ break;
}
-
- this.update(cx, |this, _| this.flush_pending_message())?;
- completion_intent = CompletionIntent::ToolResults;
}
}
- .await;
- this.update(cx, |this, _| this.flush_pending_message()).ok();
- if let Err(error) = turn_result {
- log::error!("Turn execution failed: {:?}", error);
- event_stream.send_error(error);
+ let end_turn = tool_results.is_empty();
+ while let Some(tool_result) = tool_results.next().await {
+ log::debug!("Tool finished {:?}", tool_result);
+
+ event_stream.update_tool_call_fields(
+ &tool_result.tool_use_id,
+ acp::ToolCallUpdateFields {
+ status: Some(if tool_result.is_error {
+ acp::ToolCallStatus::Failed
+ } else {
+ acp::ToolCallStatus::Completed
+ }),
+ raw_output: tool_result.output.clone(),
+ ..Default::default()
+ },
+ );
+ this.update(cx, |this, _cx| {
+ this.pending_message()
+ .tool_results
+ .insert(tool_result.tool_use_id.clone(), tool_result);
+ })?;
+ }
+
+ this.update(cx, |this, cx| {
+ this.flush_pending_message(cx);
+ if this.title.is_none() && this.pending_title_generation.is_none() {
+ this.generate_title(cx);
+ }
+ })?;
+
+ if let Some(error) = error {
+ attempt += 1;
+ let retry =
+ this.update(cx, |this, _| this.handle_completion_error(error, attempt))??;
+ let timer = cx.background_executor().timer(retry.duration);
+ event_stream.send_retry(retry);
+ timer.await;
+ this.update(cx, |this, _cx| {
+ if let Some(Message::Agent(message)) = this.messages.last() {
+ if message.tool_results.is_empty() {
+ intent = CompletionIntent::UserPrompt;
+ this.messages.push(Message::Resume);
+ }
+ }
+ })?;
+ } else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? {
+ return Err(language_model::ToolUseLimitReachedError.into());
+ } else if end_turn {
+ return Ok(());
} else {
- log::info!("Turn execution completed successfully");
+ intent = CompletionIntent::ToolResults;
+ attempt = 0;
}
- }));
- events_rx
+ }
}
- pub fn build_system_message(&self) -> LanguageModelRequestMessage {
- log::debug!("Building system message");
- let prompt = SystemPromptTemplate {
- project: &self.project_context.borrow(),
- available_tools: self.tools.keys().cloned().collect(),
+ fn handle_completion_error(
+ &mut self,
+ error: LanguageModelCompletionError,
+ attempt: u8,
+ ) -> Result<acp_thread::RetryStatus> {
+ if self.completion_mode == CompletionMode::Normal {
+ return Err(anyhow!(error));
}
- .render(&self.templates)
- .context("failed to build system prompt")
- .expect("Invalid template");
- log::debug!("System message built");
- LanguageModelRequestMessage {
- role: Role::System,
- content: vec![prompt.into()],
- cache: true,
+
+ let Some(strategy) = Self::retry_strategy_for(&error) else {
+ return Err(anyhow!(error));
+ };
+
+ let max_attempts = match &strategy {
+ RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts,
+ RetryStrategy::Fixed { max_attempts, .. } => *max_attempts,
+ };
+
+ if attempt > max_attempts {
+ return Err(anyhow!(error));
}
+
+ let delay = match &strategy {
+ RetryStrategy::ExponentialBackoff { initial_delay, .. } => {
+ let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32);
+ Duration::from_secs(delay_secs)
+ }
+ RetryStrategy::Fixed { delay, .. } => *delay,
+ };
+ log::debug!("Retry attempt {attempt} with delay {delay:?}");
+
+ Ok(acp_thread::RetryStatus {
+ last_error: error.to_string().into(),
+ attempt: attempt as usize,
+ max_attempts: max_attempts as usize,
+ started_at: Instant::now(),
+ duration: delay,
+ })
}
/// A helper method that's called on every streamed completion event.
- /// Returns an optional tool result task, which the main agentic loop in
- /// send will send back to the model when it resolves.
- fn handle_streamed_completion_event(
+ /// Returns an optional tool result task, which the main agentic loop will
+ /// send back to the model when it resolves.
+ fn handle_completion_event(
&mut self,
event: LanguageModelCompletionEvent,
- event_stream: &AgentResponseEventStream,
+ event_stream: &ThreadEventStream,
cx: &mut Context<Self>,
- ) -> Option<Task<LanguageModelToolResult>> {
+ ) -> Result<Option<Task<LanguageModelToolResult>>> {
log::trace!("Handling streamed completion event: {:?}", event);
use LanguageModelCompletionEvent::*;
match event {
StartMessage { .. } => {
- self.flush_pending_message();
+ self.flush_pending_message(cx);
self.pending_message = Some(AgentMessage::default());
}
Text(new_text) => self.handle_text_event(new_text, event_stream, cx),
@@ -0,0 +1,43 @@
+use language_model::LanguageModelToolSchemaFormat;
+use schemars::{
+ JsonSchema, Schema,
+ generate::SchemaSettings,
+ transform::{Transform, transform_subschemas},
+};
+
+pub(crate) fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
+ let mut generator = match format {
+ LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
+ LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3()
+ .with(|settings| {
+ settings.meta_schema = None;
+ settings.inline_subschemas = true;
+ })
+ .with_transform(ToJsonSchemaSubsetTransform)
+ .into_generator(),
+ };
+ generator.root_schema_for::<T>()
+}
+
+#[derive(Debug, Clone)]
+struct ToJsonSchemaSubsetTransform;
+
+impl Transform for ToJsonSchemaSubsetTransform {
+ fn transform(&mut self, schema: &mut Schema) {
+ // Ensure that the type field is not an array, this happens when we use
+ // Option<T>, the type will be [T, "null"].
+ if let Some(type_field) = schema.get_mut("type")
+ && let Some(types) = type_field.as_array()
+ && let Some(first_type) = types.first()
+ {
+ *type_field = first_type.clone();
+ }
+
+ // oneOf is not supported, use anyOf instead
+ if let Some(one_of) = schema.remove("oneOf") {
+ schema.insert("anyOf".to_string(), one_of);
+ }
+
+ transform_subschemas(self, schema);
+ }
+}
@@ -16,6 +16,29 @@ mod terminal_tool;
mod thinking_tool;
mod web_search_tool;
+/// A list of all built in tool names, for use in deduplicating MCP tool names
+pub fn default_tool_names() -> impl Iterator<Item = &'static str> {
+ [
+ CopyPathTool::name(),
+ CreateDirectoryTool::name(),
+ DeletePathTool::name(),
+ DiagnosticsTool::name(),
+ EditFileTool::name(),
+ FetchTool::name(),
+ FindPathTool::name(),
+ GrepTool::name(),
+ ListDirectoryTool::name(),
+ MovePathTool::name(),
+ NowTool::name(),
+ OpenTool::name(),
+ ReadFileTool::name(),
+ TerminalTool::name(),
+ ThinkingTool::name(),
+ WebSearchTool::name(),
+ ]
+ .into_iter()
+}
+
pub use context_server_registry::*;
pub use copy_path_tool::*;
pub use create_directory_tool::*;
@@ -33,3 +56,5 @@ pub use read_file_tool::*;
pub use terminal_tool::*;
pub use thinking_tool::*;
pub use web_search_tool::*;
+
+use crate::AgentTool;
@@ -103,7 +103,7 @@ impl ContextServerRegistry {
self.reload_tools_for_server(server_id.clone(), cx);
}
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
- self.registered_servers.remove(&server_id);
+ self.registered_servers.remove(server_id);
cx.notify();
}
}
@@ -145,7 +145,7 @@ impl AnyAgentTool for ContextServerTool {
ToolKind::Other
}
- fn initial_title(&self, _input: serde_json::Value) -> SharedString {
+ fn initial_title(&self, _input: serde_json::Value, _cx: &mut App) -> SharedString {
format!("Run MCP tool `{}`", self.tool.name).into()
}
@@ -169,22 +169,23 @@ impl AnyAgentTool for ContextServerTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
- _event_stream: ToolCallEventStream,
+ event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<AgentToolOutput>> {
let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else {
return Task::ready(Err(anyhow!("Context server not found")));
};
let tool_name = self.tool.name.clone();
- let server_clone = server.clone();
- let input_clone = input.clone();
+ let authorize = event_stream.authorize(self.initial_title(input.clone(), cx), cx);
cx.spawn(async move |_cx| {
- let Some(protocol) = server_clone.client() else {
+ authorize.await?;
+
+ let Some(protocol) = server.client() else {
bail!("Context server not initialized");
};
- let arguments = if let serde_json::Value::Object(map) = input_clone {
+ let arguments = if let serde_json::Value::Object(map) = input {
Some(map.into_iter().collect())
} else {
None
@@ -228,4 +229,14 @@ impl AnyAgentTool for ContextServerTool {
})
})
}
+
+ fn replay(
+ &self,
+ _input: serde_json::Value,
+ _output: serde_json::Value,
+ _event_stream: ToolCallEventStream,
+ _cx: &mut App,
+ ) -> Result<()> {
+ Ok(())
+ }
}
@@ -1,23 +1,18 @@
use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind;
use anyhow::{Context as _, Result, anyhow};
-use gpui::{App, AppContext, Entity, SharedString, Task};
+use gpui::{App, AppContext, Entity, Task};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use util::markdown::MarkdownInlineCode;
-/// Copies a file or directory in the project, and returns confirmation that the
-/// copy succeeded.
-///
+/// Copies a file or directory in the project, and returns confirmation that the copy succeeded.
/// Directory contents will be copied recursively (like `cp -r`).
///
-/// This tool should be used when it's desirable to create a copy of a file or
-/// directory without modifying the original. It's much more efficient than
-/// doing this by separately reading and then writing the file or directory's
-/// contents, so this tool should be preferred over that approach whenever
-/// copying is the goal.
+/// This tool should be used when it's desirable to create a copy of a file or directory without modifying the original.
+/// It's much more efficient than doing this by separately reading and then writing the file or directory's contents, so this tool should be preferred over that approach whenever copying is the goal.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CopyPathToolInput {
/// The source path of the file or directory to copy.
@@ -33,12 +28,10 @@ pub struct CopyPathToolInput {
/// You can copy the first file by providing a source_path of "directory1/a/something.txt"
/// </example>
pub source_path: String,
-
/// The destination path where the file or directory should be copied to.
///
/// <example>
- /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt",
- /// provide a destination_path of "directory2/b/copy.txt"
+ /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", provide a destination_path of "directory2/b/copy.txt"
/// </example>
pub destination_path: String,
}
@@ -57,15 +50,19 @@ impl AgentTool for CopyPathTool {
type Input = CopyPathToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "copy_path".into()
+ fn name() -> &'static str {
+ "copy_path"
}
- fn kind(&self) -> ToolKind {
+ fn kind() -> ToolKind {
ToolKind::Move
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> ui::SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> ui::SharedString {
if let Ok(input) = input {
let src = MarkdownInlineCode(&input.source_path);
let dest = MarkdownInlineCode(&input.destination_path);
@@ -9,12 +9,9 @@ use util::markdown::MarkdownInlineCode;
use crate::{AgentTool, ToolCallEventStream};
-/// Creates a new directory at the specified path within the project. Returns
-/// confirmation that the directory was created.
+/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
///
-/// This tool creates a directory and all necessary parent directories (similar
-/// to `mkdir -p`). It should be used whenever you need to create new
-/// directories within the project.
+/// This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateDirectoryToolInput {
/// The path of the new directory.
@@ -44,15 +41,19 @@ impl AgentTool for CreateDirectoryTool {
type Input = CreateDirectoryToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "create_directory".into()
+ fn name() -> &'static str {
+ "create_directory"
}
- fn kind(&self) -> ToolKind {
+ fn kind() -> ToolKind {
ToolKind::Read
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
if let Ok(input) = input {
format!("Create directory {}", MarkdownInlineCode(&input.path)).into()
} else {
@@ -9,8 +9,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
-/// Deletes the file or directory (and the directory's contents, recursively) at
-/// the specified path in the project, and returns confirmation of the deletion.
+/// Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DeletePathToolInput {
/// The path of the file or directory to delete.
@@ -45,15 +44,19 @@ impl AgentTool for DeletePathTool {
type Input = DeletePathToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "delete_path".into()
+ fn name() -> &'static str {
+ "delete_path"
}
- fn kind(&self) -> ToolKind {
+ fn kind() -> ToolKind {
ToolKind::Delete
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
if let Ok(input) = input {
format!("Delete “`{}`”", input.path).into()
} else {
@@ -63,15 +63,19 @@ impl AgentTool for DiagnosticsTool {
type Input = DiagnosticsToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "diagnostics".into()
+ fn name() -> &'static str {
+ "diagnostics"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Read
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
if let Some(path) = input.ok().and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(path),
_ => None,
@@ -5,10 +5,10 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
use cloud_llm_client::CompletionIntent;
use collections::HashSet;
-use gpui::{App, AppContext, AsyncApp, Entity, Task};
+use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
use indoc::formatdoc;
-use language::ToPoint;
use language::language_settings::{self, FormatOnSave};
+use language::{LanguageRegistry, ToPoint};
use language_model::LanguageModelToolResultContent;
use paths;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
@@ -34,25 +34,21 @@ const DEFAULT_UI_TEXT: &str = "Editing file";
/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
- /// A one-line, user-friendly markdown description of the edit. This will be
- /// shown in the UI and also passed to another model to perform the edit.
+ /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit.
///
- /// Be terse, but also descriptive in what you want to achieve with this
- /// edit. Avoid generic instructions.
+ /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions.
///
/// NEVER mention the file path in this description.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
///
- /// Make sure to include this field before all the others in the input object
- /// so that we can display it immediately.
+ /// Make sure to include this field before all the others in the input object so that we can display it immediately.
pub display_description: String,
/// The full path of the file to create or modify in the project.
///
- /// WARNING: When specifying which file path need changing, you MUST
- /// start each path with one of the project's root directories.
+ /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories.
///
/// The following examples assume we have two root directories in the project:
/// - /a/b/backend
@@ -61,22 +57,19 @@ pub struct EditFileToolInput {
/// <example>
/// `backend/src/main.rs`
///
- /// Notice how the file path starts with `backend`. Without that, the path
- /// would be ambiguous and the call would fail!
+ /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail!
/// </example>
///
/// <example>
/// `frontend/db.js`
/// </example>
pub path: PathBuf,
-
/// The mode of operation on the file. Possible values:
/// - 'edit': Make granular edits to an existing file.
/// - 'create': Create a new file if it doesn't exist.
/// - 'overwrite': Replace the entire contents of an existing file.
///
- /// When a file already exists or you just created it, prefer editing
- /// it as opposed to recreating it from scratch.
+ /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch.
pub mode: EditFileMode,
}
@@ -90,6 +83,7 @@ struct EditFileToolPartialInput {
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
+#[schemars(inline)]
pub enum EditFileMode {
Edit,
Create,
@@ -98,11 +92,13 @@ pub enum EditFileMode {
#[derive(Debug, Serialize, Deserialize)]
pub struct EditFileToolOutput {
+ #[serde(alias = "original_path")]
input_path: PathBuf,
- project_path: PathBuf,
new_text: String,
old_text: Arc<String>,
+ #[serde(default)]
diff: String,
+ #[serde(alias = "raw_output")]
edit_agent_output: EditAgentOutput,
}
@@ -122,12 +118,22 @@ impl From<EditFileToolOutput> for LanguageModelToolResultContent {
}
pub struct EditFileTool {
- thread: Entity<Thread>,
+ thread: WeakEntity<Thread>,
+ language_registry: Arc<LanguageRegistry>,
+ project: Entity<Project>,
}
impl EditFileTool {
- pub fn new(thread: Entity<Thread>) -> Self {
- Self { thread }
+ pub fn new(
+ project: Entity<Project>,
+ thread: WeakEntity<Thread>,
+ language_registry: Arc<LanguageRegistry>,
+ ) -> Self {
+ Self {
+ project,
+ thread,
+ language_registry,
+ }
}
fn authorize(
@@ -156,19 +162,22 @@ impl EditFileTool {
// It's also possible that the global config dir is configured to be inside the project,
// so check for that edge case too.
- if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
- if canonical_path.starts_with(paths::config_dir()) {
- return event_stream.authorize(
- format!("{} (global settings)", input.display_description),
- cx,
- );
- }
+ if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
+ && canonical_path.starts_with(paths::config_dir())
+ {
+ return event_stream.authorize(
+ format!("{} (global settings)", input.display_description),
+ cx,
+ );
}
// Check if path is inside the global config directory
// First check if it's already inside project - if not, try to canonicalize
- let thread = self.thread.read(cx);
- let project_path = thread.project().read(cx).find_project_path(&input.path, cx);
+ let Ok(project_path) = self.thread.read_with(cx, |thread, cx| {
+ thread.project().read(cx).find_project_path(&input.path, cx)
+ }) else {
+ return Task::ready(Err(anyhow!("thread was dropped")));
+ };
// If the path is inside the project, and it's not one of the above edge cases,
// then no confirmation is necessary. Otherwise, confirmation is necessary.
@@ -184,30 +193,58 @@ impl AgentTool for EditFileTool {
type Input = EditFileToolInput;
type Output = EditFileToolOutput;
- fn name(&self) -> SharedString {
- "edit_file".into()
+ fn name() -> &'static str {
+ "edit_file"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Edit
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ cx: &mut App,
+ ) -> SharedString {
match input {
- Ok(input) => input.display_description.into(),
+ Ok(input) => self
+ .project
+ .read(cx)
+ .find_project_path(&input.path, cx)
+ .and_then(|project_path| {
+ self.project
+ .read(cx)
+ .short_full_path_for_project_path(&project_path, cx)
+ })
+ .unwrap_or(Path::new(&input.path).into())
+ .to_string_lossy()
+ .to_string()
+ .into(),
Err(raw_input) => {
if let Some(input) =
serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()
{
+ let path = input.path.trim();
+ if !path.is_empty() {
+ return self
+ .project
+ .read(cx)
+ .find_project_path(&input.path, cx)
+ .and_then(|project_path| {
+ self.project
+ .read(cx)
+ .short_full_path_for_project_path(&project_path, cx)
+ })
+ .unwrap_or(Path::new(&input.path).into())
+ .to_string_lossy()
+ .to_string()
+ .into();
+ }
+
let description = input.display_description.trim();
if !description.is_empty() {
return description.to_string().into();
}
-
- let path = input.path.trim().to_string();
- if !path.is_empty() {
- return path.into();
- }
}
DEFAULT_UI_TEXT.into()
@@ -221,7 +258,12 @@ impl AgentTool for EditFileTool {
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
- let project = self.thread.read(cx).project().clone();
+ let Ok(project) = self
+ .thread
+ .read_with(cx, |thread, _cx| thread.project().clone())
+ else {
+ return Task::ready(Err(anyhow!("thread was dropped")));
+ };
let project_path = match resolve_path(&input, project.clone(), cx) {
Ok(path) => path,
Err(err) => return Task::ready(Err(anyhow!(err))),
@@ -237,17 +279,17 @@ impl AgentTool for EditFileTool {
});
}
- let request = self.thread.update(cx, |thread, cx| {
- thread.build_completion_request(CompletionIntent::ToolResults, cx)
- });
- let thread = self.thread.read(cx);
- let model = thread.selected_model.clone();
- let action_log = thread.action_log().clone();
-
let authorize = self.authorize(&input, &event_stream, cx);
cx.spawn(async move |cx: &mut AsyncApp| {
authorize.await?;
+ let (request, model, action_log) = self.thread.update(cx, |thread, cx| {
+ let request = thread.build_completion_request(CompletionIntent::ToolResults, cx);
+ (request, thread.model().cloned(), thread.action_log().clone())
+ })?;
+ let request = request?;
+ let model = model.context("No language model configured")?;
+
let edit_format = EditFormat::from_model(model.clone())?;
let edit_agent = EditAgent::new(
model,
@@ -266,6 +308,13 @@ impl AgentTool for EditFileTool {
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
event_stream.update_diff(diff.clone());
+ let _finalize_diff = util::defer({
+ let diff = diff.downgrade();
+ let mut cx = cx.clone();
+ move || {
+ diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
+ }
+ });
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
@@ -382,8 +431,6 @@ impl AgentTool for EditFileTool {
})
.await;
- diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
-
let input_path = input.path.display();
if unified_diff.is_empty() {
anyhow::ensure!(
@@ -413,14 +460,32 @@ impl AgentTool for EditFileTool {
Ok(EditFileToolOutput {
input_path: input.path,
- project_path: project_path.path.to_path_buf(),
- new_text: new_text.clone(),
+ new_text,
old_text,
diff: unified_diff,
edit_agent_output,
})
})
}
+
+ fn replay(
+ &self,
+ _input: Self::Input,
+ output: Self::Output,
+ event_stream: ToolCallEventStream,
+ cx: &mut App,
+ ) -> Result<()> {
+ event_stream.update_diff(cx.new(|cx| {
+ Diff::finalized(
+ output.input_path,
+ Some(output.old_text.to_string()),
+ output.new_text,
+ self.language_registry.clone(),
+ cx,
+ )
+ }));
+ Ok(())
+ }
}
/// Validate that the file path is valid, meaning:
@@ -465,7 +530,7 @@ fn resolve_path(
let parent_entry = parent_project_path
.as_ref()
- .and_then(|path| project.entry_for_path(&path, cx))
+ .and_then(|path| project.entry_for_path(path, cx))
.context("Can't create file: parent directory doesn't exist")?;
anyhow::ensure!(
@@ -492,14 +557,13 @@ fn resolve_path(
mod tests {
use super::*;
use crate::{ContextServerRegistry, Templates};
- use action_log::ActionLog;
use client::TelemetrySettings;
use fs::Fs;
use gpui::{TestAppContext, UpdateGlobal};
use language_model::fake_provider::FakeLanguageModel;
+ use prompt_store::ProjectContext;
use serde_json::json;
use settings::SettingsStore;
- use std::rc::Rc;
use util::path;
#[gpui::test]
@@ -509,18 +573,17 @@ mod tests {
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
- let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
- project,
- Rc::default(),
+ project.clone(),
+ cx.new(|_cx| ProjectContext::default()),
context_server_registry,
- action_log,
Templates::new(),
- model,
+ Some(model),
cx,
)
});
@@ -531,7 +594,12 @@ mod tests {
path: "root/nonexistent_file.txt".into(),
mode: EditFileMode::Edit,
};
- Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx)
+ Arc::new(EditFileTool::new(
+ project,
+ thread.downgrade(),
+ language_registry,
+ ))
+ .run(input, ToolCallEventStream::test().0, cx)
})
.await;
assert_eq!(
@@ -618,8 +686,7 @@ mod tests {
mode: mode.clone(),
};
- let result = cx.update(|cx| resolve_path(&input, project, cx));
- result
+ cx.update(|cx| resolve_path(&input, project, cx))
}
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
@@ -706,18 +773,16 @@ mod tests {
}
});
- let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
- project,
- Rc::default(),
+ project.clone(),
+ cx.new(|_cx| ProjectContext::default()),
context_server_registry,
- action_log.clone(),
Templates::new(),
- model.clone(),
+ Some(model.clone()),
cx,
)
});
@@ -744,9 +809,11 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
- Arc::new(EditFileTool {
- thread: thread.clone(),
- })
+ Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ language_registry.clone(),
+ ))
.run(input, ToolCallEventStream::test().0, cx)
});
@@ -771,7 +838,9 @@ mod tests {
"Code should be formatted when format_on_save is enabled"
);
- let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count());
+ let stale_buffer_count = thread
+ .read_with(cx, |thread, _cx| thread.action_log.clone())
+ .read_with(cx, |log, cx| log.stale_buffers(cx).count());
assert_eq!(
stale_buffer_count, 0,
@@ -800,7 +869,12 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
- Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx)
+ Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ language_registry,
+ ))
+ .run(input, ToolCallEventStream::test().0, cx)
});
// Stream the unformatted content
@@ -844,16 +918,15 @@ mod tests {
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
- let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
- project,
- Rc::default(),
+ project.clone(),
+ cx.new(|_cx| ProjectContext::default()),
context_server_registry,
- action_log.clone(),
Templates::new(),
- model.clone(),
+ Some(model.clone()),
cx,
)
});
@@ -881,9 +954,11 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
- Arc::new(EditFileTool {
- thread: thread.clone(),
- })
+ Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ language_registry.clone(),
+ ))
.run(input, ToolCallEventStream::test().0, cx)
});
@@ -932,9 +1007,11 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
- Arc::new(EditFileTool {
- thread: thread.clone(),
- })
+ Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ language_registry,
+ ))
.run(input, ToolCallEventStream::test().0, cx)
});
@@ -970,20 +1047,23 @@ mod tests {
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
- let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
- project,
- Rc::default(),
+ project.clone(),
+ cx.new(|_cx| ProjectContext::default()),
context_server_registry,
- action_log.clone(),
Templates::new(),
- model.clone(),
+ Some(model.clone()),
cx,
)
});
- let tool = Arc::new(EditFileTool { thread });
+ let tool = Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ language_registry,
+ ));
fs.insert_tree("/root", json!({})).await;
// Test 1: Path with .zed component should require confirmation
@@ -1001,7 +1081,10 @@ mod tests {
});
let event = stream_rx.expect_authorization().await;
- assert_eq!(event.tool_call.title, "test 1 (local settings)");
+ assert_eq!(
+ event.tool_call.fields.title,
+ Some("test 1 (local settings)".into())
+ );
// Test 2: Path outside project should require confirmation
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
@@ -1018,7 +1101,7 @@ mod tests {
});
let event = stream_rx.expect_authorization().await;
- assert_eq!(event.tool_call.title, "test 2");
+ assert_eq!(event.tool_call.fields.title, Some("test 2".into()));
// Test 3: Relative path without .zed should not require confirmation
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
@@ -1051,7 +1134,10 @@ mod tests {
)
});
let event = stream_rx.expect_authorization().await;
- assert_eq!(event.tool_call.title, "test 4 (local settings)");
+ assert_eq!(
+ event.tool_call.fields.title,
+ Some("test 4 (local settings)".into())
+ );
// Test 5: When always_allow_tool_actions is enabled, no confirmation needed
cx.update(|cx| {
@@ -1099,22 +1185,25 @@ mod tests {
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({})).await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+ let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
- let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
- project,
- Rc::default(),
+ project.clone(),
+ cx.new(|_cx| ProjectContext::default()),
context_server_registry,
- action_log.clone(),
Templates::new(),
- model.clone(),
+ Some(model.clone()),
cx,
)
});
- let tool = Arc::new(EditFileTool { thread });
+ let tool = Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ language_registry,
+ ));
// Test global config paths - these should require confirmation if they exist and are outside the project
let test_cases = vec![
@@ -1208,23 +1297,25 @@ mod tests {
cx,
)
.await;
-
- let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
- Rc::default(),
+ cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
- action_log.clone(),
Templates::new(),
- model.clone(),
+ Some(model.clone()),
cx,
)
});
- let tool = Arc::new(EditFileTool { thread });
+ let tool = Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ language_registry,
+ ));
// Test files in different worktrees
let test_cases = vec![
@@ -1290,22 +1381,25 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
- let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
- Rc::default(),
+ cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
- action_log.clone(),
Templates::new(),
- model.clone(),
+ Some(model.clone()),
cx,
)
});
- let tool = Arc::new(EditFileTool { thread });
+ let tool = Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ language_registry,
+ ));
// Test edge cases
let test_cases = vec![
@@ -1374,22 +1468,25 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
- let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
- Rc::default(),
+ cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
- action_log.clone(),
Templates::new(),
- model.clone(),
+ Some(model.clone()),
cx,
)
});
- let tool = Arc::new(EditFileTool { thread });
+ let tool = Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ language_registry,
+ ));
// Test different EditFileMode values
let modes = vec![
@@ -1455,63 +1552,187 @@ mod tests {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
- let action_log = cx.new(|_| ActionLog::new(project.clone()));
+ let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
- Rc::default(),
+ cx.new(|_cx| ProjectContext::default()),
context_server_registry,
- action_log.clone(),
Templates::new(),
- model.clone(),
+ Some(model.clone()),
cx,
)
});
- let tool = Arc::new(EditFileTool { thread });
+ let tool = Arc::new(EditFileTool::new(
+ project,
+ thread.downgrade(),
+ language_registry,
+ ));
- assert_eq!(
- tool.initial_title(Err(json!({
- "path": "src/main.rs",
- "display_description": "",
- "old_string": "old code",
- "new_string": "new code"
- }))),
- "src/main.rs"
- );
- assert_eq!(
- tool.initial_title(Err(json!({
- "path": "",
- "display_description": "Fix error handling",
- "old_string": "old code",
- "new_string": "new code"
- }))),
- "Fix error handling"
- );
- assert_eq!(
- tool.initial_title(Err(json!({
- "path": "src/main.rs",
- "display_description": "Fix error handling",
- "old_string": "old code",
- "new_string": "new code"
- }))),
- "Fix error handling"
- );
- assert_eq!(
- tool.initial_title(Err(json!({
- "path": "",
- "display_description": "",
- "old_string": "old code",
- "new_string": "new code"
- }))),
- DEFAULT_UI_TEXT
- );
- assert_eq!(
- tool.initial_title(Err(serde_json::Value::Null)),
- DEFAULT_UI_TEXT
- );
+ cx.update(|cx| {
+ // ...
+ assert_eq!(
+ tool.initial_title(
+ Err(json!({
+ "path": "src/main.rs",
+ "display_description": "",
+ "old_string": "old code",
+ "new_string": "new code"
+ })),
+ cx
+ ),
+ "src/main.rs"
+ );
+ assert_eq!(
+ tool.initial_title(
+ Err(json!({
+ "path": "",
+ "display_description": "Fix error handling",
+ "old_string": "old code",
+ "new_string": "new code"
+ })),
+ cx
+ ),
+ "Fix error handling"
+ );
+ assert_eq!(
+ tool.initial_title(
+ Err(json!({
+ "path": "src/main.rs",
+ "display_description": "Fix error handling",
+ "old_string": "old code",
+ "new_string": "new code"
+ })),
+ cx
+ ),
+ "src/main.rs"
+ );
+ assert_eq!(
+ tool.initial_title(
+ Err(json!({
+ "path": "",
+ "display_description": "",
+ "old_string": "old code",
+ "new_string": "new code"
+ })),
+ cx
+ ),
+ DEFAULT_UI_TEXT
+ );
+ assert_eq!(
+ tool.initial_title(Err(serde_json::Value::Null), cx),
+ DEFAULT_UI_TEXT
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_diff_finalization(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = project::FakeFs::new(cx.executor());
+ fs.insert_tree("/", json!({"main.rs": ""})).await;
+
+ let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
+ let languages = project.read_with(cx, |project, _cx| project.languages().clone());
+ let context_server_registry =
+ cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
+ let model = Arc::new(FakeLanguageModel::default());
+ let thread = cx.new(|cx| {
+ Thread::new(
+ project.clone(),
+ cx.new(|_cx| ProjectContext::default()),
+ context_server_registry.clone(),
+ Templates::new(),
+ Some(model.clone()),
+ cx,
+ )
+ });
+
+ // Ensure the diff is finalized after the edit completes.
+ {
+ let tool = Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ languages.clone(),
+ ));
+ let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+ let edit = cx.update(|cx| {
+ tool.run(
+ EditFileToolInput {
+ display_description: "Edit file".into(),
+ path: path!("/main.rs").into(),
+ mode: EditFileMode::Edit,
+ },
+ stream_tx,
+ cx,
+ )
+ });
+ stream_rx.expect_update_fields().await;
+ let diff = stream_rx.expect_diff().await;
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
+ cx.run_until_parked();
+ model.end_last_completion_stream();
+ edit.await.unwrap();
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
+ }
+
+ // Ensure the diff is finalized if an error occurs while editing.
+ {
+ model.forbid_requests();
+ let tool = Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ languages.clone(),
+ ));
+ let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+ let edit = cx.update(|cx| {
+ tool.run(
+ EditFileToolInput {
+ display_description: "Edit file".into(),
+ path: path!("/main.rs").into(),
+ mode: EditFileMode::Edit,
+ },
+ stream_tx,
+ cx,
+ )
+ });
+ stream_rx.expect_update_fields().await;
+ let diff = stream_rx.expect_diff().await;
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
+ edit.await.unwrap_err();
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
+ model.allow_requests();
+ }
+
+ // Ensure the diff is finalized if the tool call gets dropped.
+ {
+ let tool = Arc::new(EditFileTool::new(
+ project.clone(),
+ thread.downgrade(),
+ languages.clone(),
+ ));
+ let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
+ let edit = cx.update(|cx| {
+ tool.run(
+ EditFileToolInput {
+ display_description: "Edit file".into(),
+ path: path!("/main.rs").into(),
+ mode: EditFileMode::Edit,
+ },
+ stream_tx,
+ cx,
+ )
+ });
+ stream_rx.expect_update_fields().await;
+ let diff = stream_rx.expect_diff().await;
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
+ drop(edit);
+ cx.run_until_parked();
+ diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
+ }
}
fn init_test(cx: &mut TestAppContext) {
@@ -118,15 +118,19 @@ impl AgentTool for FetchTool {
type Input = FetchToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "fetch".into()
+ fn name() -> &'static str {
+ "fetch"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Fetch
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
match input {
Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(),
Err(_) => "Fetch URL".into(),
@@ -136,12 +140,17 @@ impl AgentTool for FetchTool {
fn run(
self: Arc<Self>,
input: Self::Input,
- _event_stream: ToolCallEventStream,
+ event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
+ let authorize = event_stream.authorize(input.url.clone(), cx);
+
let text = cx.background_spawn({
let http_client = self.http_client.clone();
- async move { Self::build_message(http_client, &input.url).await }
+ async move {
+ authorize.await?;
+ Self::build_message(http_client, &input.url).await
+ }
});
cx.foreground_executor().spawn(async move {
@@ -31,7 +31,6 @@ pub struct FindPathToolInput {
/// You can get back the first two paths by providing a glob of "*thing*.txt"
/// </example>
pub glob: String,
-
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
#[serde(default)]
@@ -86,15 +85,19 @@ impl AgentTool for FindPathTool {
type Input = FindPathToolInput;
type Output = FindPathToolOutput;
- fn name(&self) -> SharedString {
- "find_path".into()
+ fn name() -> &'static str {
+ "find_path"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Search
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
let mut title = "Find paths".to_string();
if let Ok(input) = input {
title.push_str(&format!(" matching “`{}`”", input.glob));
@@ -116,7 +119,7 @@ impl AgentTool for FindPathTool {
..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())];
event_stream.update_fields(acp::ToolCallUpdateFields {
- title: Some(if paginated_matches.len() == 0 {
+ title: Some(if paginated_matches.is_empty() {
"No matches".into()
} else if paginated_matches.len() == 1 {
"1 match".into()
@@ -166,16 +169,17 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
.collect();
cx.background_spawn(async move {
- Ok(snapshots
- .iter()
- .flat_map(|snapshot| {
+ let mut results = Vec::new();
+ for snapshot in snapshots {
+ for entry in snapshot.entries(false, 0) {
let root_name = PathBuf::from(snapshot.root_name());
- snapshot
- .entries(false, 0)
- .map(move |entry| root_name.join(&entry.path))
- .filter(|path| path_matcher.is_match(&path))
- })
- .collect())
+ if path_matcher.is_match(root_name.join(&entry.path)) {
+ results.push(snapshot.abs_path().join(entry.path.as_ref()));
+ }
+ }
+ }
+
+ Ok(results)
})
}
@@ -216,8 +220,8 @@ mod test {
assert_eq!(
matches,
&[
- PathBuf::from("root/apple/banana/carrot"),
- PathBuf::from("root/apple/bandana/carbonara")
+ PathBuf::from(path!("/root/apple/banana/carrot")),
+ PathBuf::from(path!("/root/apple/bandana/carbonara"))
]
);
@@ -228,8 +232,8 @@ mod test {
assert_eq!(
matches,
&[
- PathBuf::from("root/apple/banana/carrot"),
- PathBuf::from("root/apple/bandana/carbonara")
+ PathBuf::from(path!("/root/apple/banana/carrot")),
+ PathBuf::from(path!("/root/apple/bandana/carbonara"))
]
);
}
@@ -27,8 +27,7 @@ use util::paths::PathMatcher;
/// - DO NOT use HTML entities solely to escape characters in the tool parameters.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct GrepToolInput {
- /// A regex pattern to search for in the entire project. Note that the regex
- /// will be parsed by the Rust `regex` crate.
+ /// A regex pattern to search for in the entire project. Note that the regex will be parsed by the Rust `regex` crate.
///
/// Do NOT specify a path here! This will only be matched against the code **content**.
pub regex: String,
@@ -68,15 +67,19 @@ impl AgentTool for GrepTool {
type Input = GrepToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "grep".into()
+ fn name() -> &'static str {
+ "grep"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Search
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
match input {
Ok(input) => {
let page = input.page();
@@ -179,15 +182,14 @@ impl AgentTool for GrepTool {
// Check if this file should be excluded based on its worktree settings
if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| {
project.find_project_path(&path, cx)
- }) {
- if cx.update(|cx| {
+ })
+ && cx.update(|cx| {
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
worktree_settings.is_path_excluded(&project_path.path)
|| worktree_settings.is_path_private(&project_path.path)
}).unwrap_or(false) {
continue;
}
- }
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await?;
@@ -259,10 +261,8 @@ impl AgentTool for GrepTool {
let end_row = range.end.row;
output.push_str("\n### ");
- if let Some(parent_symbols) = &parent_symbols {
- for symbol in parent_symbols {
- write!(output, "{} › ", symbol.text)?;
- }
+ for symbol in parent_symbols {
+ write!(output, "{} › ", symbol.text)?;
}
if range.start.row == end_row {
@@ -275,12 +275,11 @@ impl AgentTool for GrepTool {
output.extend(snapshot.text_for_range(range));
output.push_str("\n```\n");
- if let Some(ancestor_range) = ancestor_range {
- if end_row < ancestor_range.end.row {
+ if let Some(ancestor_range) = ancestor_range
+ && end_row < ancestor_range.end.row {
let remaining_lines = ancestor_range.end.row - end_row;
writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?;
}
- }
matches_found += 1;
}
@@ -320,7 +319,7 @@ mod tests {
init_test(cx);
cx.executor().allow_parking();
- let fs = FakeFs::new(cx.executor().clone());
+ let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
serde_json::json!({
@@ -405,7 +404,7 @@ mod tests {
init_test(cx);
cx.executor().allow_parking();
- let fs = FakeFs::new(cx.executor().clone());
+ let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
serde_json::json!({
@@ -480,7 +479,7 @@ mod tests {
init_test(cx);
cx.executor().allow_parking();
- let fs = FakeFs::new(cx.executor().clone());
+ let fs = FakeFs::new(cx.executor());
// Create test file with syntax structures
fs.insert_tree(
@@ -765,7 +764,7 @@ mod tests {
if cfg!(windows) {
result.replace("root\\", "root/")
} else {
- result.to_string()
+ result
}
}
Err(e) => panic!("Failed to run grep tool: {}", e),
@@ -10,14 +10,12 @@ use std::fmt::Write;
use std::{path::Path, sync::Arc};
use util::markdown::MarkdownInlineCode;
-/// Lists files and directories in a given path. Prefer the `grep` or
-/// `find_path` tools when searching the codebase.
+/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListDirectoryToolInput {
/// The fully-qualified path of the directory to list in the project.
///
- /// This path should never be absolute, and the first component
- /// of the path should always be a root directory in a project.
+ /// This path should never be absolute, and the first component of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following root directories:
@@ -53,15 +51,19 @@ impl AgentTool for ListDirectoryTool {
type Input = ListDirectoryToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "list_directory".into()
+ fn name() -> &'static str {
+ "list_directory"
}
- fn kind(&self) -> ToolKind {
+ fn kind() -> ToolKind {
ToolKind::Read
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
if let Ok(input) = input {
let path = MarkdownInlineCode(&input.path);
format!("List the {path} directory's contents").into()
@@ -8,14 +8,11 @@ use serde::{Deserialize, Serialize};
use std::{path::Path, sync::Arc};
use util::markdown::MarkdownInlineCode;
-/// Moves or rename a file or directory in the project, and returns confirmation
-/// that the move succeeded.
+/// Moves or rename a file or directory in the project, and returns confirmation that the move succeeded.
///
-/// If the source and destination directories are the same, but the filename is
-/// different, this performs a rename. Otherwise, it performs a move.
+/// If the source and destination directories are the same, but the filename is different, this performs a rename. Otherwise, it performs a move.
///
-/// This tool should be used when it's desirable to move or rename a file or
-/// directory without changing its contents at all.
+/// This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct MovePathToolInput {
/// The source path of the file or directory to move/rename.
@@ -55,15 +52,19 @@ impl AgentTool for MovePathTool {
type Input = MovePathToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "move_path".into()
+ fn name() -> &'static str {
+ "move_path"
}
- fn kind(&self) -> ToolKind {
+ fn kind() -> ToolKind {
ToolKind::Move
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
if let Ok(input) = input {
let src = MarkdownInlineCode(&input.source_path);
let dest = MarkdownInlineCode(&input.destination_path);
@@ -11,6 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
+#[schemars(inline)]
pub enum Timezone {
/// Use UTC for the datetime.
Utc,
@@ -32,15 +33,19 @@ impl AgentTool for NowTool {
type Input = NowToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "now".into()
+ fn name() -> &'static str {
+ "now"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Other
}
- fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ _input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
"Get current time".into()
}
@@ -8,19 +8,15 @@ use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use util::markdown::MarkdownEscaped;
-/// This tool opens a file or URL with the default application associated with
-/// it on the user's operating system:
+/// This tool opens a file or URL with the default application associated with it on the user's operating system:
///
/// - On macOS, it's equivalent to the `open` command
/// - On Windows, it's equivalent to `start`
/// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate
///
-/// For example, it can open a web browser with a URL, open a PDF file with the
-/// default PDF viewer, etc.
+/// For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc.
///
-/// You MUST ONLY use this tool when the user has explicitly requested opening
-/// something. You MUST NEVER assume that the user would like for you to use
-/// this tool.
+/// You MUST ONLY use this tool when the user has explicitly requested opening something. You MUST NEVER assume that the user would like for you to use this tool.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct OpenToolInput {
/// The path or URL to open with the default application.
@@ -41,15 +37,19 @@ impl AgentTool for OpenTool {
type Input = OpenToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "open".into()
+ fn name() -> &'static str {
+ "open"
}
- fn kind(&self) -> ToolKind {
+ fn kind() -> ToolKind {
ToolKind::Execute
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
if let Ok(input) = input {
format!("Open `{}`", MarkdownEscaped(&input.path_or_url)).into()
} else {
@@ -65,7 +65,7 @@ impl AgentTool for OpenTool {
) -> Task<Result<Self::Output>> {
// If path_or_url turns out to be a path in the project, make it absolute.
let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx);
- let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
+ let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx);
cx.background_spawn(async move {
authorize.await?;
@@ -11,6 +11,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::sync::Arc;
+use util::markdown::MarkdownCodeBlock;
use crate::{AgentTool, ToolCallEventStream};
@@ -21,8 +22,7 @@ use crate::{AgentTool, ToolCallEventStream};
pub struct ReadFileToolInput {
/// The relative path of the file to read.
///
- /// This path should never be absolute, and the first component
- /// of the path should always be a root directory in a project.
+ /// This path should never be absolute, and the first component of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following root directories:
@@ -34,11 +34,9 @@ pub struct ReadFileToolInput {
/// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.
/// </example>
pub path: String,
-
/// Optional line number to start reading on (1-based index)
#[serde(default)]
pub start_line: Option<u32>,
-
/// Optional line number to end reading on (1-based index, inclusive)
#[serde(default)]
pub end_line: Option<u32>,
@@ -62,31 +60,34 @@ impl AgentTool for ReadFileTool {
type Input = ReadFileToolInput;
type Output = LanguageModelToolResultContent;
- fn name(&self) -> SharedString {
- "read_file".into()
+ fn name() -> &'static str {
+ "read_file"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Read
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
- if let Ok(input) = input {
- let path = &input.path;
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ cx: &mut App,
+ ) -> SharedString {
+ if let Ok(input) = input
+ && let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx)
+ && let Some(path) = self
+ .project
+ .read(cx)
+ .short_full_path_for_project_path(&project_path, cx)
+ {
match (input.start_line, input.end_line) {
(Some(start), Some(end)) => {
- format!(
- "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))",
- path, start, end, path, start, end
- )
+ format!("Read file `{}` (lines {}-{})", path.display(), start, end,)
}
(Some(start), None) => {
- format!(
- "[Read file `{}` (from line {})](@selection:{}:({}-{}))",
- path, start, path, start, start
- )
+ format!("Read file `{}` (from line {})", path.display(), start)
}
- _ => format!("[Read file `{}`](@file:{})", path, path),
+ _ => format!("Read file `{}`", path.display()),
}
.into()
} else {
@@ -103,6 +104,12 @@ impl AgentTool for ReadFileTool {
let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path)));
};
+ let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
+ return Task::ready(Err(anyhow!(
+ "Failed to convert {} to absolute path",
+ &input.path
+ )));
+ };
// Error out if this path is either excluded or private in global settings
let global_settings = WorktreeSettings::get_global(cx);
@@ -138,6 +145,14 @@ impl AgentTool for ReadFileTool {
let file_path = input.path.clone();
+ event_stream.update_fields(ToolCallUpdateFields {
+ locations: Some(vec![acp::ToolCallLocation {
+ path: abs_path,
+ line: input.start_line.map(|line| line.saturating_sub(1)),
+ }]),
+ ..Default::default()
+ });
+
if image_store::is_image_file(&self.project, &project_path, cx) {
return cx.spawn(async move |cx| {
let image_entity: Entity<ImageItem> = cx
@@ -175,7 +190,7 @@ impl AgentTool for ReadFileTool {
buffer
.file()
.as_ref()
- .map_or(true, |file| !file.disk_state().exists())
+ .is_none_or(|file| !file.disk_state().exists())
})? {
anyhow::bail!("{file_path} not found");
}
@@ -246,21 +261,25 @@ impl AgentTool for ReadFileTool {
};
project.update(cx, |project, cx| {
- if let Some(abs_path) = project.absolute_path(&project_path, cx) {
- project.set_agent_location(
- Some(AgentLocation {
- buffer: buffer.downgrade(),
- position: anchor.unwrap_or(text::Anchor::MIN),
- }),
- cx,
- );
+ project.set_agent_location(
+ Some(AgentLocation {
+ buffer: buffer.downgrade(),
+ position: anchor.unwrap_or(text::Anchor::MIN),
+ }),
+ cx,
+ );
+ if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
+ let markdown = MarkdownCodeBlock {
+ tag: &input.path,
+ text,
+ }
+ .to_string();
event_stream.update_fields(ToolCallUpdateFields {
- locations: Some(vec![acp::ToolCallLocation {
- path: abs_path,
- line: input.start_line.map(|line| line.saturating_sub(1)),
+ content: Some(vec![acp::ToolCallContent::Content {
+ content: markdown.into(),
}]),
..Default::default()
- });
+ })
}
})?;
@@ -1,19 +1,19 @@
use agent_client_protocol as acp;
use anyhow::Result;
-use futures::{FutureExt as _, future::Shared};
-use gpui::{App, AppContext, Entity, SharedString, Task};
-use project::{Project, terminals::TerminalKind};
+use gpui::{App, Entity, SharedString, Task};
+use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
+ rc::Rc,
sync::Arc,
};
-use util::{ResultExt, get_system_shell, markdown::MarkdownInlineCode};
+use util::markdown::MarkdownInlineCode;
-use crate::{AgentTool, ToolCallEventStream};
+use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream};
-const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024;
+const COMMAND_OUTPUT_LIMIT: u64 = 16 * 1024;
/// Executes a shell one-liner and returns the combined output.
///
@@ -36,28 +36,14 @@ pub struct TerminalToolInput {
pub struct TerminalTool {
project: Entity<Project>,
- determine_shell: Shared<Task<String>>,
+ environment: Rc<dyn ThreadEnvironment>,
}
impl TerminalTool {
- pub fn new(project: Entity<Project>, cx: &mut App) -> Self {
- let determine_shell = cx.background_spawn(async move {
- if cfg!(windows) {
- return get_system_shell();
- }
-
- if which::which("bash").is_ok() {
- log::info!("agent selected bash for terminal tool");
- "bash".into()
- } else {
- let shell = get_system_shell();
- log::info!("agent selected {shell} for terminal tool");
- shell
- }
- });
+ pub fn new(project: Entity<Project>, environment: Rc<dyn ThreadEnvironment>) -> Self {
Self {
project,
- determine_shell: determine_shell.shared(),
+ environment,
}
}
}
@@ -66,21 +52,25 @@ impl AgentTool for TerminalTool {
type Input = TerminalToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "terminal".into()
+ fn name() -> &'static str {
+ "terminal"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Execute
}
- fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
if let Ok(input) = input {
let mut lines = input.command.lines();
let first_line = lines.next().unwrap_or_default();
let remaining_line_count = lines.count();
match remaining_line_count {
- 0 => MarkdownInlineCode(&first_line).to_string().into(),
+ 0 => MarkdownInlineCode(first_line).to_string().into(),
1 => MarkdownInlineCode(&format!(
"{} - {} more line",
first_line, remaining_line_count
@@ -102,128 +92,49 @@ impl AgentTool for TerminalTool {
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
- let language_registry = self.project.read(cx).languages().clone();
let working_dir = match working_dir(&input, &self.project, cx) {
Ok(dir) => dir,
Err(err) => return Task::ready(Err(err)),
};
- let program = self.determine_shell.clone();
- let command = if cfg!(windows) {
- format!("$null | & {{{}}}", input.command.replace("\"", "'"))
- } else if let Some(cwd) = working_dir
- .as_ref()
- .and_then(|cwd| cwd.as_os_str().to_str())
- {
- // Make sure once we're *inside* the shell, we cd into `cwd`
- format!("(cd {cwd}; {}) </dev/null", input.command)
- } else {
- format!("({}) </dev/null", input.command)
- };
- let args = vec!["-c".into(), command];
-
- let env = match &working_dir {
- Some(dir) => self.project.update(cx, |project, cx| {
- project.directory_environment(dir.as_path().into(), cx)
- }),
- None => Task::ready(None).shared(),
- };
- let env = cx.spawn(async move |_| {
- let mut env = env.await.unwrap_or_default();
- if cfg!(unix) {
- env.insert("PAGER".into(), "cat".into());
- }
- env
- });
-
- let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
-
- cx.spawn({
- async move |cx| {
- authorize.await?;
-
- let program = program.await;
- let env = env.await;
- let terminal = self
- .project
- .update(cx, |project, cx| {
- project.create_terminal(
- TerminalKind::Task(task::SpawnInTerminal {
- command: Some(program),
- args,
- cwd: working_dir.clone(),
- env,
- ..Default::default()
- }),
- cx,
- )
- })?
- .await?;
- let acp_terminal = cx.new(|cx| {
- acp_thread::Terminal::new(
- input.command.clone(),
- working_dir.clone(),
- terminal.clone(),
- language_registry,
- cx,
- )
- })?;
- event_stream.update_terminal(acp_terminal.clone());
-
- let exit_status = terminal
- .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
- .await;
- let (content, content_line_count) = terminal.read_with(cx, |terminal, _| {
- (terminal.get_content(), terminal.total_lines())
- })?;
+ let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx);
+ cx.spawn(async move |cx| {
+ authorize.await?;
+
+ let terminal = self
+ .environment
+ .create_terminal(
+ input.command.clone(),
+ working_dir,
+ Some(COMMAND_OUTPUT_LIMIT),
+ cx,
+ )
+ .await?;
- let (processed_content, finished_with_empty_output) = process_content(
- &content,
- &input.command,
- exit_status.map(portable_pty::ExitStatus::from),
- );
+ let terminal_id = terminal.id(cx)?;
+ event_stream.update_fields(acp::ToolCallUpdateFields {
+ content: Some(vec![acp::ToolCallContent::Terminal { terminal_id }]),
+ ..Default::default()
+ });
- acp_terminal
- .update(cx, |terminal, cx| {
- terminal.finish(
- exit_status,
- content.len(),
- processed_content.len(),
- content_line_count,
- finished_with_empty_output,
- cx,
- );
- })
- .log_err();
+ let exit_status = terminal.wait_for_exit(cx)?.await;
+ let output = terminal.current_output(cx)?;
- Ok(processed_content)
- }
+ Ok(process_content(output, &input.command, exit_status))
})
}
}
fn process_content(
- content: &str,
+ output: acp::TerminalOutputResponse,
command: &str,
- exit_status: Option<portable_pty::ExitStatus>,
-) -> (String, bool) {
- let should_truncate = content.len() > COMMAND_OUTPUT_LIMIT;
-
- let content = if should_truncate {
- let mut end_ix = COMMAND_OUTPUT_LIMIT.min(content.len());
- while !content.is_char_boundary(end_ix) {
- end_ix -= 1;
- }
- // Don't truncate mid-line, clear the remainder of the last line
- end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
- &content[..end_ix]
- } else {
- content
- };
- let content = content.trim();
+ exit_status: acp::TerminalExitStatus,
+) -> String {
+ let content = output.output.trim();
let is_empty = content.is_empty();
+
let content = format!("```\n{content}\n```");
- let content = if should_truncate {
+ let content = if output.truncated {
format!(
"Command output too long. The first {} bytes:\n\n{content}",
content.len(),
@@ -232,24 +143,21 @@ fn process_content(
content
};
- let content = match exit_status {
- Some(exit_status) if exit_status.success() => {
+ let content = match exit_status.exit_code {
+ Some(0) => {
if is_empty {
"Command executed successfully.".to_string()
} else {
- content.to_string()
+ content
}
}
- Some(exit_status) => {
+ Some(exit_code) => {
if is_empty {
- format!(
- "Command \"{command}\" failed with exit code {}.",
- exit_status.exit_code()
- )
+ format!("Command \"{command}\" failed with exit code {}.", exit_code)
} else {
format!(
"Command \"{command}\" failed with exit code {}.\n\n{content}",
- exit_status.exit_code()
+ exit_code
)
}
}
@@ -260,7 +168,7 @@ fn process_content(
)
}
};
- (content, is_empty)
+ content
}
fn working_dir(
@@ -271,7 +179,7 @@ fn working_dir(
let project = project.read(cx);
let cd = &input.cd;
- if cd == "." || cd == "" {
+ if cd == "." || cd.is_empty() {
// Accept "." or "" as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
@@ -296,178 +204,10 @@ fn working_dir(
{
return Ok(Some(input_path.into()));
}
- } else {
- if let Some(worktree) = project.worktree_for_root_name(cd, cx) {
- return Ok(Some(worktree.read(cx).abs_path().to_path_buf()));
- }
+ } else if let Some(worktree) = project.worktree_for_root_name(cd, cx) {
+ return Ok(Some(worktree.read(cx).abs_path().to_path_buf()));
}
anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees.");
}
}
-
-#[cfg(test)]
-mod tests {
- use agent_settings::AgentSettings;
- use editor::EditorSettings;
- use fs::RealFs;
- use gpui::{BackgroundExecutor, TestAppContext};
- use pretty_assertions::assert_eq;
- use serde_json::json;
- use settings::{Settings, SettingsStore};
- use terminal::terminal_settings::TerminalSettings;
- use theme::ThemeSettings;
- use util::test::TempTree;
-
- use crate::AgentResponseEvent;
-
- use super::*;
-
- fn init_test(executor: &BackgroundExecutor, cx: &mut TestAppContext) {
- zlog::init_test();
-
- executor.allow_parking();
- cx.update(|cx| {
- let settings_store = SettingsStore::test(cx);
- cx.set_global(settings_store);
- language::init(cx);
- Project::init_settings(cx);
- ThemeSettings::register(cx);
- TerminalSettings::register(cx);
- EditorSettings::register(cx);
- AgentSettings::register(cx);
- });
- }
-
- #[gpui::test]
- async fn test_interactive_command(executor: BackgroundExecutor, cx: &mut TestAppContext) {
- if cfg!(windows) {
- return;
- }
-
- init_test(&executor, cx);
-
- let fs = Arc::new(RealFs::new(None, executor));
- let tree = TempTree::new(json!({
- "project": {},
- }));
- let project: Entity<Project> =
- Project::test(fs, [tree.path().join("project").as_path()], cx).await;
-
- let input = TerminalToolInput {
- command: "cat".to_owned(),
- cd: tree
- .path()
- .join("project")
- .as_path()
- .to_string_lossy()
- .to_string(),
- };
- let (event_stream_tx, mut event_stream_rx) = ToolCallEventStream::test();
- let result = cx
- .update(|cx| Arc::new(TerminalTool::new(project, cx)).run(input, event_stream_tx, cx));
-
- let auth = event_stream_rx.expect_authorization().await;
- auth.response.send(auth.options[0].id.clone()).unwrap();
- event_stream_rx.expect_terminal().await;
- assert_eq!(result.await.unwrap(), "Command executed successfully.");
- }
-
- #[gpui::test]
- async fn test_working_directory(executor: BackgroundExecutor, cx: &mut TestAppContext) {
- if cfg!(windows) {
- return;
- }
-
- init_test(&executor, cx);
-
- let fs = Arc::new(RealFs::new(None, executor));
- let tree = TempTree::new(json!({
- "project": {},
- "other-project": {},
- }));
- let project: Entity<Project> =
- Project::test(fs, [tree.path().join("project").as_path()], cx).await;
-
- let check = |input, expected, cx: &mut TestAppContext| {
- let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
- let result = cx.update(|cx| {
- Arc::new(TerminalTool::new(project.clone(), cx)).run(input, stream_tx, cx)
- });
- cx.run_until_parked();
- let event = stream_rx.try_next();
- if let Ok(Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth)))) = event {
- auth.response.send(auth.options[0].id.clone()).unwrap();
- }
-
- cx.spawn(async move |_| {
- let output = result.await;
- assert_eq!(output.ok(), expected);
- })
- };
-
- check(
- TerminalToolInput {
- command: "pwd".into(),
- cd: ".".into(),
- },
- Some(format!(
- "```\n{}\n```",
- tree.path().join("project").display()
- )),
- cx,
- )
- .await;
-
- check(
- TerminalToolInput {
- command: "pwd".into(),
- cd: "other-project".into(),
- },
- None, // other-project is a dir, but *not* a worktree (yet)
- cx,
- )
- .await;
-
- // Absolute path above the worktree root
- check(
- TerminalToolInput {
- command: "pwd".into(),
- cd: tree.path().to_string_lossy().into(),
- },
- None,
- cx,
- )
- .await;
-
- project
- .update(cx, |project, cx| {
- project.create_worktree(tree.path().join("other-project"), true, cx)
- })
- .await
- .unwrap();
-
- check(
- TerminalToolInput {
- command: "pwd".into(),
- cd: "other-project".into(),
- },
- Some(format!(
- "```\n{}\n```",
- tree.path().join("other-project").display()
- )),
- cx,
- )
- .await;
-
- check(
- TerminalToolInput {
- command: "pwd".into(),
- cd: ".".into(),
- },
- None,
- cx,
- )
- .await;
- }
-}
@@ -11,8 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
/// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ThinkingToolInput {
- /// Content to think about. This should be a description of what to think about or
- /// a problem to solve.
+ /// Content to think about. This should be a description of what to think about or a problem to solve.
content: String,
}
@@ -22,15 +21,19 @@ impl AgentTool for ThinkingTool {
type Input = ThinkingToolInput;
type Output = String;
- fn name(&self) -> SharedString {
- "thinking".into()
+ fn name() -> &'static str {
+ "thinking"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Think
}
- fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ _input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
"Thinking".into()
}
@@ -14,7 +14,7 @@ use ui::prelude::*;
use web_search::WebSearchRegistry;
/// Search the web for information using your query.
-/// Use this when you need real-time information, facts, or data that might not be in your training. \
+/// Use this when you need real-time information, facts, or data that might not be in your training.
/// Results will include snippets and links from relevant web pages.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WebSearchToolInput {
@@ -40,15 +40,19 @@ impl AgentTool for WebSearchTool {
type Input = WebSearchToolInput;
type Output = WebSearchToolOutput;
- fn name(&self) -> SharedString {
- "web_search".into()
+ fn name() -> &'static str {
+ "web_search"
}
- fn kind(&self) -> acp::ToolKind {
+ fn kind() -> acp::ToolKind {
acp::ToolKind::Fetch
}
- fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
+ fn initial_title(
+ &self,
+ _input: Result<Self::Input, serde_json::Value>,
+ _cx: &mut App,
+ ) -> SharedString {
"Searching the Web".into()
}
@@ -80,33 +84,48 @@ impl AgentTool for WebSearchTool {
}
};
- let result_text = if response.results.len() == 1 {
- "1 result".to_string()
- } else {
- format!("{} results", response.results.len())
- };
- event_stream.update_fields(acp::ToolCallUpdateFields {
- title: Some(format!("Searched the web: {result_text}")),
- content: Some(
- response
- .results
- .iter()
- .map(|result| acp::ToolCallContent::Content {
- content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
- name: result.title.clone(),
- uri: result.url.clone(),
- title: Some(result.title.clone()),
- description: Some(result.text.clone()),
- mime_type: None,
- annotations: None,
- size: None,
- }),
- })
- .collect(),
- ),
- ..Default::default()
- });
+ emit_update(&response, &event_stream);
Ok(WebSearchToolOutput(response))
})
}
+
+ fn replay(
+ &self,
+ _input: Self::Input,
+ output: Self::Output,
+ event_stream: ToolCallEventStream,
+ _cx: &mut App,
+ ) -> Result<()> {
+ emit_update(&output.0, &event_stream);
+ Ok(())
+ }
+}
+
+fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream) {
+ let result_text = if response.results.len() == 1 {
+ "1 result".to_string()
+ } else {
+ format!("{} results", response.results.len())
+ };
+ event_stream.update_fields(acp::ToolCallUpdateFields {
+ title: Some(format!("Searched the web: {result_text}")),
+ content: Some(
+ response
+ .results
+ .iter()
+ .map(|result| acp::ToolCallContent::Content {
+ content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
+ name: result.title.clone(),
+ uri: result.url.clone(),
+ title: Some(result.title.clone()),
+ description: Some(result.text.clone()),
+ mime_type: None,
+ annotations: None,
+ size: None,
+ }),
+ })
+ .collect(),
+ ),
+ ..Default::default()
+ });
}
@@ -6,7 +6,7 @@ publish.workspace = true
license = "GPL-3.0-or-later"
[features]
-test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"]
+test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
e2e = []
[lints]
@@ -17,33 +17,36 @@ path = "src/agent_servers.rs"
doctest = false
[dependencies]
+acp_tools.workspace = true
acp_thread.workspace = true
+action_log.workspace = true
agent-client-protocol.workspace = true
-agentic-coding-protocol.workspace = true
+agent_settings.workspace = true
anyhow.workspace = true
+client.workspace = true
collections.workspace = true
-context_server.workspace = true
+env_logger = { workspace = true, optional = true }
+fs.workspace = true
futures.workspace = true
gpui.workspace = true
+gpui_tokio = { workspace = true, optional = true }
indoc.workspace = true
-itertools.workspace = true
+language.workspace = true
+language_model.workspace = true
+language_models.workspace = true
log.workspace = true
-paths.workspace = true
project.workspace = true
-rand.workspace = true
-schemars.workspace = true
+reqwest_client = { workspace = true, optional = true }
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
-strum.workspace = true
+task.workspace = true
tempfile.workspace = true
thiserror.workspace = true
ui.workspace = true
util.workspace = true
-uuid.workspace = true
watch.workspace = true
-which.workspace = true
workspace-hack.workspace = true
[target.'cfg(unix)'.dependencies]
@@ -51,8 +54,12 @@ libc.workspace = true
nix.workspace = true
[dev-dependencies]
+client = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
+fs.workspace = true
language.workspace = true
indoc.workspace = true
acp_thread = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
+gpui_tokio.workspace = true
+reqwest_client = { workspace = true, features = ["test-support"] }
@@ -1,34 +1,670 @@
-use std::{path::Path, rc::Rc};
-
-use crate::AgentServerCommand;
use acp_thread::AgentConnection;
-use anyhow::Result;
-use gpui::AsyncApp;
+use acp_tools::AcpConnectionRegistry;
+use action_log::ActionLog;
+use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
+use anyhow::anyhow;
+use collections::HashMap;
+use futures::AsyncBufReadExt as _;
+use futures::io::BufReader;
+use project::Project;
+use project::agent_server_store::AgentServerCommand;
+use serde::Deserialize;
+use util::ResultExt as _;
+
+use std::path::PathBuf;
+use std::{any::Any, cell::RefCell};
+use std::{path::Path, rc::Rc};
use thiserror::Error;
-mod v0;
-mod v1;
+use anyhow::{Context as _, Result};
+use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity};
+
+use acp_thread::{AcpThread, AuthRequired, LoadError};
#[derive(Debug, Error)]
#[error("Unsupported version")]
pub struct UnsupportedVersion;
+pub struct AcpConnection {
+ server_name: SharedString,
+ connection: Rc<acp::ClientSideConnection>,
+ sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
+ auth_methods: Vec<acp::AuthMethod>,
+ agent_capabilities: acp::AgentCapabilities,
+ default_mode: Option<acp::SessionModeId>,
+ root_dir: PathBuf,
+ // NB: Don't move this into the wait_task, since we need to ensure the process is
+ // killed on drop (setting kill_on_drop on the command seems to not always work).
+ child: smol::process::Child,
+ _io_task: Task<Result<()>>,
+ _wait_task: Task<Result<()>>,
+ _stderr_task: Task<Result<()>>,
+}
+
+pub struct AcpSession {
+ thread: WeakEntity<AcpThread>,
+ suppress_abort_err: bool,
+ session_modes: Option<Rc<RefCell<acp::SessionModeState>>>,
+}
+
pub async fn connect(
- server_name: &'static str,
+ server_name: SharedString,
command: AgentServerCommand,
root_dir: &Path,
+ default_mode: Option<acp::SessionModeId>,
+ is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Rc<dyn AgentConnection>> {
- let conn = v1::AcpConnection::stdio(server_name, command.clone(), &root_dir, cx).await;
-
- match conn {
- Ok(conn) => Ok(Rc::new(conn) as _),
- Err(err) if err.is::<UnsupportedVersion>() => {
- // Consider re-using initialize response and subprocess when adding another version here
- let conn: Rc<dyn AgentConnection> =
- Rc::new(v0::AcpConnection::stdio(server_name, command, &root_dir, cx).await?);
- Ok(conn)
+ let conn = AcpConnection::stdio(
+ server_name,
+ command.clone(),
+ root_dir,
+ default_mode,
+ is_remote,
+ cx,
+ )
+ .await?;
+ Ok(Rc::new(conn) as _)
+}
+
+const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
+
+impl AcpConnection {
+ pub async fn stdio(
+ server_name: SharedString,
+ command: AgentServerCommand,
+ root_dir: &Path,
+ default_mode: Option<acp::SessionModeId>,
+ is_remote: bool,
+ cx: &mut AsyncApp,
+ ) -> Result<Self> {
+ let mut child = util::command::new_smol_command(command.path);
+ child
+ .args(command.args.iter().map(|arg| arg.as_str()))
+ .envs(command.env.iter().flatten())
+ .stdin(std::process::Stdio::piped())
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped());
+ if !is_remote {
+ child.current_dir(root_dir);
+ }
+ let mut child = child.spawn()?;
+
+ let stdout = child.stdout.take().context("Failed to take stdout")?;
+ let stdin = child.stdin.take().context("Failed to take stdin")?;
+ let stderr = child.stderr.take().context("Failed to take stderr")?;
+ log::trace!("Spawned (pid: {})", child.id());
+
+ let sessions = Rc::new(RefCell::new(HashMap::default()));
+
+ let client = ClientDelegate {
+ sessions: sessions.clone(),
+ cx: cx.clone(),
+ };
+ let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
+ let foreground_executor = cx.foreground_executor().clone();
+ move |fut| {
+ foreground_executor.spawn(fut).detach();
+ }
+ });
+
+ let io_task = cx.background_spawn(io_task);
+
+ let stderr_task = cx.background_spawn(async move {
+ let mut stderr = BufReader::new(stderr);
+ let mut line = String::new();
+ while let Ok(n) = stderr.read_line(&mut line).await
+ && n > 0
+ {
+ log::warn!("agent stderr: {}", &line);
+ line.clear();
+ }
+ Ok(())
+ });
+
+ let wait_task = cx.spawn({
+ let sessions = sessions.clone();
+ let status_fut = child.status();
+ async move |cx| {
+ let status = status_fut.await?;
+
+ for session in sessions.borrow().values() {
+ session
+ .thread
+ .update(cx, |thread, cx| {
+ thread.emit_load_error(LoadError::Exited { status }, cx)
+ })
+ .ok();
+ }
+
+ anyhow::Ok(())
+ }
+ });
+
+ let connection = Rc::new(connection);
+
+ cx.update(|cx| {
+ AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
+ registry.set_active_connection(server_name.clone(), &connection, cx)
+ });
+ })?;
+
+ let response = connection
+ .initialize(acp::InitializeRequest {
+ protocol_version: acp::VERSION,
+ client_capabilities: acp::ClientCapabilities {
+ fs: acp::FileSystemCapability {
+ read_text_file: true,
+ write_text_file: true,
+ },
+ terminal: true,
+ },
+ })
+ .await?;
+
+ if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
+ return Err(UnsupportedVersion.into());
}
- Err(err) => Err(err),
+
+ Ok(Self {
+ auth_methods: response.auth_methods,
+ root_dir: root_dir.to_owned(),
+ connection,
+ server_name,
+ sessions,
+ agent_capabilities: response.agent_capabilities,
+ default_mode,
+ _io_task: io_task,
+ _wait_task: wait_task,
+ _stderr_task: stderr_task,
+ child,
+ })
+ }
+
+ pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
+ &self.agent_capabilities.prompt_capabilities
+ }
+
+ pub fn root_dir(&self) -> &Path {
+ &self.root_dir
+ }
+}
+
+impl Drop for AcpConnection {
+ fn drop(&mut self) {
+ // See the comment on the child field.
+ self.child.kill().log_err();
+ }
+}
+
+impl AgentConnection for AcpConnection {
+ fn new_thread(
+ self: Rc<Self>,
+ project: Entity<Project>,
+ cwd: &Path,
+ cx: &mut App,
+ ) -> Task<Result<Entity<AcpThread>>> {
+ let name = self.server_name.clone();
+ let conn = self.connection.clone();
+ let sessions = self.sessions.clone();
+ let default_mode = self.default_mode.clone();
+ let cwd = cwd.to_path_buf();
+ let context_server_store = project.read(cx).context_server_store().read(cx);
+ let mcp_servers = if project.read(cx).is_local() {
+ context_server_store
+ .configured_server_ids()
+ .iter()
+ .filter_map(|id| {
+ let configuration = context_server_store.configuration_for_server(id)?;
+ let command = configuration.command();
+ Some(acp::McpServer::Stdio {
+ name: id.0.to_string(),
+ command: command.path.clone(),
+ args: command.args.clone(),
+ env: if let Some(env) = command.env.as_ref() {
+ env.iter()
+ .map(|(name, value)| acp::EnvVariable {
+ name: name.clone(),
+ value: value.clone(),
+ })
+ .collect()
+ } else {
+ vec![]
+ },
+ })
+ })
+ .collect()
+ } else {
+ // In SSH projects, the external agent is running on the remote
+ // machine, and currently we only run MCP servers on the local
+ // machine. So don't pass any MCP servers to the agent in that case.
+ Vec::new()
+ };
+
+ cx.spawn(async move |cx| {
+ let response = conn
+ .new_session(acp::NewSessionRequest { mcp_servers, cwd })
+ .await
+ .map_err(|err| {
+ if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
+ let mut error = AuthRequired::new();
+
+ if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
+ error = error.with_description(err.message);
+ }
+
+ anyhow!(error)
+ } else {
+ anyhow!(err)
+ }
+ })?;
+
+ let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
+
+ if let Some(default_mode) = default_mode {
+ if let Some(modes) = modes.as_ref() {
+ let mut modes_ref = modes.borrow_mut();
+ let has_mode = modes_ref.available_modes.iter().any(|mode| mode.id == default_mode);
+
+ if has_mode {
+ let initial_mode_id = modes_ref.current_mode_id.clone();
+
+ cx.spawn({
+ let default_mode = default_mode.clone();
+ let session_id = response.session_id.clone();
+ let modes = modes.clone();
+ async move |_| {
+ let result = conn.set_session_mode(acp::SetSessionModeRequest {
+ session_id,
+ mode_id: default_mode,
+ })
+ .await.log_err();
+
+ if result.is_none() {
+ modes.borrow_mut().current_mode_id = initial_mode_id;
+ }
+ }
+ }).detach();
+
+ modes_ref.current_mode_id = default_mode;
+ } else {
+ let available_modes = modes_ref
+ .available_modes
+ .iter()
+ .map(|mode| format!("- `{}`: {}", mode.id, mode.name))
+ .collect::<Vec<_>>()
+ .join("\n");
+
+ log::warn!(
+ "`{default_mode}` is not valid {name} mode. Available options:\n{available_modes}",
+ );
+ }
+ } else {
+ log::warn!(
+ "`{name}` does not support modes, but `default_mode` was set in settings.",
+ );
+ }
+ }
+
+ let session_id = response.session_id;
+ let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
+ let thread = cx.new(|cx| {
+ AcpThread::new(
+ self.server_name.clone(),
+ self.clone(),
+ project,
+ action_log,
+ session_id.clone(),
+ // ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
+ watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
+ cx,
+ )
+ })?;
+
+ let session = AcpSession {
+ thread: thread.downgrade(),
+ suppress_abort_err: false,
+ session_modes: modes
+ };
+ sessions.borrow_mut().insert(session_id, session);
+
+ Ok(thread)
+ })
+ }
+
+ fn auth_methods(&self) -> &[acp::AuthMethod] {
+ &self.auth_methods
+ }
+
+ fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
+ let conn = self.connection.clone();
+ cx.foreground_executor().spawn(async move {
+ let result = conn
+ .authenticate(acp::AuthenticateRequest {
+ method_id: method_id.clone(),
+ })
+ .await?;
+
+ Ok(result)
+ })
+ }
+
+ fn prompt(
+ &self,
+ _id: Option<acp_thread::UserMessageId>,
+ params: acp::PromptRequest,
+ cx: &mut App,
+ ) -> Task<Result<acp::PromptResponse>> {
+ let conn = self.connection.clone();
+ let sessions = self.sessions.clone();
+ let session_id = params.session_id.clone();
+ cx.foreground_executor().spawn(async move {
+ let result = conn.prompt(params).await;
+
+ let mut suppress_abort_err = false;
+
+ if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
+ suppress_abort_err = session.suppress_abort_err;
+ session.suppress_abort_err = false;
+ }
+
+ match result {
+ Ok(response) => Ok(response),
+ Err(err) => {
+ if err.code != ErrorCode::INTERNAL_ERROR.code {
+ anyhow::bail!(err)
+ }
+
+ let Some(data) = &err.data else {
+ anyhow::bail!(err)
+ };
+
+ // Temporary workaround until the following PR is generally available:
+ // https://github.com/google-gemini/gemini-cli/pull/6656
+
+ #[derive(Deserialize)]
+ #[serde(deny_unknown_fields)]
+ struct ErrorDetails {
+ details: Box<str>,
+ }
+
+ match serde_json::from_value(data.clone()) {
+ Ok(ErrorDetails { details }) => {
+ if suppress_abort_err
+ && (details.contains("This operation was aborted")
+ || details.contains("The user aborted a request"))
+ {
+ Ok(acp::PromptResponse {
+ stop_reason: acp::StopReason::Cancelled,
+ })
+ } else {
+ Err(anyhow!(details))
+ }
+ }
+ Err(_) => Err(anyhow!(err)),
+ }
+ }
+ }
+ })
+ }
+
+ fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
+ if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
+ session.suppress_abort_err = true;
+ }
+ let conn = self.connection.clone();
+ let params = acp::CancelNotification {
+ session_id: session_id.clone(),
+ };
+ cx.foreground_executor()
+ .spawn(async move { conn.cancel(params).await })
+ .detach();
+ }
+
+ fn session_modes(
+ &self,
+ session_id: &acp::SessionId,
+ _cx: &App,
+ ) -> Option<Rc<dyn acp_thread::AgentSessionModes>> {
+ let sessions = self.sessions.clone();
+ let sessions_ref = sessions.borrow();
+ let Some(session) = sessions_ref.get(session_id) else {
+ return None;
+ };
+
+ if let Some(modes) = session.session_modes.as_ref() {
+ Some(Rc::new(AcpSessionModes {
+ connection: self.connection.clone(),
+ session_id: session_id.clone(),
+ state: modes.clone(),
+ }) as _)
+ } else {
+ None
+ }
+ }
+
+ fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+ self
+ }
+}
+
+struct AcpSessionModes {
+ session_id: acp::SessionId,
+ connection: Rc<acp::ClientSideConnection>,
+ state: Rc<RefCell<acp::SessionModeState>>,
+}
+
+impl acp_thread::AgentSessionModes for AcpSessionModes {
+ fn current_mode(&self) -> acp::SessionModeId {
+ self.state.borrow().current_mode_id.clone()
+ }
+
+ fn all_modes(&self) -> Vec<acp::SessionMode> {
+ self.state.borrow().available_modes.clone()
+ }
+
+ fn set_mode(&self, mode_id: acp::SessionModeId, cx: &mut App) -> Task<Result<()>> {
+ let connection = self.connection.clone();
+ let session_id = self.session_id.clone();
+ let old_mode_id;
+ {
+ let mut state = self.state.borrow_mut();
+ old_mode_id = state.current_mode_id.clone();
+ state.current_mode_id = mode_id.clone();
+ };
+ let state = self.state.clone();
+ cx.foreground_executor().spawn(async move {
+ let result = connection
+ .set_session_mode(acp::SetSessionModeRequest {
+ session_id,
+ mode_id,
+ })
+ .await;
+
+ if result.is_err() {
+ state.borrow_mut().current_mode_id = old_mode_id;
+ }
+
+ result?;
+
+ Ok(())
+ })
+ }
+}
+
+struct ClientDelegate {
+ sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
+ cx: AsyncApp,
+}
+
+impl acp::Client for ClientDelegate {
+ async fn request_permission(
+ &self,
+ arguments: acp::RequestPermissionRequest,
+ ) -> Result<acp::RequestPermissionResponse, acp::Error> {
+ let respect_always_allow_setting;
+ let thread;
+ {
+ let sessions_ref = self.sessions.borrow();
+ let session = sessions_ref
+ .get(&arguments.session_id)
+ .context("Failed to get session")?;
+ respect_always_allow_setting = session.session_modes.is_none();
+ thread = session.thread.clone();
+ }
+
+ let cx = &mut self.cx.clone();
+
+ let task = thread.update(cx, |thread, cx| {
+ thread.request_tool_call_authorization(
+ arguments.tool_call,
+ arguments.options,
+ respect_always_allow_setting,
+ cx,
+ )
+ })??;
+
+ let outcome = task.await;
+
+ Ok(acp::RequestPermissionResponse { outcome })
+ }
+
+ async fn write_text_file(
+ &self,
+ arguments: acp::WriteTextFileRequest,
+ ) -> Result<(), acp::Error> {
+ let cx = &mut self.cx.clone();
+ let task = self
+ .session_thread(&arguments.session_id)?
+ .update(cx, |thread, cx| {
+ thread.write_text_file(arguments.path, arguments.content, cx)
+ })?;
+
+ task.await?;
+
+ Ok(())
+ }
+
+ async fn read_text_file(
+ &self,
+ arguments: acp::ReadTextFileRequest,
+ ) -> Result<acp::ReadTextFileResponse, acp::Error> {
+ let task = self.session_thread(&arguments.session_id)?.update(
+ &mut self.cx.clone(),
+ |thread, cx| {
+ thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
+ },
+ )?;
+
+ let content = task.await?;
+
+ Ok(acp::ReadTextFileResponse { content })
+ }
+
+ async fn session_notification(
+ &self,
+ notification: acp::SessionNotification,
+ ) -> Result<(), acp::Error> {
+ let sessions = self.sessions.borrow();
+ let session = sessions
+ .get(¬ification.session_id)
+ .context("Failed to get session")?;
+
+ if let acp::SessionUpdate::CurrentModeUpdate { current_mode_id } = ¬ification.update {
+ if let Some(session_modes) = &session.session_modes {
+ session_modes.borrow_mut().current_mode_id = current_mode_id.clone();
+ } else {
+ log::error!(
+ "Got a `CurrentModeUpdate` notification, but they agent didn't specify `modes` during setting setup."
+ );
+ }
+ }
+
+ session.thread.update(&mut self.cx.clone(), |thread, cx| {
+ thread.handle_session_update(notification.update, cx)
+ })??;
+
+ Ok(())
+ }
+
+ async fn create_terminal(
+ &self,
+ args: acp::CreateTerminalRequest,
+ ) -> Result<acp::CreateTerminalResponse, acp::Error> {
+ let terminal = self
+ .session_thread(&args.session_id)?
+ .update(&mut self.cx.clone(), |thread, cx| {
+ thread.create_terminal(
+ args.command,
+ args.args,
+ args.env,
+ args.cwd,
+ args.output_byte_limit,
+ cx,
+ )
+ })?
+ .await?;
+ Ok(
+ terminal.read_with(&self.cx, |terminal, _| acp::CreateTerminalResponse {
+ terminal_id: terminal.id().clone(),
+ })?,
+ )
+ }
+
+ async fn kill_terminal(&self, args: acp::KillTerminalRequest) -> Result<(), acp::Error> {
+ self.session_thread(&args.session_id)?
+ .update(&mut self.cx.clone(), |thread, cx| {
+ thread.kill_terminal(args.terminal_id, cx)
+ })??;
+
+ Ok(())
+ }
+
+ async fn release_terminal(&self, args: acp::ReleaseTerminalRequest) -> Result<(), acp::Error> {
+ self.session_thread(&args.session_id)?
+ .update(&mut self.cx.clone(), |thread, cx| {
+ thread.release_terminal(args.terminal_id, cx)
+ })??;
+
+ Ok(())
+ }
+
+ async fn terminal_output(
+ &self,
+ args: acp::TerminalOutputRequest,
+ ) -> Result<acp::TerminalOutputResponse, acp::Error> {
+ self.session_thread(&args.session_id)?
+ .read_with(&mut self.cx.clone(), |thread, cx| {
+ let out = thread
+ .terminal(args.terminal_id)?
+ .read(cx)
+ .current_output(cx);
+
+ Ok(out)
+ })?
+ }
+
+ async fn wait_for_terminal_exit(
+ &self,
+ args: acp::WaitForTerminalExitRequest,
+ ) -> Result<acp::WaitForTerminalExitResponse, acp::Error> {
+ let exit_status = self
+ .session_thread(&args.session_id)?
+ .update(&mut self.cx.clone(), |thread, cx| {
+ anyhow::Ok(thread.terminal(args.terminal_id)?.read(cx).wait_for_exit())
+ })??
+ .await;
+
+ Ok(acp::WaitForTerminalExitResponse { exit_status })
+ }
+}
+
+impl ClientDelegate {
+ fn session_thread(&self, session_id: &acp::SessionId) -> Result<WeakEntity<AcpThread>> {
+ let sessions = self.sessions.borrow();
+ sessions
+ .get(session_id)
+ .context("Failed to get session")
+ .map(|session| session.thread.clone())
}
}
@@ -1,510 +0,0 @@
-// Translates old acp agents into the new schema
-use agent_client_protocol as acp;
-use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
-use anyhow::{Context as _, Result, anyhow};
-use futures::channel::oneshot;
-use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
-use project::Project;
-use std::{cell::RefCell, path::Path, rc::Rc};
-use ui::App;
-use util::ResultExt as _;
-
-use crate::AgentServerCommand;
-use acp_thread::{AcpThread, AgentConnection, AuthRequired};
-
-#[derive(Clone)]
-struct OldAcpClientDelegate {
- thread: Rc<RefCell<WeakEntity<AcpThread>>>,
- cx: AsyncApp,
- next_tool_call_id: Rc<RefCell<u64>>,
- // sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
-}
-
-impl OldAcpClientDelegate {
- fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
- Self {
- thread,
- cx,
- next_tool_call_id: Rc::new(RefCell::new(0)),
- }
- }
-}
-
-impl acp_old::Client for OldAcpClientDelegate {
- async fn stream_assistant_message_chunk(
- &self,
- params: acp_old::StreamAssistantMessageChunkParams,
- ) -> Result<(), acp_old::Error> {
- let cx = &mut self.cx.clone();
-
- cx.update(|cx| {
- self.thread
- .borrow()
- .update(cx, |thread, cx| match params.chunk {
- acp_old::AssistantMessageChunk::Text { text } => {
- thread.push_assistant_content_block(text.into(), false, cx)
- }
- acp_old::AssistantMessageChunk::Thought { thought } => {
- thread.push_assistant_content_block(thought.into(), true, cx)
- }
- })
- .log_err();
- })?;
-
- Ok(())
- }
-
- async fn request_tool_call_confirmation(
- &self,
- request: acp_old::RequestToolCallConfirmationParams,
- ) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
- let cx = &mut self.cx.clone();
-
- let old_acp_id = *self.next_tool_call_id.borrow() + 1;
- self.next_tool_call_id.replace(old_acp_id);
-
- let tool_call = into_new_tool_call(
- acp::ToolCallId(old_acp_id.to_string().into()),
- request.tool_call,
- );
-
- let mut options = match request.confirmation {
- acp_old::ToolCallConfirmation::Edit { .. } => vec![(
- acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
- acp::PermissionOptionKind::AllowAlways,
- "Always Allow Edits".to_string(),
- )],
- acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
- acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
- acp::PermissionOptionKind::AllowAlways,
- format!("Always Allow {}", root_command),
- )],
- acp_old::ToolCallConfirmation::Mcp {
- server_name,
- tool_name,
- ..
- } => vec![
- (
- acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
- acp::PermissionOptionKind::AllowAlways,
- format!("Always Allow {}", server_name),
- ),
- (
- acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
- acp::PermissionOptionKind::AllowAlways,
- format!("Always Allow {}", tool_name),
- ),
- ],
- acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
- acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
- acp::PermissionOptionKind::AllowAlways,
- "Always Allow".to_string(),
- )],
- acp_old::ToolCallConfirmation::Other { .. } => vec![(
- acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
- acp::PermissionOptionKind::AllowAlways,
- "Always Allow".to_string(),
- )],
- };
-
- options.extend([
- (
- acp_old::ToolCallConfirmationOutcome::Allow,
- acp::PermissionOptionKind::AllowOnce,
- "Allow".to_string(),
- ),
- (
- acp_old::ToolCallConfirmationOutcome::Reject,
- acp::PermissionOptionKind::RejectOnce,
- "Reject".to_string(),
- ),
- ]);
-
- let mut outcomes = Vec::with_capacity(options.len());
- let mut acp_options = Vec::with_capacity(options.len());
-
- for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
- outcomes.push(outcome);
- acp_options.push(acp::PermissionOption {
- id: acp::PermissionOptionId(index.to_string().into()),
- name: label,
- kind,
- })
- }
-
- let response = cx
- .update(|cx| {
- self.thread.borrow().update(cx, |thread, cx| {
- thread.request_tool_call_authorization(tool_call, acp_options, cx)
- })
- })?
- .context("Failed to update thread")?
- .await;
-
- let outcome = match response {
- Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
- Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
- };
-
- Ok(acp_old::RequestToolCallConfirmationResponse {
- id: acp_old::ToolCallId(old_acp_id),
- outcome: outcome,
- })
- }
-
- async fn push_tool_call(
- &self,
- request: acp_old::PushToolCallParams,
- ) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
- let cx = &mut self.cx.clone();
-
- let old_acp_id = *self.next_tool_call_id.borrow() + 1;
- self.next_tool_call_id.replace(old_acp_id);
-
- cx.update(|cx| {
- self.thread.borrow().update(cx, |thread, cx| {
- thread.upsert_tool_call(
- into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
- cx,
- )
- })
- })?
- .context("Failed to update thread")?;
-
- Ok(acp_old::PushToolCallResponse {
- id: acp_old::ToolCallId(old_acp_id),
- })
- }
-
- async fn update_tool_call(
- &self,
- request: acp_old::UpdateToolCallParams,
- ) -> Result<(), acp_old::Error> {
- let cx = &mut self.cx.clone();
-
- cx.update(|cx| {
- self.thread.borrow().update(cx, |thread, cx| {
- thread.update_tool_call(
- acp::ToolCallUpdate {
- id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
- fields: acp::ToolCallUpdateFields {
- status: Some(into_new_tool_call_status(request.status)),
- content: Some(
- request
- .content
- .into_iter()
- .map(into_new_tool_call_content)
- .collect::<Vec<_>>(),
- ),
- ..Default::default()
- },
- },
- cx,
- )
- })
- })?
- .context("Failed to update thread")??;
-
- Ok(())
- }
-
- async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
- let cx = &mut self.cx.clone();
-
- cx.update(|cx| {
- self.thread.borrow().update(cx, |thread, cx| {
- thread.update_plan(
- acp::Plan {
- entries: request
- .entries
- .into_iter()
- .map(into_new_plan_entry)
- .collect(),
- },
- cx,
- )
- })
- })?
- .context("Failed to update thread")?;
-
- Ok(())
- }
-
- async fn read_text_file(
- &self,
- acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
- ) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
- let content = self
- .cx
- .update(|cx| {
- self.thread.borrow().update(cx, |thread, cx| {
- thread.read_text_file(path, line, limit, false, cx)
- })
- })?
- .context("Failed to update thread")?
- .await?;
- Ok(acp_old::ReadTextFileResponse { content })
- }
-
- async fn write_text_file(
- &self,
- acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
- ) -> Result<(), acp_old::Error> {
- self.cx
- .update(|cx| {
- self.thread
- .borrow()
- .update(cx, |thread, cx| thread.write_text_file(path, content, cx))
- })?
- .context("Failed to update thread")?
- .await?;
-
- Ok(())
- }
-}
-
-fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
- acp::ToolCall {
- id: id,
- title: request.label,
- kind: acp_kind_from_old_icon(request.icon),
- status: acp::ToolCallStatus::InProgress,
- content: request
- .content
- .into_iter()
- .map(into_new_tool_call_content)
- .collect(),
- locations: request
- .locations
- .into_iter()
- .map(into_new_tool_call_location)
- .collect(),
- raw_input: None,
- raw_output: None,
- }
-}
-
-fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
- match icon {
- acp_old::Icon::FileSearch => acp::ToolKind::Search,
- acp_old::Icon::Folder => acp::ToolKind::Search,
- acp_old::Icon::Globe => acp::ToolKind::Search,
- acp_old::Icon::Hammer => acp::ToolKind::Other,
- acp_old::Icon::LightBulb => acp::ToolKind::Think,
- acp_old::Icon::Pencil => acp::ToolKind::Edit,
- acp_old::Icon::Regex => acp::ToolKind::Search,
- acp_old::Icon::Terminal => acp::ToolKind::Execute,
- }
-}
-
-fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
- match status {
- acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
- acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
- acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
- }
-}
-
-fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
- match content {
- acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
- acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
- diff: into_new_diff(diff),
- },
- }
-}
-
-fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
- acp::Diff {
- path: diff.path,
- old_text: diff.old_text,
- new_text: diff.new_text,
- }
-}
-
-fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
- acp::ToolCallLocation {
- path: location.path,
- line: location.line,
- }
-}
-
-fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
- acp::PlanEntry {
- content: entry.content,
- priority: into_new_plan_priority(entry.priority),
- status: into_new_plan_status(entry.status),
- }
-}
-
-fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
- match priority {
- acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
- acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
- acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
- }
-}
-
-fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
- match status {
- acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
- acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
- acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
- }
-}
-
-pub struct AcpConnection {
- pub name: &'static str,
- pub connection: acp_old::AgentConnection,
- pub _child_status: Task<Result<()>>,
- pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
-}
-
-impl AcpConnection {
- pub fn stdio(
- name: &'static str,
- command: AgentServerCommand,
- root_dir: &Path,
- cx: &mut AsyncApp,
- ) -> Task<Result<Self>> {
- let root_dir = root_dir.to_path_buf();
-
- cx.spawn(async move |cx| {
- let mut child = util::command::new_smol_command(&command.path)
- .args(command.args.iter())
- .current_dir(root_dir)
- .stdin(std::process::Stdio::piped())
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::inherit())
- .kill_on_drop(true)
- .spawn()?;
-
- let stdin = child.stdin.take().unwrap();
- let stdout = child.stdout.take().unwrap();
- log::trace!("Spawned (pid: {})", child.id());
-
- let foreground_executor = cx.foreground_executor().clone();
-
- let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
-
- let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
- OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
- stdin,
- stdout,
- move |fut| foreground_executor.spawn(fut).detach(),
- );
-
- let io_task = cx.background_spawn(async move {
- io_fut.await.log_err();
- });
-
- let child_status = cx.background_spawn(async move {
- let result = match child.status().await {
- Err(e) => Err(anyhow!(e)),
- Ok(result) if result.success() => Ok(()),
- Ok(result) => Err(anyhow!(result)),
- };
- drop(io_task);
- result
- });
-
- Ok(Self {
- name,
- connection,
- _child_status: child_status,
- current_thread: thread_rc,
- })
- })
- }
-}
-
-impl AgentConnection for AcpConnection {
- fn new_thread(
- self: Rc<Self>,
- project: Entity<Project>,
- _cwd: &Path,
- cx: &mut AsyncApp,
- ) -> Task<Result<Entity<AcpThread>>> {
- let task = self.connection.request_any(
- acp_old::InitializeParams {
- protocol_version: acp_old::ProtocolVersion::latest(),
- }
- .into_any(),
- );
- let current_thread = self.current_thread.clone();
- cx.spawn(async move |cx| {
- let result = task.await?;
- let result = acp_old::InitializeParams::response_from_any(result)?;
-
- if !result.is_authenticated {
- anyhow::bail!(AuthRequired)
- }
-
- cx.update(|cx| {
- let thread = cx.new(|cx| {
- let session_id = acp::SessionId("acp-old-no-id".into());
- AcpThread::new(self.name, self.clone(), project, session_id, cx)
- });
- current_thread.replace(thread.downgrade());
- thread
- })
- })
- }
-
- fn auth_methods(&self) -> &[acp::AuthMethod] {
- &[]
- }
-
- fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
- let task = self
- .connection
- .request_any(acp_old::AuthenticateParams.into_any());
- cx.foreground_executor().spawn(async move {
- task.await?;
- Ok(())
- })
- }
-
- fn prompt(
- &self,
- _id: Option<acp_thread::UserMessageId>,
- params: acp::PromptRequest,
- cx: &mut App,
- ) -> Task<Result<acp::PromptResponse>> {
- let chunks = params
- .prompt
- .into_iter()
- .filter_map(|block| match block {
- acp::ContentBlock::Text(text) => {
- Some(acp_old::UserMessageChunk::Text { text: text.text })
- }
- acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
- path: link.uri.into(),
- }),
- _ => None,
- })
- .collect();
-
- let task = self
- .connection
- .request_any(acp_old::SendUserMessageParams { chunks }.into_any());
- cx.foreground_executor().spawn(async move {
- task.await?;
- anyhow::Ok(acp::PromptResponse {
- stop_reason: acp::StopReason::EndTurn,
- })
- })
- }
-
- fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
- let task = self
- .connection
- .request_any(acp_old::CancelSendMessageParams.into_any());
- cx.foreground_executor()
- .spawn(async move {
- task.await?;
- anyhow::Ok(())
- })
- .detach_and_log_err(cx)
- }
-}
@@ -1,283 +0,0 @@
-use agent_client_protocol::{self as acp, Agent as _};
-use anyhow::anyhow;
-use collections::HashMap;
-use futures::channel::oneshot;
-use project::Project;
-use std::cell::RefCell;
-use std::path::Path;
-use std::rc::Rc;
-
-use anyhow::{Context as _, Result};
-use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
-
-use crate::{AgentServerCommand, acp::UnsupportedVersion};
-use acp_thread::{AcpThread, AgentConnection, AuthRequired};
-
-pub struct AcpConnection {
- server_name: &'static str,
- connection: Rc<acp::ClientSideConnection>,
- sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
- auth_methods: Vec<acp::AuthMethod>,
- _io_task: Task<Result<()>>,
-}
-
-pub struct AcpSession {
- thread: WeakEntity<AcpThread>,
-}
-
-const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
-
-impl AcpConnection {
- pub async fn stdio(
- server_name: &'static str,
- command: AgentServerCommand,
- root_dir: &Path,
- cx: &mut AsyncApp,
- ) -> Result<Self> {
- let mut child = util::command::new_smol_command(&command.path)
- .args(command.args.iter().map(|arg| arg.as_str()))
- .envs(command.env.iter().flatten())
- .current_dir(root_dir)
- .stdin(std::process::Stdio::piped())
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::inherit())
- .kill_on_drop(true)
- .spawn()?;
-
- let stdout = child.stdout.take().expect("Failed to take stdout");
- let stdin = child.stdin.take().expect("Failed to take stdin");
- log::trace!("Spawned (pid: {})", child.id());
-
- let sessions = Rc::new(RefCell::new(HashMap::default()));
-
- let client = ClientDelegate {
- sessions: sessions.clone(),
- cx: cx.clone(),
- };
- let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
- let foreground_executor = cx.foreground_executor().clone();
- move |fut| {
- foreground_executor.spawn(fut).detach();
- }
- });
-
- let io_task = cx.background_spawn(io_task);
-
- cx.spawn({
- let sessions = sessions.clone();
- async move |cx| {
- let status = child.status().await?;
-
- for session in sessions.borrow().values() {
- session
- .thread
- .update(cx, |thread, cx| thread.emit_server_exited(status, cx))
- .ok();
- }
-
- anyhow::Ok(())
- }
- })
- .detach();
-
- let response = connection
- .initialize(acp::InitializeRequest {
- protocol_version: acp::VERSION,
- client_capabilities: acp::ClientCapabilities {
- fs: acp::FileSystemCapability {
- read_text_file: true,
- write_text_file: true,
- },
- },
- })
- .await?;
-
- if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
- return Err(UnsupportedVersion.into());
- }
-
- Ok(Self {
- auth_methods: response.auth_methods,
- connection: connection.into(),
- server_name,
- sessions,
- _io_task: io_task,
- })
- }
-}
-
-impl AgentConnection for AcpConnection {
- fn new_thread(
- self: Rc<Self>,
- project: Entity<Project>,
- cwd: &Path,
- cx: &mut AsyncApp,
- ) -> Task<Result<Entity<AcpThread>>> {
- let conn = self.connection.clone();
- let sessions = self.sessions.clone();
- let cwd = cwd.to_path_buf();
- cx.spawn(async move |cx| {
- let response = conn
- .new_session(acp::NewSessionRequest {
- mcp_servers: vec![],
- cwd,
- })
- .await
- .map_err(|err| {
- if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
- anyhow!(AuthRequired)
- } else {
- anyhow!(err)
- }
- })?;
-
- let session_id = response.session_id;
-
- let thread = cx.new(|cx| {
- AcpThread::new(
- self.server_name,
- self.clone(),
- project,
- session_id.clone(),
- cx,
- )
- })?;
-
- let session = AcpSession {
- thread: thread.downgrade(),
- };
- sessions.borrow_mut().insert(session_id, session);
-
- Ok(thread)
- })
- }
-
- fn auth_methods(&self) -> &[acp::AuthMethod] {
- &self.auth_methods
- }
-
- fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
- let conn = self.connection.clone();
- cx.foreground_executor().spawn(async move {
- let result = conn
- .authenticate(acp::AuthenticateRequest {
- method_id: method_id.clone(),
- })
- .await?;
-
- Ok(result)
- })
- }
-
- fn prompt(
- &self,
- _id: Option<acp_thread::UserMessageId>,
- params: acp::PromptRequest,
- cx: &mut App,
- ) -> Task<Result<acp::PromptResponse>> {
- let conn = self.connection.clone();
- cx.foreground_executor().spawn(async move {
- let response = conn.prompt(params).await?;
- Ok(response)
- })
- }
-
- fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
- let conn = self.connection.clone();
- let params = acp::CancelNotification {
- session_id: session_id.clone(),
- };
- cx.foreground_executor()
- .spawn(async move { conn.cancel(params).await })
- .detach();
- }
-}
-
-struct ClientDelegate {
- sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
- cx: AsyncApp,
-}
-
-impl acp::Client for ClientDelegate {
- async fn request_permission(
- &self,
- arguments: acp::RequestPermissionRequest,
- ) -> Result<acp::RequestPermissionResponse, acp::Error> {
- let cx = &mut self.cx.clone();
- let rx = self
- .sessions
- .borrow()
- .get(&arguments.session_id)
- .context("Failed to get session")?
- .thread
- .update(cx, |thread, cx| {
- thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
- })?;
-
- let result = rx.await;
-
- let outcome = match result {
- Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
- Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
- };
-
- Ok(acp::RequestPermissionResponse { outcome })
- }
-
- async fn write_text_file(
- &self,
- arguments: acp::WriteTextFileRequest,
- ) -> Result<(), acp::Error> {
- let cx = &mut self.cx.clone();
- let task = self
- .sessions
- .borrow()
- .get(&arguments.session_id)
- .context("Failed to get session")?
- .thread
- .update(cx, |thread, cx| {
- thread.write_text_file(arguments.path, arguments.content, cx)
- })?;
-
- task.await?;
-
- Ok(())
- }
-
- async fn read_text_file(
- &self,
- arguments: acp::ReadTextFileRequest,
- ) -> Result<acp::ReadTextFileResponse, acp::Error> {
- let cx = &mut self.cx.clone();
- let task = self
- .sessions
- .borrow()
- .get(&arguments.session_id)
- .context("Failed to get session")?
- .thread
- .update(cx, |thread, cx| {
- thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
- })?;
-
- let content = task.await?;
-
- Ok(acp::ReadTextFileResponse { content })
- }
-
- async fn session_notification(
- &self,
- notification: acp::SessionNotification,
- ) -> Result<(), acp::Error> {
- let cx = &mut self.cx.clone();
- let sessions = self.sessions.borrow();
- let session = sessions
- .get(¬ification.session_id)
- .context("Failed to get session")?;
-
- session.thread.update(cx, |thread, cx| {
- thread.handle_session_update(notification.update, cx)
- })??;
-
- Ok(())
- }
-}
@@ -1,176 +1,79 @@
mod acp;
mod claude;
+mod custom;
mod gemini;
-mod settings;
-#[cfg(test)]
-mod e2e_tests;
+#[cfg(any(test, feature = "test-support"))]
+pub mod e2e_tests;
pub use claude::*;
+pub use custom::*;
+use fs::Fs;
pub use gemini::*;
-pub use settings::*;
+use project::agent_server_store::AgentServerStore;
use acp_thread::AgentConnection;
use anyhow::Result;
-use collections::HashMap;
-use gpui::{App, AsyncApp, Entity, SharedString, Task};
+use gpui::{App, Entity, SharedString, Task};
use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use std::{
- path::{Path, PathBuf},
- rc::Rc,
- sync::Arc,
-};
-use util::ResultExt as _;
+use std::{any::Any, path::Path, rc::Rc, sync::Arc};
-pub fn init(cx: &mut App) {
- settings::init(cx);
+pub use acp::AcpConnection;
+
+pub struct AgentServerDelegate {
+ store: Entity<AgentServerStore>,
+ project: Entity<Project>,
+ status_tx: Option<watch::Sender<SharedString>>,
+ new_version_available: Option<watch::Sender<Option<String>>>,
+}
+
+impl AgentServerDelegate {
+ pub fn new(
+ store: Entity<AgentServerStore>,
+ project: Entity<Project>,
+ status_tx: Option<watch::Sender<SharedString>>,
+ new_version_tx: Option<watch::Sender<Option<String>>>,
+ ) -> Self {
+ Self {
+ store,
+ project,
+ status_tx,
+ new_version_available: new_version_tx,
+ }
+ }
+
+ pub fn project(&self) -> &Entity<Project> {
+ &self.project
+ }
}
pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName;
- fn name(&self) -> &'static str;
- fn empty_state_headline(&self) -> &'static str;
- fn empty_state_message(&self) -> &'static str;
+ fn name(&self) -> SharedString;
+ fn telemetry_id(&self) -> &'static str;
+ fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
+ None
+ }
+ fn set_default_mode(
+ &self,
+ _mode_id: Option<agent_client_protocol::SessionModeId>,
+ _fs: Arc<dyn Fs>,
+ _cx: &mut App,
+ ) {
+ }
fn connect(
&self,
- root_dir: &Path,
- project: &Entity<Project>,
+ root_dir: Option<&Path>,
+ delegate: AgentServerDelegate,
cx: &mut App,
- ) -> Task<Result<Rc<dyn AgentConnection>>>;
-}
-
-impl std::fmt::Debug for AgentServerCommand {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let filtered_env = self.env.as_ref().map(|env| {
- env.iter()
- .map(|(k, v)| {
- (
- k,
- if util::redact::should_redact(k) {
- "[REDACTED]"
- } else {
- v
- },
- )
- })
- .collect::<Vec<_>>()
- });
+ ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
- f.debug_struct("AgentServerCommand")
- .field("path", &self.path)
- .field("args", &self.args)
- .field("env", &filtered_env)
- .finish()
- }
-}
-
-pub enum AgentServerVersion {
- Supported,
- Unsupported {
- error_message: SharedString,
- upgrade_message: SharedString,
- upgrade_command: String,
- },
+ fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
}
-#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
-pub struct AgentServerCommand {
- #[serde(rename = "command")]
- pub path: PathBuf,
- #[serde(default)]
- pub args: Vec<String>,
- pub env: Option<HashMap<String, String>>,
-}
-
-impl AgentServerCommand {
- pub(crate) async fn resolve(
- path_bin_name: &'static str,
- extra_args: &[&'static str],
- fallback_path: Option<&Path>,
- settings: Option<AgentServerSettings>,
- project: &Entity<Project>,
- cx: &mut AsyncApp,
- ) -> Option<Self> {
- if let Some(agent_settings) = settings {
- return Some(Self {
- path: agent_settings.command.path,
- args: agent_settings
- .command
- .args
- .into_iter()
- .chain(extra_args.iter().map(|arg| arg.to_string()))
- .collect(),
- env: agent_settings.command.env,
- });
- } else {
- match find_bin_in_path(path_bin_name, project, cx).await {
- Some(path) => Some(Self {
- path,
- args: extra_args.iter().map(|arg| arg.to_string()).collect(),
- env: None,
- }),
- None => fallback_path.and_then(|path| {
- if path.exists() {
- Some(Self {
- path: path.to_path_buf(),
- args: extra_args.iter().map(|arg| arg.to_string()).collect(),
- env: None,
- })
- } else {
- None
- }
- }),
- }
- }
+impl dyn AgentServer {
+ pub fn downcast<T: 'static + AgentServer + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
+ self.into_any().downcast().ok()
}
}
-
-async fn find_bin_in_path(
- bin_name: &'static str,
- project: &Entity<Project>,
- cx: &mut AsyncApp,
-) -> Option<PathBuf> {
- let (env_task, root_dir) = project
- .update(cx, |project, cx| {
- let worktree = project.visible_worktrees(cx).next();
- match worktree {
- Some(worktree) => {
- let env_task = project.environment().update(cx, |env, cx| {
- env.get_worktree_environment(worktree.clone(), cx)
- });
-
- let path = worktree.read(cx).abs_path();
- (env_task, path)
- }
- None => {
- let path: Arc<Path> = paths::home_dir().as_path().into();
- let env_task = project.environment().update(cx, |env, cx| {
- env.get_directory_environment(path.clone(), cx)
- });
- (env_task, path)
- }
- }
- })
- .log_err()?;
-
- cx.background_executor()
- .spawn(async move {
- let which_result = if cfg!(windows) {
- which::which(bin_name)
- } else {
- let env = env_task.await.unwrap_or_default();
- let shell_path = env.get("PATH").cloned();
- which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
- };
-
- if let Err(which::Error::CannotFindBinaryPath) = which_result {
- return None;
- }
-
- which_result.log_err()
- })
- .await
-}
@@ -1,1066 +1,96 @@
-mod mcp_server;
-pub mod tools;
-
-use collections::HashMap;
-use context_server::listener::McpServerTool;
-use project::Project;
-use settings::SettingsStore;
-use smol::process::Child;
-use std::cell::RefCell;
-use std::fmt::Display;
+use agent_client_protocol as acp;
+use fs::Fs;
+use settings::{SettingsStore, update_settings_file};
use std::path::Path;
use std::rc::Rc;
-use uuid::Uuid;
+use std::sync::Arc;
+use std::{any::Any, path::PathBuf};
-use agent_client_protocol as acp;
-use anyhow::{Result, anyhow};
-use futures::channel::oneshot;
-use futures::{AsyncBufReadExt, AsyncWriteExt};
-use futures::{
- AsyncRead, AsyncWrite, FutureExt, StreamExt,
- channel::mpsc::{self, UnboundedReceiver, UnboundedSender},
- io::BufReader,
- select_biased,
-};
-use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
-use serde::{Deserialize, Serialize};
-use util::{ResultExt, debug_panic};
+use anyhow::{Context as _, Result};
+use gpui::{App, AppContext as _, SharedString, Task};
+use project::agent_server_store::{AllAgentServersSettings, CLAUDE_CODE_NAME};
-use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
-use crate::claude::tools::ClaudeTool;
-use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
-use acp_thread::{AcpThread, AgentConnection};
+use crate::{AgentServer, AgentServerDelegate};
+use acp_thread::AgentConnection;
#[derive(Clone)]
pub struct ClaudeCode;
-impl AgentServer for ClaudeCode {
- fn name(&self) -> &'static str {
- "Claude Code"
- }
+pub struct AgentServerLoginCommand {
+ pub path: PathBuf,
+ pub arguments: Vec<String>,
+}
- fn empty_state_headline(&self) -> &'static str {
- self.name()
+impl AgentServer for ClaudeCode {
+ fn telemetry_id(&self) -> &'static str {
+ "claude-code"
}
- fn empty_state_message(&self) -> &'static str {
- "How can I help you today?"
+ fn name(&self) -> SharedString {
+ "Claude Code".into()
}
fn logo(&self) -> ui::IconName {
ui::IconName::AiClaude
}
- fn connect(
- &self,
- _root_dir: &Path,
- _project: &Entity<Project>,
- _cx: &mut App,
- ) -> Task<Result<Rc<dyn AgentConnection>>> {
- let connection = ClaudeAgentConnection {
- sessions: Default::default(),
- };
-
- Task::ready(Ok(Rc::new(connection) as _))
- }
-}
-
-struct ClaudeAgentConnection {
- sessions: Rc<RefCell<HashMap<acp::SessionId, ClaudeAgentSession>>>,
-}
-
-impl AgentConnection for ClaudeAgentConnection {
- fn new_thread(
- self: Rc<Self>,
- project: Entity<Project>,
- cwd: &Path,
- cx: &mut AsyncApp,
- ) -> Task<Result<Entity<AcpThread>>> {
- let cwd = cwd.to_owned();
- cx.spawn(async move |cx| {
- let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
- let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?;
-
- let mut mcp_servers = HashMap::default();
- mcp_servers.insert(
- mcp_server::SERVER_NAME.to_string(),
- permission_mcp_server.server_config()?,
- );
- let mcp_config = McpConfig { mcp_servers };
-
- let mcp_config_file = tempfile::NamedTempFile::new()?;
- let (mcp_config_file, mcp_config_path) = mcp_config_file.into_parts();
-
- let mut mcp_config_file = smol::fs::File::from(mcp_config_file);
- mcp_config_file
- .write_all(serde_json::to_string(&mcp_config)?.as_bytes())
- .await?;
- mcp_config_file.flush().await?;
-
- let settings = cx.read_global(|settings: &SettingsStore, _| {
- settings.get::<AllAgentServersSettings>(None).claude.clone()
- })?;
-
- let Some(command) = AgentServerCommand::resolve(
- "claude",
- &[],
- Some(&util::paths::home_dir().join(".claude/local/claude")),
- settings,
- &project,
- cx,
- )
- .await
- else {
- anyhow::bail!("Failed to find claude binary");
- };
-
- let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded();
- let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
-
- let session_id = acp::SessionId(Uuid::new_v4().to_string().into());
-
- log::trace!("Starting session with id: {}", session_id);
-
- let mut child = spawn_claude(
- &command,
- ClaudeSessionMode::Start,
- session_id.clone(),
- &mcp_config_path,
- &cwd,
- )?;
-
- let stdin = child.stdin.take().unwrap();
- let stdout = child.stdout.take().unwrap();
-
- let pid = child.id();
- log::trace!("Spawned (pid: {})", pid);
-
- cx.background_spawn(async move {
- let mut outgoing_rx = Some(outgoing_rx);
-
- ClaudeAgentSession::handle_io(
- outgoing_rx.take().unwrap(),
- incoming_message_tx.clone(),
- stdin,
- stdout,
- )
- .await?;
-
- log::trace!("Stopped (pid: {})", pid);
-
- drop(mcp_config_path);
- anyhow::Ok(())
- })
- .detach();
-
- let turn_state = Rc::new(RefCell::new(TurnState::None));
-
- let handler_task = cx.spawn({
- let turn_state = turn_state.clone();
- let mut thread_rx = thread_rx.clone();
- async move |cx| {
- while let Some(message) = incoming_message_rx.next().await {
- ClaudeAgentSession::handle_message(
- thread_rx.clone(),
- message,
- turn_state.clone(),
- cx,
- )
- .await
- }
-
- if let Some(status) = child.status().await.log_err() {
- if let Some(thread) = thread_rx.recv().await.ok() {
- thread
- .update(cx, |thread, cx| {
- thread.emit_server_exited(status, cx);
- })
- .ok();
- }
- }
- }
- });
-
- let thread = cx.new(|cx| {
- AcpThread::new("Claude Code", self.clone(), project, session_id.clone(), cx)
- })?;
-
- thread_tx.send(thread.downgrade())?;
-
- let session = ClaudeAgentSession {
- outgoing_tx,
- turn_state,
- _handler_task: handler_task,
- _mcp_server: Some(permission_mcp_server),
- };
-
- self.sessions.borrow_mut().insert(session_id, session);
-
- Ok(thread)
- })
- }
+ fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
+ let settings = cx.read_global(|settings: &SettingsStore, _| {
+ settings.get::<AllAgentServersSettings>(None).claude.clone()
+ });
- fn auth_methods(&self) -> &[acp::AuthMethod] {
- &[]
+ settings
+ .as_ref()
+ .and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
- fn authenticate(&self, _: acp::AuthMethodId, _cx: &mut App) -> Task<Result<()>> {
- Task::ready(Err(anyhow!("Authentication not supported")))
+ fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
+ update_settings_file::<AllAgentServersSettings>(fs, cx, |settings, _| {
+ settings.claude.get_or_insert_default().default_mode = mode_id.map(|m| m.to_string())
+ });
}
- fn prompt(
+ fn connect(
&self,
- _id: Option<acp_thread::UserMessageId>,
- params: acp::PromptRequest,
+ root_dir: Option<&Path>,
+ delegate: AgentServerDelegate,
cx: &mut App,
- ) -> Task<Result<acp::PromptResponse>> {
- let sessions = self.sessions.borrow();
- let Some(session) = sessions.get(¶ms.session_id) else {
- return Task::ready(Err(anyhow!(
- "Attempted to send message to nonexistent session {}",
- params.session_id
- )));
- };
-
- let (end_tx, end_rx) = oneshot::channel();
- session.turn_state.replace(TurnState::InProgress { end_tx });
-
- let mut content = String::new();
- for chunk in params.prompt {
- match chunk {
- acp::ContentBlock::Text(text_content) => {
- content.push_str(&text_content.text);
- }
- acp::ContentBlock::ResourceLink(resource_link) => {
- content.push_str(&format!("@{}", resource_link.uri));
- }
- acp::ContentBlock::Audio(_)
- | acp::ContentBlock::Image(_)
- | acp::ContentBlock::Resource(_) => {
- // TODO
- }
- }
- }
-
- if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User {
- message: Message {
- role: Role::User,
- content: Content::UntaggedText(content),
- id: None,
- model: None,
- stop_reason: None,
- stop_sequence: None,
- usage: None,
- },
- session_id: Some(params.session_id.to_string()),
- }) {
- return Task::ready(Err(anyhow!(err)));
- }
-
- cx.foreground_executor().spawn(async move { end_rx.await? })
- }
-
- fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
- let sessions = self.sessions.borrow();
- let Some(session) = sessions.get(&session_id) else {
- log::warn!("Attempted to cancel nonexistent session {}", session_id);
- return;
- };
-
- let request_id = new_request_id();
-
- let turn_state = session.turn_state.take();
- let TurnState::InProgress { end_tx } = turn_state else {
- // Already cancelled or idle, put it back
- session.turn_state.replace(turn_state);
- return;
- };
-
- session.turn_state.replace(TurnState::CancelRequested {
- end_tx,
- request_id: request_id.clone(),
- });
-
- session
- .outgoing_tx
- .unbounded_send(SdkMessage::ControlRequest {
- request_id,
- request: ControlRequest::Interrupt,
- })
- .log_err();
- }
-}
-
-#[derive(Clone, Copy)]
-enum ClaudeSessionMode {
- Start,
- #[expect(dead_code)]
- Resume,
-}
+ ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
+ let name = self.name();
+ let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
+ let is_remote = delegate.project.read(cx).is_via_remote_server();
+ let store = delegate.store.downgrade();
+ let default_mode = self.default_mode(cx);
-fn spawn_claude(
- command: &AgentServerCommand,
- mode: ClaudeSessionMode,
- session_id: acp::SessionId,
- mcp_config_path: &Path,
- root_dir: &Path,
-) -> Result<Child> {
- let child = util::command::new_smol_command(&command.path)
- .args([
- "--input-format",
- "stream-json",
- "--output-format",
- "stream-json",
- "--print",
- "--verbose",
- "--mcp-config",
- mcp_config_path.to_string_lossy().as_ref(),
- "--permission-prompt-tool",
- &format!(
- "mcp__{}__{}",
- mcp_server::SERVER_NAME,
- mcp_server::PermissionTool::NAME,
- ),
- "--allowedTools",
- &format!(
- "mcp__{}__{},mcp__{}__{}",
- mcp_server::SERVER_NAME,
- mcp_server::EditTool::NAME,
- mcp_server::SERVER_NAME,
- mcp_server::ReadTool::NAME
- ),
- "--disallowedTools",
- "Read,Edit",
- ])
- .args(match mode {
- ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()],
- ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()],
+ cx.spawn(async move |cx| {
+ let (command, root_dir, login) = store
+ .update(cx, |store, cx| {
+ let agent = store
+ .get_external_agent(&CLAUDE_CODE_NAME.into())
+ .context("Claude Code is not registered")?;
+ anyhow::Ok(agent.get_command(
+ root_dir.as_deref(),
+ Default::default(),
+ delegate.status_tx,
+ delegate.new_version_available,
+ &mut cx.to_async(),
+ ))
+ })??
+ .await?;
+ let connection = crate::acp::connect(
+ name,
+ command,
+ root_dir.as_ref(),
+ default_mode,
+ is_remote,
+ cx,
+ )
+ .await?;
+ Ok((connection, login))
})
- .args(command.args.iter().map(|arg| arg.as_str()))
- .current_dir(root_dir)
- .stdin(std::process::Stdio::piped())
- .stdout(std::process::Stdio::piped())
- .stderr(std::process::Stdio::inherit())
- .kill_on_drop(true)
- .spawn()?;
-
- Ok(child)
-}
-
-struct ClaudeAgentSession {
- outgoing_tx: UnboundedSender<SdkMessage>,
- turn_state: Rc<RefCell<TurnState>>,
- _mcp_server: Option<ClaudeZedMcpServer>,
- _handler_task: Task<()>,
-}
-
-#[derive(Debug, Default)]
-enum TurnState {
- #[default]
- None,
- InProgress {
- end_tx: oneshot::Sender<Result<acp::PromptResponse>>,
- },
- CancelRequested {
- end_tx: oneshot::Sender<Result<acp::PromptResponse>>,
- request_id: String,
- },
- CancelConfirmed {
- end_tx: oneshot::Sender<Result<acp::PromptResponse>>,
- },
-}
-
-impl TurnState {
- fn is_cancelled(&self) -> bool {
- matches!(self, TurnState::CancelConfirmed { .. })
- }
-
- fn end_tx(self) -> Option<oneshot::Sender<Result<acp::PromptResponse>>> {
- match self {
- TurnState::None => None,
- TurnState::InProgress { end_tx, .. } => Some(end_tx),
- TurnState::CancelRequested { end_tx, .. } => Some(end_tx),
- TurnState::CancelConfirmed { end_tx } => Some(end_tx),
- }
}
- fn confirm_cancellation(self, id: &str) -> Self {
- match self {
- TurnState::CancelRequested { request_id, end_tx } if request_id == id => {
- TurnState::CancelConfirmed { end_tx }
- }
- _ => self,
- }
- }
-}
-
-impl ClaudeAgentSession {
- async fn handle_message(
- mut thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
- message: SdkMessage,
- turn_state: Rc<RefCell<TurnState>>,
- cx: &mut AsyncApp,
- ) {
- match message {
- // we should only be sending these out, they don't need to be in the thread
- SdkMessage::ControlRequest { .. } => {}
- SdkMessage::User {
- message,
- session_id: _,
- } => {
- let Some(thread) = thread_rx
- .recv()
- .await
- .log_err()
- .and_then(|entity| entity.upgrade())
- else {
- log::error!("Received an SDK message but thread is gone");
- return;
- };
-
- for chunk in message.content.chunks() {
- match chunk {
- ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
- if !turn_state.borrow().is_cancelled() {
- thread
- .update(cx, |thread, cx| {
- thread.push_user_content_block(None, text.into(), cx)
- })
- .log_err();
- }
- }
- ContentChunk::ToolResult {
- content,
- tool_use_id,
- } => {
- let content = content.to_string();
- thread
- .update(cx, |thread, cx| {
- thread.update_tool_call(
- acp::ToolCallUpdate {
- id: acp::ToolCallId(tool_use_id.into()),
- fields: acp::ToolCallUpdateFields {
- status: if turn_state.borrow().is_cancelled() {
- // Do not set to completed if turn was cancelled
- None
- } else {
- Some(acp::ToolCallStatus::Completed)
- },
- content: (!content.is_empty())
- .then(|| vec![content.into()]),
- ..Default::default()
- },
- },
- cx,
- )
- })
- .log_err();
- }
- ContentChunk::Thinking { .. }
- | ContentChunk::RedactedThinking
- | ContentChunk::ToolUse { .. } => {
- debug_panic!(
- "Should not get {:?} with role: assistant. should we handle this?",
- chunk
- );
- }
-
- ContentChunk::Image
- | ContentChunk::Document
- | ContentChunk::WebSearchToolResult => {
- thread
- .update(cx, |thread, cx| {
- thread.push_assistant_content_block(
- format!("Unsupported content: {:?}", chunk).into(),
- false,
- cx,
- )
- })
- .log_err();
- }
- }
- }
- }
- SdkMessage::Assistant {
- message,
- session_id: _,
- } => {
- let Some(thread) = thread_rx
- .recv()
- .await
- .log_err()
- .and_then(|entity| entity.upgrade())
- else {
- log::error!("Received an SDK message but thread is gone");
- return;
- };
-
- for chunk in message.content.chunks() {
- match chunk {
- ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
- thread
- .update(cx, |thread, cx| {
- thread.push_assistant_content_block(text.into(), false, cx)
- })
- .log_err();
- }
- ContentChunk::Thinking { thinking } => {
- thread
- .update(cx, |thread, cx| {
- thread.push_assistant_content_block(thinking.into(), true, cx)
- })
- .log_err();
- }
- ContentChunk::RedactedThinking => {
- thread
- .update(cx, |thread, cx| {
- thread.push_assistant_content_block(
- "[REDACTED]".into(),
- true,
- cx,
- )
- })
- .log_err();
- }
- ContentChunk::ToolUse { id, name, input } => {
- let claude_tool = ClaudeTool::infer(&name, input);
-
- thread
- .update(cx, |thread, cx| {
- if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
- thread.update_plan(
- acp::Plan {
- entries: params
- .todos
- .into_iter()
- .map(Into::into)
- .collect(),
- },
- cx,
- )
- } else {
- thread.upsert_tool_call(
- claude_tool.as_acp(acp::ToolCallId(id.into())),
- cx,
- );
- }
- })
- .log_err();
- }
- ContentChunk::ToolResult { .. } | ContentChunk::WebSearchToolResult => {
- debug_panic!(
- "Should not get tool results with role: assistant. should we handle this?"
- );
- }
- ContentChunk::Image | ContentChunk::Document => {
- thread
- .update(cx, |thread, cx| {
- thread.push_assistant_content_block(
- format!("Unsupported content: {:?}", chunk).into(),
- false,
- cx,
- )
- })
- .log_err();
- }
- }
- }
- }
- SdkMessage::Result {
- is_error,
- subtype,
- result,
- ..
- } => {
- let turn_state = turn_state.take();
- let was_cancelled = turn_state.is_cancelled();
- let Some(end_turn_tx) = turn_state.end_tx() else {
- debug_panic!("Received `SdkMessage::Result` but there wasn't an active turn");
- return;
- };
-
- if is_error || (!was_cancelled && subtype == ResultErrorType::ErrorDuringExecution)
- {
- end_turn_tx
- .send(Err(anyhow!(
- "Error: {}",
- result.unwrap_or_else(|| subtype.to_string())
- )))
- .ok();
- } else {
- let stop_reason = match subtype {
- ResultErrorType::Success => acp::StopReason::EndTurn,
- ResultErrorType::ErrorMaxTurns => acp::StopReason::MaxTurnRequests,
- ResultErrorType::ErrorDuringExecution => acp::StopReason::Cancelled,
- };
- end_turn_tx
- .send(Ok(acp::PromptResponse { stop_reason }))
- .ok();
- }
- }
- SdkMessage::ControlResponse { response } => {
- if matches!(response.subtype, ResultErrorType::Success) {
- let new_state = turn_state.take().confirm_cancellation(&response.request_id);
- turn_state.replace(new_state);
- } else {
- log::error!("Control response error: {:?}", response);
- }
- }
- SdkMessage::System { .. } => {}
- }
- }
-
- async fn handle_io(
- mut outgoing_rx: UnboundedReceiver<SdkMessage>,
- incoming_tx: UnboundedSender<SdkMessage>,
- mut outgoing_bytes: impl Unpin + AsyncWrite,
- incoming_bytes: impl Unpin + AsyncRead,
- ) -> Result<UnboundedReceiver<SdkMessage>> {
- let mut output_reader = BufReader::new(incoming_bytes);
- let mut outgoing_line = Vec::new();
- let mut incoming_line = String::new();
- loop {
- select_biased! {
- message = outgoing_rx.next() => {
- if let Some(message) = message {
- outgoing_line.clear();
- serde_json::to_writer(&mut outgoing_line, &message)?;
- log::trace!("send: {}", String::from_utf8_lossy(&outgoing_line));
- outgoing_line.push(b'\n');
- outgoing_bytes.write_all(&outgoing_line).await.ok();
- } else {
- break;
- }
- }
- bytes_read = output_reader.read_line(&mut incoming_line).fuse() => {
- if bytes_read? == 0 {
- break
- }
- log::trace!("recv: {}", &incoming_line);
- match serde_json::from_str::<SdkMessage>(&incoming_line) {
- Ok(message) => {
- incoming_tx.unbounded_send(message).log_err();
- }
- Err(error) => {
- log::error!("failed to parse incoming message: {error}. Raw: {incoming_line}");
- }
- }
- incoming_line.clear();
- }
- }
- }
-
- Ok(outgoing_rx)
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct Message {
- role: Role,
- content: Content,
- #[serde(skip_serializing_if = "Option::is_none")]
- id: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- model: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- stop_reason: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- stop_sequence: Option<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- usage: Option<Usage>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(untagged)]
-enum Content {
- UntaggedText(String),
- Chunks(Vec<ContentChunk>),
-}
-
-impl Content {
- pub fn chunks(self) -> impl Iterator<Item = ContentChunk> {
- match self {
- Self::Chunks(chunks) => chunks.into_iter(),
- Self::UntaggedText(text) => vec![ContentChunk::Text { text: text.clone() }].into_iter(),
- }
- }
-}
-
-impl Display for Content {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Content::UntaggedText(txt) => write!(f, "{}", txt),
- Content::Chunks(chunks) => {
- for chunk in chunks {
- write!(f, "{}", chunk)?;
- }
- Ok(())
- }
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(tag = "type", rename_all = "snake_case")]
-enum ContentChunk {
- Text {
- text: String,
- },
- ToolUse {
- id: String,
- name: String,
- input: serde_json::Value,
- },
- ToolResult {
- content: Content,
- tool_use_id: String,
- },
- Thinking {
- thinking: String,
- },
- RedactedThinking,
- // TODO
- Image,
- Document,
- WebSearchToolResult,
- #[serde(untagged)]
- UntaggedText(String),
-}
-
-impl Display for ContentChunk {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- ContentChunk::Text { text } => write!(f, "{}", text),
- ContentChunk::Thinking { thinking } => write!(f, "Thinking: {}", thinking),
- ContentChunk::RedactedThinking => write!(f, "Thinking: [REDACTED]"),
- ContentChunk::UntaggedText(text) => write!(f, "{}", text),
- ContentChunk::ToolResult { content, .. } => write!(f, "{}", content),
- ContentChunk::Image
- | ContentChunk::Document
- | ContentChunk::ToolUse { .. }
- | ContentChunk::WebSearchToolResult => {
- write!(f, "\n{:?}\n", &self)
- }
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct Usage {
- input_tokens: u32,
- cache_creation_input_tokens: u32,
- cache_read_input_tokens: u32,
- output_tokens: u32,
- service_tier: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
-enum Role {
- System,
- Assistant,
- User,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct MessageParam {
- role: Role,
- content: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(tag = "type", rename_all = "snake_case")]
-enum SdkMessage {
- // An assistant message
- Assistant {
- message: Message, // from Anthropic SDK
- #[serde(skip_serializing_if = "Option::is_none")]
- session_id: Option<String>,
- },
- // A user message
- User {
- message: Message, // from Anthropic SDK
- #[serde(skip_serializing_if = "Option::is_none")]
- session_id: Option<String>,
- },
- // Emitted as the last message in a conversation
- Result {
- subtype: ResultErrorType,
- duration_ms: f64,
- duration_api_ms: f64,
- is_error: bool,
- num_turns: i32,
- #[serde(skip_serializing_if = "Option::is_none")]
- result: Option<String>,
- session_id: String,
- total_cost_usd: f64,
- },
- // Emitted as the first message at the start of a conversation
- System {
- cwd: String,
- session_id: String,
- tools: Vec<String>,
- model: String,
- mcp_servers: Vec<McpServer>,
- #[serde(rename = "apiKeySource")]
- api_key_source: String,
- #[serde(rename = "permissionMode")]
- permission_mode: PermissionMode,
- },
- /// Messages used to control the conversation, outside of chat messages to the model
- ControlRequest {
- request_id: String,
- request: ControlRequest,
- },
- /// Response to a control request
- ControlResponse { response: ControlResponse },
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(tag = "subtype", rename_all = "snake_case")]
-enum ControlRequest {
- /// Cancel the current conversation
- Interrupt,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct ControlResponse {
- request_id: String,
- subtype: ResultErrorType,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
-#[serde(rename_all = "snake_case")]
-enum ResultErrorType {
- Success,
- ErrorMaxTurns,
- ErrorDuringExecution,
-}
-
-impl Display for ResultErrorType {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- ResultErrorType::Success => write!(f, "success"),
- ResultErrorType::ErrorMaxTurns => write!(f, "error_max_turns"),
- ResultErrorType::ErrorDuringExecution => write!(f, "error_during_execution"),
- }
- }
-}
-
-fn new_request_id() -> String {
- use rand::Rng;
- // In the Claude Code TS SDK they just generate a random 12 character string,
- // `Math.random().toString(36).substring(2, 15)`
- rand::thread_rng()
- .sample_iter(&rand::distributions::Alphanumeric)
- .take(12)
- .map(char::from)
- .collect()
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-struct McpServer {
- name: String,
- status: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum PermissionMode {
- Default,
- AcceptEdits,
- BypassPermissions,
- Plan,
-}
-
-#[cfg(test)]
-pub(crate) mod tests {
- use super::*;
- use crate::e2e_tests;
- use gpui::TestAppContext;
- use serde_json::json;
-
- crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow");
-
- pub fn local_command() -> AgentServerCommand {
- AgentServerCommand {
- path: "claude".into(),
- args: vec![],
- env: None,
- }
- }
-
- #[gpui::test]
- #[cfg_attr(not(feature = "e2e"), ignore)]
- async fn test_todo_plan(cx: &mut TestAppContext) {
- let fs = e2e_tests::init_test(cx).await;
- let project = Project::test(fs, [], cx).await;
- let thread =
- e2e_tests::new_test_thread(ClaudeCode, project.clone(), "/private/tmp", cx).await;
-
- thread
- .update(cx, |thread, cx| {
- thread.send_raw(
- "Create a todo plan for initializing a new React app. I'll follow it myself, do not execute on it.",
- cx,
- )
- })
- .await
- .unwrap();
-
- let mut entries_len = 0;
-
- thread.read_with(cx, |thread, _| {
- entries_len = thread.plan().entries.len();
- assert!(thread.plan().entries.len() > 0, "Empty plan");
- });
-
- thread
- .update(cx, |thread, cx| {
- thread.send_raw(
- "Mark the first entry status as in progress without acting on it.",
- cx,
- )
- })
- .await
- .unwrap();
-
- thread.read_with(cx, |thread, _| {
- assert!(matches!(
- thread.plan().entries[0].status,
- acp::PlanEntryStatus::InProgress
- ));
- assert_eq!(thread.plan().entries.len(), entries_len);
- });
-
- thread
- .update(cx, |thread, cx| {
- thread.send_raw(
- "Now mark the first entry as completed without acting on it.",
- cx,
- )
- })
- .await
- .unwrap();
-
- thread.read_with(cx, |thread, _| {
- assert!(matches!(
- thread.plan().entries[0].status,
- acp::PlanEntryStatus::Completed
- ));
- assert_eq!(thread.plan().entries.len(), entries_len);
- });
- }
-
- #[test]
- fn test_deserialize_content_untagged_text() {
- let json = json!("Hello, world!");
- let content: Content = serde_json::from_value(json).unwrap();
- match content {
- Content::UntaggedText(text) => assert_eq!(text, "Hello, world!"),
- _ => panic!("Expected UntaggedText variant"),
- }
- }
-
- #[test]
- fn test_deserialize_content_chunks() {
- let json = json!([
- {
- "type": "text",
- "text": "Hello"
- },
- {
- "type": "tool_use",
- "id": "tool_123",
- "name": "calculator",
- "input": {"operation": "add", "a": 1, "b": 2}
- }
- ]);
- let content: Content = serde_json::from_value(json).unwrap();
- match content {
- Content::Chunks(chunks) => {
- assert_eq!(chunks.len(), 2);
- match &chunks[0] {
- ContentChunk::Text { text } => assert_eq!(text, "Hello"),
- _ => panic!("Expected Text chunk"),
- }
- match &chunks[1] {
- ContentChunk::ToolUse { id, name, input } => {
- assert_eq!(id, "tool_123");
- assert_eq!(name, "calculator");
- assert_eq!(input["operation"], "add");
- assert_eq!(input["a"], 1);
- assert_eq!(input["b"], 2);
- }
- _ => panic!("Expected ToolUse chunk"),
- }
- }
- _ => panic!("Expected Chunks variant"),
- }
- }
-
- #[test]
- fn test_deserialize_tool_result_untagged_text() {
- let json = json!({
- "type": "tool_result",
- "content": "Result content",
- "tool_use_id": "tool_456"
- });
- let chunk: ContentChunk = serde_json::from_value(json).unwrap();
- match chunk {
- ContentChunk::ToolResult {
- content,
- tool_use_id,
- } => {
- match content {
- Content::UntaggedText(text) => assert_eq!(text, "Result content"),
- _ => panic!("Expected UntaggedText content"),
- }
- assert_eq!(tool_use_id, "tool_456");
- }
- _ => panic!("Expected ToolResult variant"),
- }
- }
-
- #[test]
- fn test_deserialize_tool_result_chunks() {
- let json = json!({
- "type": "tool_result",
- "content": [
- {
- "type": "text",
- "text": "Processing complete"
- },
- {
- "type": "text",
- "text": "Result: 42"
- }
- ],
- "tool_use_id": "tool_789"
- });
- let chunk: ContentChunk = serde_json::from_value(json).unwrap();
- match chunk {
- ContentChunk::ToolResult {
- content,
- tool_use_id,
- } => {
- match content {
- Content::Chunks(chunks) => {
- assert_eq!(chunks.len(), 2);
- match &chunks[0] {
- ContentChunk::Text { text } => assert_eq!(text, "Processing complete"),
- _ => panic!("Expected Text chunk"),
- }
- match &chunks[1] {
- ContentChunk::Text { text } => assert_eq!(text, "Result: 42"),
- _ => panic!("Expected Text chunk"),
- }
- }
- _ => panic!("Expected Chunks content"),
- }
- assert_eq!(tool_use_id, "tool_789");
- }
- _ => panic!("Expected ToolResult variant"),
- }
+ fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+ self
}
}
@@ -1,302 +0,0 @@
-use std::path::PathBuf;
-
-use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
-use acp_thread::AcpThread;
-use agent_client_protocol as acp;
-use anyhow::{Context, Result};
-use collections::HashMap;
-use context_server::listener::{McpServerTool, ToolResponse};
-use context_server::types::{
- Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
- ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests,
-};
-use gpui::{App, AsyncApp, Task, WeakEntity};
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-
-pub struct ClaudeZedMcpServer {
- server: context_server::listener::McpServer,
-}
-
-pub const SERVER_NAME: &str = "zed";
-
-impl ClaudeZedMcpServer {
- pub async fn new(
- thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
- cx: &AsyncApp,
- ) -> Result<Self> {
- let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
- mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
-
- mcp_server.add_tool(PermissionTool {
- thread_rx: thread_rx.clone(),
- });
- mcp_server.add_tool(ReadTool {
- thread_rx: thread_rx.clone(),
- });
- mcp_server.add_tool(EditTool {
- thread_rx: thread_rx.clone(),
- });
-
- Ok(Self { server: mcp_server })
- }
-
- pub fn server_config(&self) -> Result<McpServerConfig> {
- #[cfg(not(test))]
- let zed_path = std::env::current_exe()
- .context("finding current executable path for use in mcp_server")?;
-
- #[cfg(test)]
- let zed_path = crate::e2e_tests::get_zed_path();
-
- Ok(McpServerConfig {
- command: zed_path,
- args: vec![
- "--nc".into(),
- self.server.socket_path().display().to_string(),
- ],
- env: None,
- })
- }
-
- fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
- cx.foreground_executor().spawn(async move {
- Ok(InitializeResponse {
- protocol_version: ProtocolVersion("2025-06-18".into()),
- capabilities: ServerCapabilities {
- experimental: None,
- logging: None,
- completions: None,
- prompts: None,
- resources: None,
- tools: Some(ToolsCapabilities {
- list_changed: Some(false),
- }),
- },
- server_info: Implementation {
- name: SERVER_NAME.into(),
- version: "0.1.0".into(),
- },
- meta: None,
- })
- })
- }
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct McpConfig {
- pub mcp_servers: HashMap<String, McpServerConfig>,
-}
-
-#[derive(Serialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct McpServerConfig {
- pub command: PathBuf,
- pub args: Vec<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub env: Option<HashMap<String, String>>,
-}
-
-// Tools
-
-#[derive(Clone)]
-pub struct PermissionTool {
- thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct PermissionToolParams {
- tool_name: String,
- input: serde_json::Value,
- tool_use_id: Option<String>,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct PermissionToolResponse {
- behavior: PermissionToolBehavior,
- updated_input: serde_json::Value,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "snake_case")]
-enum PermissionToolBehavior {
- Allow,
- Deny,
-}
-
-impl McpServerTool for PermissionTool {
- type Input = PermissionToolParams;
- type Output = ();
-
- const NAME: &'static str = "Confirmation";
-
- fn description(&self) -> &'static str {
- "Request permission for tool calls"
- }
-
- async fn run(
- &self,
- input: Self::Input,
- cx: &mut AsyncApp,
- ) -> Result<ToolResponse<Self::Output>> {
- let mut thread_rx = self.thread_rx.clone();
- let Some(thread) = thread_rx.recv().await?.upgrade() else {
- anyhow::bail!("Thread closed");
- };
-
- let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
- let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
- let allow_option_id = acp::PermissionOptionId("allow".into());
- let reject_option_id = acp::PermissionOptionId("reject".into());
-
- let chosen_option = thread
- .update(cx, |thread, cx| {
- thread.request_tool_call_authorization(
- claude_tool.as_acp(tool_call_id),
- vec![
- acp::PermissionOption {
- id: allow_option_id.clone(),
- name: "Allow".into(),
- kind: acp::PermissionOptionKind::AllowOnce,
- },
- acp::PermissionOption {
- id: reject_option_id.clone(),
- name: "Reject".into(),
- kind: acp::PermissionOptionKind::RejectOnce,
- },
- ],
- cx,
- )
- })?
- .await?;
-
- let response = if chosen_option == allow_option_id {
- PermissionToolResponse {
- behavior: PermissionToolBehavior::Allow,
- updated_input: input.input,
- }
- } else {
- debug_assert_eq!(chosen_option, reject_option_id);
- PermissionToolResponse {
- behavior: PermissionToolBehavior::Deny,
- updated_input: input.input,
- }
- };
-
- Ok(ToolResponse {
- content: vec![ToolResponseContent::Text {
- text: serde_json::to_string(&response)?,
- }],
- structured_content: (),
- })
- }
-}
-
-#[derive(Clone)]
-pub struct ReadTool {
- thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
-}
-
-impl McpServerTool for ReadTool {
- type Input = ReadToolParams;
- type Output = ();
-
- const NAME: &'static str = "Read";
-
- fn description(&self) -> &'static str {
- "Read the contents of a file. In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents."
- }
-
- fn annotations(&self) -> ToolAnnotations {
- ToolAnnotations {
- title: Some("Read file".to_string()),
- read_only_hint: Some(true),
- destructive_hint: Some(false),
- open_world_hint: Some(false),
- idempotent_hint: None,
- }
- }
-
- async fn run(
- &self,
- input: Self::Input,
- cx: &mut AsyncApp,
- ) -> Result<ToolResponse<Self::Output>> {
- let mut thread_rx = self.thread_rx.clone();
- let Some(thread) = thread_rx.recv().await?.upgrade() else {
- anyhow::bail!("Thread closed");
- };
-
- let content = thread
- .update(cx, |thread, cx| {
- thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
- })?
- .await?;
-
- Ok(ToolResponse {
- content: vec![ToolResponseContent::Text { text: content }],
- structured_content: (),
- })
- }
-}
-
-#[derive(Clone)]
-pub struct EditTool {
- thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
-}
-
-impl McpServerTool for EditTool {
- type Input = EditToolParams;
- type Output = ();
-
- const NAME: &'static str = "Edit";
-
- fn description(&self) -> &'static str {
- "Edits a file. In sessions with mcp__zed__Edit always use it instead of Edit as it will show the diff to the user better."
- }
-
- fn annotations(&self) -> ToolAnnotations {
- ToolAnnotations {
- title: Some("Edit file".to_string()),
- read_only_hint: Some(false),
- destructive_hint: Some(false),
- open_world_hint: Some(false),
- idempotent_hint: Some(false),
- }
- }
-
- async fn run(
- &self,
- input: Self::Input,
- cx: &mut AsyncApp,
- ) -> Result<ToolResponse<Self::Output>> {
- let mut thread_rx = self.thread_rx.clone();
- let Some(thread) = thread_rx.recv().await?.upgrade() else {
- anyhow::bail!("Thread closed");
- };
-
- let content = thread
- .update(cx, |thread, cx| {
- thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
- })?
- .await?;
-
- let new_content = content.replace(&input.old_text, &input.new_text);
- if new_content == content {
- return Err(anyhow::anyhow!("The old_text was not found in the content"));
- }
-
- thread
- .update(cx, |thread, cx| {
- thread.write_text_file(input.abs_path, new_content, cx)
- })?
- .await?;
-
- Ok(ToolResponse {
- content: vec![],
- structured_content: (),
- })
- }
-}
@@ -1,661 +0,0 @@
-use std::path::PathBuf;
-
-use agent_client_protocol as acp;
-use itertools::Itertools;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use util::ResultExt;
-
-pub enum ClaudeTool {
- Task(Option<TaskToolParams>),
- NotebookRead(Option<NotebookReadToolParams>),
- NotebookEdit(Option<NotebookEditToolParams>),
- Edit(Option<EditToolParams>),
- MultiEdit(Option<MultiEditToolParams>),
- ReadFile(Option<ReadToolParams>),
- Write(Option<WriteToolParams>),
- Ls(Option<LsToolParams>),
- Glob(Option<GlobToolParams>),
- Grep(Option<GrepToolParams>),
- Terminal(Option<BashToolParams>),
- WebFetch(Option<WebFetchToolParams>),
- WebSearch(Option<WebSearchToolParams>),
- TodoWrite(Option<TodoWriteToolParams>),
- ExitPlanMode(Option<ExitPlanModeToolParams>),
- Other {
- name: String,
- input: serde_json::Value,
- },
-}
-
-impl ClaudeTool {
- pub fn infer(tool_name: &str, input: serde_json::Value) -> Self {
- match tool_name {
- // Known tools
- "mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()),
- "mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()),
- "MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()),
- "Write" => Self::Write(serde_json::from_value(input).log_err()),
- "LS" => Self::Ls(serde_json::from_value(input).log_err()),
- "Glob" => Self::Glob(serde_json::from_value(input).log_err()),
- "Grep" => Self::Grep(serde_json::from_value(input).log_err()),
- "Bash" => Self::Terminal(serde_json::from_value(input).log_err()),
- "WebFetch" => Self::WebFetch(serde_json::from_value(input).log_err()),
- "WebSearch" => Self::WebSearch(serde_json::from_value(input).log_err()),
- "TodoWrite" => Self::TodoWrite(serde_json::from_value(input).log_err()),
- "exit_plan_mode" => Self::ExitPlanMode(serde_json::from_value(input).log_err()),
- "Task" => Self::Task(serde_json::from_value(input).log_err()),
- "NotebookRead" => Self::NotebookRead(serde_json::from_value(input).log_err()),
- "NotebookEdit" => Self::NotebookEdit(serde_json::from_value(input).log_err()),
- // Inferred from name
- _ => {
- let tool_name = tool_name.to_lowercase();
-
- if tool_name.contains("edit") || tool_name.contains("write") {
- Self::Edit(None)
- } else if tool_name.contains("terminal") {
- Self::Terminal(None)
- } else {
- Self::Other {
- name: tool_name.to_string(),
- input,
- }
- }
- }
- }
- }
-
- pub fn label(&self) -> String {
- match &self {
- Self::Task(Some(params)) => params.description.clone(),
- Self::Task(None) => "Task".into(),
- Self::NotebookRead(Some(params)) => {
- format!("Read Notebook {}", params.notebook_path.display())
- }
- Self::NotebookRead(None) => "Read Notebook".into(),
- Self::NotebookEdit(Some(params)) => {
- format!("Edit Notebook {}", params.notebook_path.display())
- }
- Self::NotebookEdit(None) => "Edit Notebook".into(),
- Self::Terminal(Some(params)) => format!("`{}`", params.command),
- Self::Terminal(None) => "Terminal".into(),
- Self::ReadFile(_) => "Read File".into(),
- Self::Ls(Some(params)) => {
- format!("List Directory {}", params.path.display())
- }
- Self::Ls(None) => "List Directory".into(),
- Self::Edit(Some(params)) => {
- format!("Edit {}", params.abs_path.display())
- }
- Self::Edit(None) => "Edit".into(),
- Self::MultiEdit(Some(params)) => {
- format!("Multi Edit {}", params.file_path.display())
- }
- Self::MultiEdit(None) => "Multi Edit".into(),
- Self::Write(Some(params)) => {
- format!("Write {}", params.file_path.display())
- }
- Self::Write(None) => "Write".into(),
- Self::Glob(Some(params)) => {
- format!("Glob `{params}`")
- }
- Self::Glob(None) => "Glob".into(),
- Self::Grep(Some(params)) => format!("`{params}`"),
- Self::Grep(None) => "Grep".into(),
- Self::WebFetch(Some(params)) => format!("Fetch {}", params.url),
- Self::WebFetch(None) => "Fetch".into(),
- Self::WebSearch(Some(params)) => format!("Web Search: {}", params),
- Self::WebSearch(None) => "Web Search".into(),
- Self::TodoWrite(Some(params)) => format!(
- "Update TODOs: {}",
- params.todos.iter().map(|todo| &todo.content).join(", ")
- ),
- Self::TodoWrite(None) => "Update TODOs".into(),
- Self::ExitPlanMode(_) => "Exit Plan Mode".into(),
- Self::Other { name, .. } => name.clone(),
- }
- }
- pub fn content(&self) -> Vec<acp::ToolCallContent> {
- match &self {
- Self::Other { input, .. } => vec![
- format!(
- "```json\n{}```",
- serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
- )
- .into(),
- ],
- Self::Task(Some(params)) => vec![params.prompt.clone().into()],
- Self::NotebookRead(Some(params)) => {
- vec![params.notebook_path.display().to_string().into()]
- }
- Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()],
- Self::Terminal(Some(params)) => vec![
- format!(
- "`{}`\n\n{}",
- params.command,
- params.description.as_deref().unwrap_or_default()
- )
- .into(),
- ],
- Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()],
- Self::Ls(Some(params)) => vec![params.path.display().to_string().into()],
- Self::Glob(Some(params)) => vec![params.to_string().into()],
- Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
- Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
- Self::WebSearch(Some(params)) => vec![params.to_string().into()],
- Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
- Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
- diff: acp::Diff {
- path: params.abs_path.clone(),
- old_text: Some(params.old_text.clone()),
- new_text: params.new_text.clone(),
- },
- }],
- Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff {
- diff: acp::Diff {
- path: params.file_path.clone(),
- old_text: None,
- new_text: params.content.clone(),
- },
- }],
- Self::MultiEdit(Some(params)) => {
- // todo: show multiple edits in a multibuffer?
- params
- .edits
- .first()
- .map(|edit| {
- vec![acp::ToolCallContent::Diff {
- diff: acp::Diff {
- path: params.file_path.clone(),
- old_text: Some(edit.old_string.clone()),
- new_text: edit.new_string.clone(),
- },
- }]
- })
- .unwrap_or_default()
- }
- Self::TodoWrite(Some(_)) => {
- // These are mapped to plan updates later
- vec![]
- }
- Self::Task(None)
- | Self::NotebookRead(None)
- | Self::NotebookEdit(None)
- | Self::Terminal(None)
- | Self::ReadFile(None)
- | Self::Ls(None)
- | Self::Glob(None)
- | Self::Grep(None)
- | Self::WebFetch(None)
- | Self::WebSearch(None)
- | Self::TodoWrite(None)
- | Self::ExitPlanMode(None)
- | Self::Edit(None)
- | Self::Write(None)
- | Self::MultiEdit(None) => vec![],
- }
- }
-
- pub fn kind(&self) -> acp::ToolKind {
- match self {
- Self::Task(_) => acp::ToolKind::Think,
- Self::NotebookRead(_) => acp::ToolKind::Read,
- Self::NotebookEdit(_) => acp::ToolKind::Edit,
- Self::Edit(_) => acp::ToolKind::Edit,
- Self::MultiEdit(_) => acp::ToolKind::Edit,
- Self::Write(_) => acp::ToolKind::Edit,
- Self::ReadFile(_) => acp::ToolKind::Read,
- Self::Ls(_) => acp::ToolKind::Search,
- Self::Glob(_) => acp::ToolKind::Search,
- Self::Grep(_) => acp::ToolKind::Search,
- Self::Terminal(_) => acp::ToolKind::Execute,
- Self::WebSearch(_) => acp::ToolKind::Search,
- Self::WebFetch(_) => acp::ToolKind::Fetch,
- Self::TodoWrite(_) => acp::ToolKind::Think,
- Self::ExitPlanMode(_) => acp::ToolKind::Think,
- Self::Other { .. } => acp::ToolKind::Other,
- }
- }
-
- pub fn locations(&self) -> Vec<acp::ToolCallLocation> {
- match &self {
- Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation {
- path: abs_path.clone(),
- line: None,
- }],
- Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
- vec![acp::ToolCallLocation {
- path: file_path.clone(),
- line: None,
- }]
- }
- Self::Write(Some(WriteToolParams { file_path, .. })) => {
- vec![acp::ToolCallLocation {
- path: file_path.clone(),
- line: None,
- }]
- }
- Self::ReadFile(Some(ReadToolParams {
- abs_path, offset, ..
- })) => vec![acp::ToolCallLocation {
- path: abs_path.clone(),
- line: *offset,
- }],
- Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
- vec![acp::ToolCallLocation {
- path: notebook_path.clone(),
- line: None,
- }]
- }
- Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
- vec![acp::ToolCallLocation {
- path: notebook_path.clone(),
- line: None,
- }]
- }
- Self::Glob(Some(GlobToolParams {
- path: Some(path), ..
- })) => vec![acp::ToolCallLocation {
- path: path.clone(),
- line: None,
- }],
- Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation {
- path: path.clone(),
- line: None,
- }],
- Self::Grep(Some(GrepToolParams {
- path: Some(path), ..
- })) => vec![acp::ToolCallLocation {
- path: PathBuf::from(path),
- line: None,
- }],
- Self::Task(_)
- | Self::NotebookRead(None)
- | Self::NotebookEdit(None)
- | Self::Edit(None)
- | Self::MultiEdit(None)
- | Self::Write(None)
- | Self::ReadFile(None)
- | Self::Ls(None)
- | Self::Glob(_)
- | Self::Grep(_)
- | Self::Terminal(_)
- | Self::WebFetch(_)
- | Self::WebSearch(_)
- | Self::TodoWrite(_)
- | Self::ExitPlanMode(_)
- | Self::Other { .. } => vec![],
- }
- }
-
- pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall {
- acp::ToolCall {
- id,
- kind: self.kind(),
- status: acp::ToolCallStatus::InProgress,
- title: self.label(),
- content: self.content(),
- locations: self.locations(),
- raw_input: None,
- raw_output: None,
- }
- }
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct EditToolParams {
- /// The absolute path to the file to read.
- pub abs_path: PathBuf,
- /// The old text to replace (must be unique in the file)
- pub old_text: String,
- /// The new text.
- pub new_text: String,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct ReadToolParams {
- /// The absolute path to the file to read.
- pub abs_path: PathBuf,
- /// Which line to start reading from. Omit to start from the beginning.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub offset: Option<u32>,
- /// How many lines to read. Omit for the whole file.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub limit: Option<u32>,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct WriteToolParams {
- /// Absolute path for new file
- pub file_path: PathBuf,
- /// File content
- pub content: String,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct BashToolParams {
- /// Shell command to execute
- pub command: String,
- /// 5-10 word description of what command does
- #[serde(skip_serializing_if = "Option::is_none")]
- pub description: Option<String>,
- /// Timeout in ms (max 600000ms/10min, default 120000ms)
- #[serde(skip_serializing_if = "Option::is_none")]
- pub timeout: Option<u32>,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct GlobToolParams {
- /// Glob pattern like **/*.js or src/**/*.ts
- pub pattern: String,
- /// Directory to search in (omit for current directory)
- #[serde(skip_serializing_if = "Option::is_none")]
- pub path: Option<PathBuf>,
-}
-
-impl std::fmt::Display for GlobToolParams {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- if let Some(path) = &self.path {
- write!(f, "{}", path.display())?;
- }
- write!(f, "{}", self.pattern)
- }
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct LsToolParams {
- /// Absolute path to directory
- pub path: PathBuf,
- /// Array of glob patterns to ignore
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub ignore: Vec<String>,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct GrepToolParams {
- /// Regex pattern to search for
- pub pattern: String,
- /// File/directory to search (defaults to current directory)
- #[serde(skip_serializing_if = "Option::is_none")]
- pub path: Option<String>,
- /// "content" (shows lines), "files_with_matches" (default), "count"
- #[serde(skip_serializing_if = "Option::is_none")]
- pub output_mode: Option<GrepOutputMode>,
- /// Filter files with glob pattern like "*.js"
- #[serde(skip_serializing_if = "Option::is_none")]
- pub glob: Option<String>,
- /// File type filter like "js", "py", "rust"
- #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
- pub file_type: Option<String>,
- /// Case insensitive search
- #[serde(rename = "-i", default, skip_serializing_if = "is_false")]
- pub case_insensitive: bool,
- /// Show line numbers (content mode only)
- #[serde(rename = "-n", default, skip_serializing_if = "is_false")]
- pub line_numbers: bool,
- /// Lines after match (content mode only)
- #[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
- pub after_context: Option<u32>,
- /// Lines before match (content mode only)
- #[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
- pub before_context: Option<u32>,
- /// Lines before and after match (content mode only)
- #[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
- pub context: Option<u32>,
- /// Enable multiline/cross-line matching
- #[serde(default, skip_serializing_if = "is_false")]
- pub multiline: bool,
- /// Limit output to first N results
- #[serde(skip_serializing_if = "Option::is_none")]
- pub head_limit: Option<u32>,
-}
-
-impl std::fmt::Display for GrepToolParams {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "grep")?;
-
- // Boolean flags
- if self.case_insensitive {
- write!(f, " -i")?;
- }
- if self.line_numbers {
- write!(f, " -n")?;
- }
-
- // Context options
- if let Some(after) = self.after_context {
- write!(f, " -A {}", after)?;
- }
- if let Some(before) = self.before_context {
- write!(f, " -B {}", before)?;
- }
- if let Some(context) = self.context {
- write!(f, " -C {}", context)?;
- }
-
- // Output mode
- if let Some(mode) = &self.output_mode {
- match mode {
- GrepOutputMode::FilesWithMatches => write!(f, " -l")?,
- GrepOutputMode::Count => write!(f, " -c")?,
- GrepOutputMode::Content => {} // Default mode
- }
- }
-
- // Head limit
- if let Some(limit) = self.head_limit {
- write!(f, " | head -{}", limit)?;
- }
-
- // Glob pattern
- if let Some(glob) = &self.glob {
- write!(f, " --include=\"{}\"", glob)?;
- }
-
- // File type
- if let Some(file_type) = &self.file_type {
- write!(f, " --type={}", file_type)?;
- }
-
- // Multiline
- if self.multiline {
- write!(f, " -P")?; // Perl-compatible regex for multiline
- }
-
- // Pattern (escaped if contains special characters)
- write!(f, " \"{}\"", self.pattern)?;
-
- // Path
- if let Some(path) = &self.path {
- write!(f, " {}", path)?;
- }
-
- Ok(())
- }
-}
-
-#[derive(Default, Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
-#[serde(rename_all = "snake_case")]
-pub enum TodoPriority {
- High,
- #[default]
- Medium,
- Low,
-}
-
-impl Into<acp::PlanEntryPriority> for TodoPriority {
- fn into(self) -> acp::PlanEntryPriority {
- match self {
- TodoPriority::High => acp::PlanEntryPriority::High,
- TodoPriority::Medium => acp::PlanEntryPriority::Medium,
- TodoPriority::Low => acp::PlanEntryPriority::Low,
- }
- }
-}
-
-#[derive(Deserialize, Serialize, JsonSchema, Debug)]
-#[serde(rename_all = "snake_case")]
-pub enum TodoStatus {
- Pending,
- InProgress,
- Completed,
-}
-
-impl Into<acp::PlanEntryStatus> for TodoStatus {
- fn into(self) -> acp::PlanEntryStatus {
- match self {
- TodoStatus::Pending => acp::PlanEntryStatus::Pending,
- TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
- TodoStatus::Completed => acp::PlanEntryStatus::Completed,
- }
- }
-}
-
-#[derive(Deserialize, Serialize, JsonSchema, Debug)]
-pub struct Todo {
- /// Task description
- pub content: String,
- /// Current status of the todo
- pub status: TodoStatus,
- /// Priority level of the todo
- #[serde(default)]
- pub priority: TodoPriority,
-}
-
-impl Into<acp::PlanEntry> for Todo {
- fn into(self) -> acp::PlanEntry {
- acp::PlanEntry {
- content: self.content,
- priority: self.priority.into(),
- status: self.status.into(),
- }
- }
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct TodoWriteToolParams {
- pub todos: Vec<Todo>,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct ExitPlanModeToolParams {
- /// Implementation plan in markdown format
- pub plan: String,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct TaskToolParams {
- /// Short 3-5 word description of task
- pub description: String,
- /// Detailed task for agent to perform
- pub prompt: String,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct NotebookReadToolParams {
- /// Absolute path to .ipynb file
- pub notebook_path: PathBuf,
- /// Specific cell ID to read
- #[serde(skip_serializing_if = "Option::is_none")]
- pub cell_id: Option<String>,
-}
-
-#[derive(Deserialize, Serialize, JsonSchema, Debug)]
-#[serde(rename_all = "snake_case")]
-pub enum CellType {
- Code,
- Markdown,
-}
-
-#[derive(Deserialize, Serialize, JsonSchema, Debug)]
-#[serde(rename_all = "snake_case")]
-pub enum EditMode {
- Replace,
- Insert,
- Delete,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct NotebookEditToolParams {
- /// Absolute path to .ipynb file
- pub notebook_path: PathBuf,
- /// New cell content
- pub new_source: String,
- /// Cell ID to edit
- #[serde(skip_serializing_if = "Option::is_none")]
- pub cell_id: Option<String>,
- /// Type of cell (code or markdown)
- #[serde(skip_serializing_if = "Option::is_none")]
- pub cell_type: Option<CellType>,
- /// Edit operation mode
- #[serde(skip_serializing_if = "Option::is_none")]
- pub edit_mode: Option<EditMode>,
-}
-
-#[derive(Deserialize, Serialize, JsonSchema, Debug)]
-pub struct MultiEditItem {
- /// The text to search for and replace
- pub old_string: String,
- /// The replacement text
- pub new_string: String,
- /// Whether to replace all occurrences or just the first
- #[serde(default, skip_serializing_if = "is_false")]
- pub replace_all: bool,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct MultiEditToolParams {
- /// Absolute path to file
- pub file_path: PathBuf,
- /// List of edits to apply
- pub edits: Vec<MultiEditItem>,
-}
-
-fn is_false(v: &bool) -> bool {
- !*v
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-#[serde(rename_all = "snake_case")]
-pub enum GrepOutputMode {
- Content,
- FilesWithMatches,
- Count,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct WebFetchToolParams {
- /// Valid URL to fetch
- #[serde(rename = "url")]
- pub url: String,
- /// What to extract from content
- pub prompt: String,
-}
-
-#[derive(Deserialize, JsonSchema, Debug)]
-pub struct WebSearchToolParams {
- /// Search query (min 2 chars)
- pub query: String,
- /// Only include these domains
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub allowed_domains: Vec<String>,
- /// Exclude these domains
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub blocked_domains: Vec<String>,
-}
-
-impl std::fmt::Display for WebSearchToolParams {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "\"{}\"", self.query)?;
-
- if !self.allowed_domains.is_empty() {
- write!(f, " (allowed: {})", self.allowed_domains.join(", "))?;
- }
-
- if !self.blocked_domains.is_empty() {
- write!(f, " (blocked: {})", self.blocked_domains.join(", "))?;
- }
-
- Ok(())
- }
-}
@@ -0,0 +1,102 @@
+use crate::AgentServerDelegate;
+use acp_thread::AgentConnection;
+use agent_client_protocol as acp;
+use anyhow::{Context as _, Result};
+use fs::Fs;
+use gpui::{App, AppContext as _, SharedString, Task};
+use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName};
+use settings::{SettingsStore, update_settings_file};
+use std::{path::Path, rc::Rc, sync::Arc};
+use ui::IconName;
+
+/// A generic agent server implementation for custom user-defined agents
+pub struct CustomAgentServer {
+ name: SharedString,
+}
+
+impl CustomAgentServer {
+ pub fn new(name: SharedString) -> Self {
+ Self { name }
+ }
+}
+
+impl crate::AgentServer for CustomAgentServer {
+ fn telemetry_id(&self) -> &'static str {
+ "custom"
+ }
+
+ fn name(&self) -> SharedString {
+ self.name.clone()
+ }
+
+ fn logo(&self) -> IconName {
+ IconName::Terminal
+ }
+
+ fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
+ let settings = cx.read_global(|settings: &SettingsStore, _| {
+ settings
+ .get::<AllAgentServersSettings>(None)
+ .custom
+ .get(&self.name())
+ .cloned()
+ });
+
+ settings
+ .as_ref()
+ .and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
+ }
+
+ fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
+ let name = self.name();
+ update_settings_file::<AllAgentServersSettings>(fs, cx, move |settings, _| {
+ settings.custom.get_mut(&name).unwrap().default_mode = mode_id.map(|m| m.to_string())
+ });
+ }
+
+ fn connect(
+ &self,
+ root_dir: Option<&Path>,
+ delegate: AgentServerDelegate,
+ cx: &mut App,
+ ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
+ let name = self.name();
+ let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
+ let is_remote = delegate.project.read(cx).is_via_remote_server();
+ let default_mode = self.default_mode(cx);
+ let store = delegate.store.downgrade();
+
+ cx.spawn(async move |cx| {
+ let (command, root_dir, login) = store
+ .update(cx, |store, cx| {
+ let agent = store
+ .get_external_agent(&ExternalAgentServerName(name.clone()))
+ .with_context(|| {
+ format!("Custom agent server `{}` is not registered", name)
+ })?;
+ anyhow::Ok(agent.get_command(
+ root_dir.as_deref(),
+ Default::default(),
+ delegate.status_tx,
+ delegate.new_version_available,
+ &mut cx.to_async(),
+ ))
+ })??
+ .await?;
+ let connection = crate::acp::connect(
+ name,
+ command,
+ root_dir.as_ref(),
+ default_mode,
+ is_remote,
+ cx,
+ )
+ .await?;
+ Ok((connection, login))
+ })
+ }
+
+ fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
+ self
+ }
+}
@@ -1,24 +1,33 @@
+use crate::{AgentServer, AgentServerDelegate};
+use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
+use agent_client_protocol as acp;
+use futures::{FutureExt, StreamExt, channel::mpsc, select};
+use gpui::{AppContext, Entity, TestAppContext};
+use indoc::indoc;
+#[cfg(test)]
+use project::agent_server_store::BuiltinAgentServerSettings;
+use project::{FakeFs, Project, agent_server_store::AllAgentServersSettings};
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
-
-use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings};
-use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
-use agent_client_protocol as acp;
-
-use futures::{FutureExt, StreamExt, channel::mpsc, select};
-use gpui::{Entity, TestAppContext};
-use indoc::indoc;
-use project::{FakeFs, Project};
-use settings::{Settings, SettingsStore};
use util::path;
-pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
- let fs = init_test(cx).await;
- let project = Project::test(fs, [], cx).await;
- let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
+pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
+where
+ T: AgentServer + 'static,
+ F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+ let fs = init_test(cx).await as Arc<dyn fs::Fs>;
+ let project = Project::test(fs.clone(), [], cx).await;
+ let thread = new_test_thread(
+ server(&fs, &project, cx).await,
+ project.clone(),
+ "/private/tmp",
+ cx,
+ )
+ .await;
thread
.update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx))
@@ -42,8 +51,12 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont
});
}
-pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
- let _fs = init_test(cx).await;
+pub async fn test_path_mentions<T, F>(server: F, cx: &mut TestAppContext)
+where
+ T: AgentServer + 'static,
+ F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+ let fs = init_test(cx).await as _;
let tempdir = tempfile::tempdir().unwrap();
std::fs::write(
@@ -56,7 +69,13 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
)
.expect("failed to write file");
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
- let thread = new_test_thread(server, project.clone(), tempdir.path(), cx).await;
+ let thread = new_test_thread(
+ server(&fs, &project, cx).await,
+ project.clone(),
+ tempdir.path(),
+ cx,
+ )
+ .await;
thread
.update(cx, |thread, cx| {
thread.send(
@@ -110,15 +129,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
drop(tempdir);
}
-pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
- let _fs = init_test(cx).await;
+pub async fn test_tool_call<T, F>(server: F, cx: &mut TestAppContext)
+where
+ T: AgentServer + 'static,
+ F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+ let fs = init_test(cx).await as _;
let tempdir = tempfile::tempdir().unwrap();
let foo_path = tempdir.path().join("foo");
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
- let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
+ let thread = new_test_thread(
+ server(&fs, &project, cx).await,
+ project.clone(),
+ "/private/tmp",
+ cx,
+ )
+ .await;
thread
.update(cx, |thread, cx| {
@@ -134,7 +163,9 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
matches!(
entry,
AgentThreadEntry::ToolCall(ToolCall {
- status: ToolCallStatus::Allowed { .. },
+ status: ToolCallStatus::Pending
+ | ToolCallStatus::InProgress
+ | ToolCallStatus::Completed,
..
})
)
@@ -150,14 +181,23 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
drop(tempdir);
}
-pub async fn test_tool_call_with_permission(
- server: impl AgentServer + 'static,
+pub async fn test_tool_call_with_permission<T, F>(
+ server: F,
allow_option_id: acp::PermissionOptionId,
cx: &mut TestAppContext,
-) {
- let fs = init_test(cx).await;
- let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
- let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
+) where
+ T: AgentServer + 'static,
+ F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+ let fs = init_test(cx).await as Arc<dyn fs::Fs>;
+ let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await;
+ let thread = new_test_thread(
+ server(&fs, &project, cx).await,
+ project.clone(),
+ "/private/tmp",
+ cx,
+ )
+ .await;
let full_turn = thread.update(cx, |thread, cx| {
thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
@@ -212,7 +252,9 @@ pub async fn test_tool_call_with_permission(
assert!(thread.entries().iter().any(|entry| matches!(
entry,
AgentThreadEntry::ToolCall(ToolCall {
- status: ToolCallStatus::Allowed { .. },
+ status: ToolCallStatus::Pending
+ | ToolCallStatus::InProgress
+ | ToolCallStatus::Completed,
..
})
)));
@@ -223,7 +265,9 @@ pub async fn test_tool_call_with_permission(
thread.read_with(cx, |thread, cx| {
let AgentThreadEntry::ToolCall(ToolCall {
content,
- status: ToolCallStatus::Allowed { .. },
+ status: ToolCallStatus::Pending
+ | ToolCallStatus::InProgress
+ | ToolCallStatus::Completed,
..
}) = thread
.entries()
@@ -241,11 +285,21 @@ pub async fn test_tool_call_with_permission(
});
}
-pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
- let fs = init_test(cx).await;
-
- let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
- let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
+pub async fn test_cancel<T, F>(server: F, cx: &mut TestAppContext)
+where
+ T: AgentServer + 'static,
+ F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+ let fs = init_test(cx).await as Arc<dyn fs::Fs>;
+
+ let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await;
+ let thread = new_test_thread(
+ server(&fs, &project, cx).await,
+ project.clone(),
+ "/private/tmp",
+ cx,
+ )
+ .await;
let _ = thread.update(cx, |thread, cx| {
thread.send_raw(
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
@@ -310,10 +364,20 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
});
}
-pub async fn test_thread_drop(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
- let fs = init_test(cx).await;
- let project = Project::test(fs, [], cx).await;
- let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
+pub async fn test_thread_drop<T, F>(server: F, cx: &mut TestAppContext)
+where
+ T: AgentServer + 'static,
+ F: AsyncFn(&Arc<dyn fs::Fs>, &Entity<Project>, &mut TestAppContext) -> T,
+{
+ let fs = init_test(cx).await as Arc<dyn fs::Fs>;
+ let project = Project::test(fs.clone(), [], cx).await;
+ let thread = new_test_thread(
+ server(&fs, &project, cx).await,
+ project.clone(),
+ "/private/tmp",
+ cx,
+ )
+ .await;
thread
.update(cx, |thread, cx| thread.send_raw("Hello from test!", cx))
@@ -380,27 +444,43 @@ macro_rules! common_e2e_tests {
}
};
}
+pub use common_e2e_tests;
// Helpers
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
+ use settings::Settings;
+
env_logger::try_init().ok();
cx.update(|cx| {
- let settings_store = SettingsStore::test(cx);
+ let settings_store = settings::SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
language::init(cx);
- crate::settings::init(cx);
-
- crate::AllAgentServersSettings::override_global(
+ gpui_tokio::init(cx);
+ let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap();
+ cx.set_http_client(Arc::new(http_client));
+ client::init_settings(cx);
+ let client = client::Client::production(cx);
+ let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
+ language_model::init(client.clone(), cx);
+ language_models::init(user_store, client, cx);
+ agent_settings::init(cx);
+ AllAgentServersSettings::register(cx);
+
+ #[cfg(test)]
+ AllAgentServersSettings::override_global(
AllAgentServersSettings {
- claude: Some(AgentServerSettings {
- command: crate::claude::tests::local_command(),
- }),
- gemini: Some(AgentServerSettings {
- command: crate::gemini::tests::local_command(),
+ claude: Some(BuiltinAgentServerSettings {
+ path: Some("claude-code-acp".into()),
+ args: None,
+ env: None,
+ ignore_system_version: None,
+ default_mode: None,
}),
+ gemini: Some(crate::gemini::tests::local_command().into()),
+ custom: collections::HashMap::default(),
},
cx,
);
@@ -417,17 +497,17 @@ pub async fn new_test_thread(
current_dir: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> Entity<AcpThread> {
- let connection = cx
- .update(|cx| server.connect(current_dir.as_ref(), &project, cx))
- .await
- .unwrap();
+ let store = project.read_with(cx, |project, _| project.agent_server_store().clone());
+ let delegate = AgentServerDelegate::new(store, project.clone(), None, None);
- let thread = connection
- .new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async())
+ let (connection, _) = cx
+ .update(|cx| server.connect(Some(current_dir.as_ref()), delegate, cx))
.await
.unwrap();
- thread
+ cx.update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx))
+ .await
+ .unwrap()
}
pub async fn run_until_first_tool_call(
@@ -465,7 +545,7 @@ pub fn get_zed_path() -> PathBuf {
while zed_path
.file_name()
- .map_or(true, |name| name.to_string_lossy() != "debug")
+ .is_none_or(|name| name.to_string_lossy() != "debug")
{
if !zed_path.pop() {
panic!("Could not find target directory");
@@ -1,32 +1,26 @@
-use std::path::Path;
use std::rc::Rc;
-
-use crate::{AgentServer, AgentServerCommand};
-use acp_thread::{AgentConnection, LoadError};
-use anyhow::Result;
-use gpui::{Entity, Task};
-use project::Project;
+use std::{any::Any, path::Path};
+
+use crate::{AgentServer, AgentServerDelegate};
+use acp_thread::AgentConnection;
+use anyhow::{Context as _, Result};
+use client::ProxySettings;
+use collections::HashMap;
+use gpui::{App, AppContext, SharedString, Task};
+use language_models::provider::google::GoogleLanguageModelProvider;
+use project::agent_server_store::GEMINI_NAME;
use settings::SettingsStore;
-use ui::App;
-
-use crate::AllAgentServersSettings;
#[derive(Clone)]
pub struct Gemini;
-const ACP_ARG: &str = "--experimental-acp";
-
impl AgentServer for Gemini {
- fn name(&self) -> &'static str {
- "Gemini"
- }
-
- fn empty_state_headline(&self) -> &'static str {
- "Welcome to Gemini"
+ fn telemetry_id(&self) -> &'static str {
+ "gemini-cli"
}
- fn empty_state_message(&self) -> &'static str {
- "Ask questions, edit files, run commands.\nBe specific for the best results."
+ fn name(&self) -> SharedString {
+ "Gemini CLI".into()
}
fn logo(&self) -> ui::IconName {
@@ -35,66 +29,73 @@ impl AgentServer for Gemini {
fn connect(
&self,
- root_dir: &Path,
- project: &Entity<Project>,
+ root_dir: Option<&Path>,
+ delegate: AgentServerDelegate,
cx: &mut App,
- ) -> Task<Result<Rc<dyn AgentConnection>>> {
- let project = project.clone();
- let root_dir = root_dir.to_path_buf();
- let server_name = self.name();
- cx.spawn(async move |cx| {
- let settings = cx.read_global(|settings: &SettingsStore, _| {
- settings.get::<AllAgentServersSettings>(None).gemini.clone()
- })?;
-
- let Some(command) =
- AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await
- else {
- anyhow::bail!("Failed to find gemini binary");
- };
-
- let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
- if result.is_err() {
- let version_fut = util::command::new_smol_command(&command.path)
- .args(command.args.iter())
- .arg("--version")
- .kill_on_drop(true)
- .output();
-
- let help_fut = util::command::new_smol_command(&command.path)
- .args(command.args.iter())
- .arg("--help")
- .kill_on_drop(true)
- .output();
+ ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
+ let name = self.name();
+ let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
+ let is_remote = delegate.project.read(cx).is_via_remote_server();
+ let store = delegate.store.downgrade();
+ let proxy_url = cx.read_global(|settings: &SettingsStore, _| {
+ settings.get::<ProxySettings>(None).proxy.clone()
+ });
+ let default_mode = self.default_mode(cx);
- let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
-
- let current_version = String::from_utf8(version_output?.stdout)?;
- let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
-
- if !supported {
- return Err(LoadError::Unsupported {
- error_message: format!(
- "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).",
- current_version
- ).into(),
- upgrade_message: "Upgrade Gemini to Latest".into(),
- upgrade_command: "npm install -g @google/gemini-cli@latest".into(),
- }.into())
- }
+ cx.spawn(async move |cx| {
+ let mut extra_env = HashMap::default();
+ if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
+ extra_env.insert("GEMINI_API_KEY".into(), api_key.key);
+ }
+ let (mut command, root_dir, login) = store
+ .update(cx, |store, cx| {
+ let agent = store
+ .get_external_agent(&GEMINI_NAME.into())
+ .context("Gemini CLI is not registered")?;
+ anyhow::Ok(agent.get_command(
+ root_dir.as_deref(),
+ extra_env,
+ delegate.status_tx,
+ delegate.new_version_available,
+ &mut cx.to_async(),
+ ))
+ })??
+ .await?;
+
+ // Add proxy flag if proxy settings are configured in Zed and not in the args
+ if let Some(proxy_url_value) = &proxy_url
+ && !command.args.iter().any(|arg| arg.contains("--proxy"))
+ {
+ command.args.push("--proxy".into());
+ command.args.push(proxy_url_value.clone());
}
- result
+
+ let connection = crate::acp::connect(
+ name,
+ command,
+ root_dir.as_ref(),
+ default_mode,
+ is_remote,
+ cx,
+ )
+ .await?;
+ Ok((connection, login))
})
}
+
+ fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+ self
+ }
}
#[cfg(test)]
pub(crate) mod tests {
+ use project::agent_server_store::AgentServerCommand;
+
use super::*;
- use crate::AgentServerCommand;
use std::path::Path;
- crate::common_e2e_tests!(Gemini, allow_option_id = "proceed_once");
+ crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once");
pub fn local_command() -> AgentServerCommand {
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
@@ -1,41 +1,121 @@
+use agent_client_protocol as acp;
+use std::path::PathBuf;
+
use crate::AgentServerCommand;
use anyhow::Result;
-use gpui::App;
+use collections::HashMap;
+use gpui::{App, SharedString};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
pub fn init(cx: &mut App) {
AllAgentServersSettings::register(cx);
}
-#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
+#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "agent_servers")]
pub struct AllAgentServersSettings {
- pub gemini: Option<AgentServerSettings>,
- pub claude: Option<AgentServerSettings>,
+ pub gemini: Option<BuiltinAgentServerSettings>,
+ pub claude: Option<BuiltinAgentServerSettings>,
+
+ /// Custom agent servers configured by the user
+ #[serde(flatten)]
+ pub custom: HashMap<SharedString, CustomAgentServerSettings>,
+}
+
+#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+pub struct BuiltinAgentServerSettings {
+ /// Absolute path to a binary to be used when launching this agent.
+ ///
+ /// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
+ #[serde(rename = "command")]
+ pub path: Option<PathBuf>,
+ /// If a binary is specified in `command`, it will be passed these arguments.
+ pub args: Option<Vec<String>>,
+ /// If a binary is specified in `command`, it will be passed these environment variables.
+ pub env: Option<HashMap<String, String>>,
+ /// Whether to skip searching `$PATH` for an agent server binary when
+ /// launching this agent.
+ ///
+ /// This has no effect if a `command` is specified. Otherwise, when this is
+ /// `false`, Zed will search `$PATH` for an agent server binary and, if one
+ /// is found, use it for threads with this agent. If no agent binary is
+ /// found on `$PATH`, Zed will automatically install and use its own binary.
+ /// When this is `true`, Zed will not search `$PATH`, and will always use
+ /// its own binary.
+ ///
+ /// Default: true
+ pub ignore_system_version: Option<bool>,
+ /// The default mode for new threads.
+ ///
+ /// Note: Not all agents support modes.
+ ///
+ /// Default: None
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub default_mode: Option<acp::SessionModeId>,
+}
+
+impl BuiltinAgentServerSettings {
+ pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
+ self.path.map(|path| AgentServerCommand {
+ path,
+ args: self.args.unwrap_or_default(),
+ env: self.env,
+ })
+ }
}
-#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
-pub struct AgentServerSettings {
+impl From<AgentServerCommand> for BuiltinAgentServerSettings {
+ fn from(value: AgentServerCommand) -> Self {
+ BuiltinAgentServerSettings {
+ path: Some(value.path),
+ args: Some(value.args),
+ env: value.env,
+ ..Default::default()
+ }
+ }
+}
+
+#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
+pub struct CustomAgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
+ /// The default mode for new threads.
+ ///
+ /// Note: Not all agents support modes.
+ ///
+ /// Default: None
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub default_mode: Option<acp::SessionModeId>,
}
impl settings::Settings for AllAgentServersSettings {
- const KEY: Option<&'static str> = Some("agent_servers");
-
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
- for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() {
+ for AllAgentServersSettings {
+ gemini,
+ claude,
+ custom,
+ } in sources.defaults_and_customizations()
+ {
if gemini.is_some() {
settings.gemini = gemini.clone();
}
if claude.is_some() {
settings.claude = claude.clone();
}
+
+ // Merge custom agents
+ for (name, config) in custom {
+ // Skip built-in agent names to avoid conflicts
+ if name != "gemini" && name != "claude" {
+ settings.custom.insert(name.clone(), config.clone());
+ }
+ }
}
Ok(settings)
@@ -58,7 +58,7 @@ impl AgentProfileSettings {
|| self
.context_servers
.get(server_id)
- .map_or(false, |preset| preset.tools.get(tool_name) == Some(&true))
+ .is_some_and(|preset| preset.tools.get(tool_name) == Some(&true))
}
}
@@ -8,13 +8,15 @@ use gpui::{App, Pixels, SharedString};
use language_model::LanguageModel;
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use std::borrow::Cow;
pub use crate::agent_profile::*;
pub const SUMMARIZE_THREAD_PROMPT: &str =
include_str!("../../agent/src/prompts/summarize_thread_prompt.txt");
+pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str =
+ include_str!("../../agent/src/prompts/summarize_thread_detailed_prompt.txt");
pub fn init(cx: &mut App) {
AgentSettings::register(cx);
@@ -116,15 +118,15 @@ pub struct LanguageModelParameters {
impl LanguageModelParameters {
pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool {
- if let Some(provider) = &self.provider {
- if provider.0 != model.provider_id().0 {
- return false;
- }
+ if let Some(provider) = &self.provider
+ && provider.0 != model.provider_id().0
+ {
+ return false;
}
- if let Some(setting_model) = &self.model {
- if *setting_model != model.id().0 {
- return false;
- }
+ if let Some(setting_model) = &self.model
+ && *setting_model != model.id().0
+ {
+ return false;
}
true
}
@@ -221,7 +223,8 @@ impl AgentSettingsContent {
}
}
-#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
+#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
+#[settings_key(key = "agent", fallback_key = "assistant")]
pub struct AgentSettingsContent {
/// Whether the Agent is enabled.
///
@@ -266,6 +269,10 @@ pub struct AgentSettingsContent {
/// Whenever a tool action would normally wait for your confirmation
/// that you allow it, always choose to allow it.
///
+ /// This setting has no effect on external agents that support permission modes, such as Claude Code.
+ ///
+ /// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code.
+ ///
/// Default: false
always_allow_tool_actions: Option<bool>,
/// Where to show a popup notification when the agent is waiting for user input.
@@ -309,7 +316,7 @@ pub struct AgentSettingsContent {
///
/// Default: true
expand_terminal_card: Option<bool>,
- /// Whether to always use cmd-enter (or ctrl-enter on Linux) to send messages in the agent panel.
+ /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
///
/// Default: false
use_modifier_to_send: Option<bool>,
@@ -350,18 +357,19 @@ impl JsonSchema for LanguageModelProviderSetting {
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"enum": [
- "anthropic",
"amazon-bedrock",
+ "anthropic",
+ "copilot_chat",
+ "deepseek",
"google",
"lmstudio",
+ "mistral",
"ollama",
"openai",
- "zed.dev",
- "copilot_chat",
- "deepseek",
"openrouter",
- "mistral",
- "vercel"
+ "vercel",
+ "x_ai",
+ "zed.dev"
]
})
}
@@ -396,10 +404,6 @@ pub struct ContextServerPresetContent {
}
impl Settings for AgentSettings {
- const KEY: Option<&'static str> = Some("agent");
-
- const FALLBACK_KEY: Option<&'static str> = Some("assistant");
-
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
type FileContent = AgentSettingsContent;
@@ -503,9 +507,8 @@ impl Settings for AgentSettings {
}
}
- debug_assert_eq!(
- sources.default.always_allow_tool_actions.unwrap_or(false),
- false,
+ debug_assert!(
+ !sources.default.always_allow_tool_actions.unwrap_or(false),
"For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!"
);
@@ -25,6 +25,7 @@ agent_servers.workspace = true
agent_settings.workspace = true
ai_onboarding.workspace = true
anyhow.workspace = true
+arrayvec.workspace = true
assistant_context.workspace = true
assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true
@@ -50,7 +51,6 @@ fuzzy.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
-indexed_docs.workspace = true
indoc.workspace = true
inventory.workspace = true
itertools.workspace = true
@@ -68,6 +68,7 @@ ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
+postage.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true
@@ -80,6 +81,7 @@ serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
+shlex.workspace = true
smol.workspace = true
streaming_diff.workspace = true
task.workspace = true
@@ -103,10 +105,13 @@ workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
+acp_thread = { workspace = true, features = ["test-support"] }
agent = { workspace = true, features = ["test-support"] }
+agent2 = { workspace = true, features = ["test-support"] }
assistant_context = { workspace = true, features = ["test-support"] }
assistant_tools.workspace = true
buffer_diff = { workspace = true, features = ["test-support"] }
+db = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
@@ -1,10 +1,14 @@
mod completion_provider;
-mod message_history;
+mod entry_view_state;
+mod message_editor;
+mod mode_selector;
mod model_selector;
mod model_selector_popover;
+mod thread_history;
mod thread_view;
-pub use message_history::MessageHistory;
+pub use mode_selector::ModeSelector;
pub use model_selector::AcpModelSelector;
pub use model_selector_popover::AcpModelSelectorPopover;
+pub use thread_history::*;
pub use thread_view::AcpThreadView;
@@ -1,207 +1,44 @@
+use std::cell::{Cell, RefCell};
use std::ops::Range;
-use std::path::{Path, PathBuf};
+use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
-use acp_thread::{MentionUri, selection_name};
-use anyhow::{Context as _, Result, anyhow};
-use collections::{HashMap, HashSet};
-use editor::display_map::CreaseId;
-use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
-use file_icons::FileIcons;
-use futures::future::try_join_all;
+use acp_thread::MentionUri;
+use agent_client_protocol as acp;
+use agent2::{HistoryEntry, HistoryStore};
+use anyhow::Result;
+use editor::{CompletionProvider, Editor, ExcerptId};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
-use http_client::HttpClientWithUrl;
-use itertools::Itertools as _;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
-use parking_lot::Mutex;
+use project::lsp_store::CompletionDocumentation;
use project::{
- Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
+ Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
+ ProjectPath, Symbol, WorktreeId,
};
use prompt_store::PromptStore;
use rope::Point;
-use text::{Anchor, OffsetRangeExt as _, ToPoint as _};
+use text::{Anchor, ToPoint as _};
use ui::prelude::*;
-use url::Url;
use workspace::Workspace;
-use workspace::notifications::NotifyResultExt;
-use agent::{
- context::RULES_ICON,
- thread_store::{TextThreadStore, ThreadStore},
-};
-
-use crate::context_picker::fetch_context_picker::fetch_url_content;
+use crate::AgentPanel;
+use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
use crate::context_picker::file_context_picker::{FileMatch, search_files};
use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
use crate::context_picker::symbol_context_picker::SymbolMatch;
use crate::context_picker::symbol_context_picker::search_symbols;
-use crate::context_picker::thread_context_picker::{
- ThreadContextEntry, ThreadMatch, search_threads,
-};
use crate::context_picker::{
- ContextPickerAction, ContextPickerEntry, ContextPickerMode, RecentEntry,
- available_context_picker_entries, recent_context_picker_entries, selection_ranges,
+ ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
};
-#[derive(Default)]
-pub struct MentionSet {
- uri_by_crease_id: HashMap<CreaseId, MentionUri>,
- fetch_results: HashMap<Url, String>,
-}
-
-impl MentionSet {
- pub fn insert(&mut self, crease_id: CreaseId, uri: MentionUri) {
- self.uri_by_crease_id.insert(crease_id, uri);
- }
-
- pub fn add_fetch_result(&mut self, url: Url, content: String) {
- self.fetch_results.insert(url, content);
- }
-
- pub fn drain(&mut self) -> impl Iterator<Item = CreaseId> {
- self.fetch_results.clear();
- self.uri_by_crease_id.drain().map(|(id, _)| id)
- }
-
- pub fn contents(
- &self,
- project: Entity<Project>,
- thread_store: Entity<ThreadStore>,
- text_thread_store: Entity<TextThreadStore>,
- window: &mut Window,
- cx: &mut App,
- ) -> Task<Result<HashMap<CreaseId, Mention>>> {
- let contents = self
- .uri_by_crease_id
- .iter()
- .map(|(&crease_id, uri)| {
- match uri {
- MentionUri::File(path) => {
- let uri = uri.clone();
- let path = path.to_path_buf();
- let buffer_task = project.update(cx, |project, cx| {
- let path = project
- .find_project_path(path, cx)
- .context("Failed to find project path")?;
- anyhow::Ok(project.open_buffer(path, cx))
- });
-
- cx.spawn(async move |cx| {
- let buffer = buffer_task?.await?;
- let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
-
- anyhow::Ok((crease_id, Mention { uri, content }))
- })
- }
- MentionUri::Symbol {
- path, line_range, ..
- }
- | MentionUri::Selection {
- path, line_range, ..
- } => {
- let uri = uri.clone();
- let path_buf = path.clone();
- let line_range = line_range.clone();
-
- let buffer_task = project.update(cx, |project, cx| {
- let path = project
- .find_project_path(&path_buf, cx)
- .context("Failed to find project path")?;
- anyhow::Ok(project.open_buffer(path, cx))
- });
-
- cx.spawn(async move |cx| {
- let buffer = buffer_task?.await?;
- let content = buffer.read_with(cx, |buffer, _cx| {
- buffer
- .text_for_range(
- Point::new(line_range.start, 0)
- ..Point::new(
- line_range.end,
- buffer.line_len(line_range.end),
- ),
- )
- .collect()
- })?;
-
- anyhow::Ok((crease_id, Mention { uri, content }))
- })
- }
- MentionUri::Thread { id: thread_id, .. } => {
- let open_task = thread_store.update(cx, |thread_store, cx| {
- thread_store.open_thread(&thread_id, window, cx)
- });
-
- let uri = uri.clone();
- cx.spawn(async move |cx| {
- let thread = open_task.await?;
- let content = thread.read_with(cx, |thread, _cx| {
- thread.latest_detailed_summary_or_text().to_string()
- })?;
-
- anyhow::Ok((crease_id, Mention { uri, content }))
- })
- }
- MentionUri::TextThread { path, .. } => {
- let context = text_thread_store.update(cx, |text_thread_store, cx| {
- text_thread_store.open_local_context(path.as_path().into(), cx)
- });
- let uri = uri.clone();
- cx.spawn(async move |cx| {
- let context = context.await?;
- let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
- anyhow::Ok((crease_id, Mention { uri, content: xml }))
- })
- }
- MentionUri::Rule { id: prompt_id, .. } => {
- let Some(prompt_store) = thread_store.read(cx).prompt_store().clone()
- else {
- return Task::ready(Err(anyhow!("missing prompt store")));
- };
- let text_task = prompt_store.read(cx).load(*prompt_id, cx);
- let uri = uri.clone();
- cx.spawn(async move |_| {
- // TODO: report load errors instead of just logging
- let text = text_task.await?;
- anyhow::Ok((crease_id, Mention { uri, content: text }))
- })
- }
- MentionUri::Fetch { url } => {
- let Some(content) = self.fetch_results.get(&url) else {
- return Task::ready(Err(anyhow!("missing fetch result")));
- };
- Task::ready(Ok((
- crease_id,
- Mention {
- uri: uri.clone(),
- content: content.clone(),
- },
- )))
- }
- }
- })
- .collect::<Vec<_>>();
-
- cx.spawn(async move |_cx| {
- let contents = try_join_all(contents).await?.into_iter().collect();
- anyhow::Ok(contents)
- })
- }
-}
-
-#[derive(Debug)]
-pub struct Mention {
- pub uri: MentionUri,
- pub content: String,
-}
-
pub(crate) enum Match {
File(FileMatch),
Symbol(SymbolMatch),
- Thread(ThreadMatch),
+ Thread(HistoryEntry),
+ RecentThread(HistoryEntry),
Fetch(SharedString),
Rules(RulesContextEntry),
Entry(EntryMatch),
@@ -218,6 +55,7 @@ impl Match {
Match::File(file) => file.mat.score,
Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
Match::Thread(_) => 1.,
+ Match::RecentThread(_) => 1.,
Match::Symbol(_) => 1.,
Match::Rules(_) => 1.,
Match::Fetch(_) => 1.,
@@ -225,227 +63,44 @@ impl Match {
}
}
-fn search(
- mode: Option<ContextPickerMode>,
- query: String,
- cancellation_flag: Arc<AtomicBool>,
- recent_entries: Vec<RecentEntry>,
- prompt_store: Option<Entity<PromptStore>>,
- thread_store: WeakEntity<ThreadStore>,
- text_thread_context_store: WeakEntity<assistant_context::ContextStore>,
- workspace: Entity<Workspace>,
- cx: &mut App,
-) -> Task<Vec<Match>> {
- match mode {
- Some(ContextPickerMode::File) => {
- let search_files_task =
- search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
- cx.background_spawn(async move {
- search_files_task
- .await
- .into_iter()
- .map(Match::File)
- .collect()
- })
- }
-
- Some(ContextPickerMode::Symbol) => {
- let search_symbols_task =
- search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
- cx.background_spawn(async move {
- search_symbols_task
- .await
- .into_iter()
- .map(Match::Symbol)
- .collect()
- })
- }
-
- Some(ContextPickerMode::Thread) => {
- if let Some((thread_store, context_store)) = thread_store
- .upgrade()
- .zip(text_thread_context_store.upgrade())
- {
- let search_threads_task = search_threads(
- query.clone(),
- cancellation_flag.clone(),
- thread_store,
- context_store,
- cx,
- );
- cx.background_spawn(async move {
- search_threads_task
- .await
- .into_iter()
- .map(Match::Thread)
- .collect()
- })
- } else {
- Task::ready(Vec::new())
- }
- }
-
- Some(ContextPickerMode::Fetch) => {
- if !query.is_empty() {
- Task::ready(vec![Match::Fetch(query.into())])
- } else {
- Task::ready(Vec::new())
- }
- }
-
- Some(ContextPickerMode::Rules) => {
- if let Some(prompt_store) = prompt_store.as_ref() {
- let search_rules_task =
- search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
- cx.background_spawn(async move {
- search_rules_task
- .await
- .into_iter()
- .map(Match::Rules)
- .collect::<Vec<_>>()
- })
- } else {
- Task::ready(Vec::new())
- }
- }
-
- None => {
- if query.is_empty() {
- let mut matches = recent_entries
- .into_iter()
- .map(|entry| match entry {
- RecentEntry::File {
- project_path,
- path_prefix,
- } => Match::File(FileMatch {
- mat: fuzzy::PathMatch {
- score: 1.,
- positions: Vec::new(),
- worktree_id: project_path.worktree_id.to_usize(),
- path: project_path.path,
- path_prefix,
- is_dir: false,
- distance_to_relative_ancestor: 0,
- },
- is_recent: true,
- }),
- RecentEntry::Thread(thread_context_entry) => Match::Thread(ThreadMatch {
- thread: thread_context_entry,
- is_recent: true,
- }),
- })
- .collect::<Vec<_>>();
-
- matches.extend(
- available_context_picker_entries(
- &prompt_store,
- &Some(thread_store.clone()),
- &workspace,
- cx,
- )
- .into_iter()
- .map(|mode| {
- Match::Entry(EntryMatch {
- entry: mode,
- mat: None,
- })
- }),
- );
-
- Task::ready(matches)
- } else {
- let executor = cx.background_executor().clone();
-
- let search_files_task =
- search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
-
- let entries = available_context_picker_entries(
- &prompt_store,
- &Some(thread_store.clone()),
- &workspace,
- cx,
- );
- let entry_candidates = entries
- .iter()
- .enumerate()
- .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
- .collect::<Vec<_>>();
-
- cx.background_spawn(async move {
- let mut matches = search_files_task
- .await
- .into_iter()
- .map(Match::File)
- .collect::<Vec<_>>();
-
- let entry_matches = fuzzy::match_strings(
- &entry_candidates,
- &query,
- false,
- true,
- 100,
- &Arc::new(AtomicBool::default()),
- executor,
- )
- .await;
-
- matches.extend(entry_matches.into_iter().map(|mat| {
- Match::Entry(EntryMatch {
- entry: entries[mat.candidate_id],
- mat: Some(mat),
- })
- }));
-
- matches.sort_by(|a, b| {
- b.score()
- .partial_cmp(&a.score())
- .unwrap_or(std::cmp::Ordering::Equal)
- });
-
- matches
- })
- }
- }
- }
-}
-
pub struct ContextPickerCompletionProvider {
- mention_set: Arc<Mutex<MentionSet>>,
+ message_editor: WeakEntity<MessageEditor>,
workspace: WeakEntity<Workspace>,
- thread_store: WeakEntity<ThreadStore>,
- text_thread_store: WeakEntity<TextThreadStore>,
- editor: WeakEntity<Editor>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
+ available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
}
impl ContextPickerCompletionProvider {
pub fn new(
- mention_set: Arc<Mutex<MentionSet>>,
+ message_editor: WeakEntity<MessageEditor>,
workspace: WeakEntity<Workspace>,
- thread_store: WeakEntity<ThreadStore>,
- text_thread_store: WeakEntity<TextThreadStore>,
- editor: WeakEntity<Editor>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
+ available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
) -> Self {
Self {
- mention_set,
+ message_editor,
workspace,
- thread_store,
- text_thread_store,
- editor,
+ history_store,
+ prompt_store,
+ prompt_capabilities,
+ available_commands,
}
}
fn completion_for_entry(
entry: ContextPickerEntry,
- excerpt_id: ExcerptId,
source_range: Range<Anchor>,
- editor: Entity<Editor>,
- mention_set: Arc<Mutex<MentionSet>>,
+ message_editor: WeakEntity<MessageEditor>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
match entry {
ContextPickerEntry::Mode(mode) => Some(Completion {
- replace_range: source_range.clone(),
+ replace_range: source_range,
new_text: format!("@{} ", mode.keyword()),
label: CodeLabel::plain(mode.label().to_string(), None),
icon_path: Some(mode.icon().path().into()),
@@ -458,134 +113,26 @@ impl ContextPickerCompletionProvider {
confirm: Some(Arc::new(|_, _, _| true)),
}),
ContextPickerEntry::Action(action) => {
- let (new_text, on_action) = match action {
- ContextPickerAction::AddSelections => {
- let selections = selection_ranges(workspace, cx);
-
- const PLACEHOLDER: &str = "selection ";
-
- let new_text = std::iter::repeat(PLACEHOLDER)
- .take(selections.len())
- .chain(std::iter::once(""))
- .join(" ");
-
- let callback = Arc::new({
- let mention_set = mention_set.clone();
- let selections = selections.clone();
- move |_, window: &mut Window, cx: &mut App| {
- let editor = editor.clone();
- let mention_set = mention_set.clone();
- let selections = selections.clone();
- window.defer(cx, move |window, cx| {
- let mut current_offset = 0;
-
- for (buffer, selection_range) in selections {
- let snapshot =
- editor.read(cx).buffer().read(cx).snapshot(cx);
- let Some(start) = snapshot
- .anchor_in_excerpt(excerpt_id, source_range.start)
- else {
- return;
- };
-
- let offset = start.to_offset(&snapshot) + current_offset;
- let text_len = PLACEHOLDER.len() - 1;
-
- let range = snapshot.anchor_after(offset)
- ..snapshot.anchor_after(offset + text_len);
-
- let path = buffer
- .read(cx)
- .file()
- .map_or(PathBuf::from("untitled"), |file| {
- file.path().to_path_buf()
- });
-
- let point_range = snapshot
- .as_singleton()
- .map(|(_, _, snapshot)| {
- selection_range.to_point(&snapshot)
- })
- .unwrap_or_default();
- let line_range = point_range.start.row..point_range.end.row;
- let crease = crate::context_picker::crease_for_mention(
- selection_name(&path, &line_range).into(),
- IconName::Reader.path().into(),
- range,
- editor.downgrade(),
- );
-
- let [crease_id]: [_; 1] =
- editor.update(cx, |editor, cx| {
- let crease_ids =
- editor.insert_creases(vec![crease.clone()], cx);
- editor.fold_creases(
- vec![crease],
- false,
- window,
- cx,
- );
- crease_ids.try_into().unwrap()
- });
-
- mention_set.lock().insert(
- crease_id,
- MentionUri::Selection { path, line_range },
- );
-
- current_offset += text_len + 1;
- }
- });
-
- false
- }
- });
-
- (new_text, callback)
- }
- };
-
- Some(Completion {
- replace_range: source_range.clone(),
- new_text,
- label: CodeLabel::plain(action.label().to_string(), None),
- icon_path: Some(action.icon().path().into()),
- documentation: None,
- source: project::CompletionSource::Custom,
- insert_text_mode: None,
- // This ensures that when a user accepts this completion, the
- // completion menu will still be shown after "@category " is
- // inserted
- confirm: Some(on_action),
- })
+ Self::completion_for_action(action, source_range, message_editor, workspace, cx)
}
}
}
fn completion_for_thread(
- thread_entry: ThreadContextEntry,
- excerpt_id: ExcerptId,
+ thread_entry: HistoryEntry,
source_range: Range<Anchor>,
recent: bool,
- editor: Entity<Editor>,
- mention_set: Arc<Mutex<MentionSet>>,
+ editor: WeakEntity<MessageEditor>,
+ cx: &mut App,
) -> Completion {
+ let uri = thread_entry.mention_uri();
+
let icon_for_completion = if recent {
- IconName::HistoryRerun
+ IconName::HistoryRerun.path().into()
} else {
- IconName::Thread
+ uri.icon_path(cx)
};
- let uri = match &thread_entry {
- ThreadContextEntry::Thread { id, title } => MentionUri::Thread {
- id: id.clone(),
- name: title.to_string(),
- },
- ThreadContextEntry::Context { path, title } => MentionUri::TextThread {
- path: path.to_path_buf(),
- name: title.to_string(),
- },
- };
let new_text = format!("{} ", uri.as_link());
let new_text_len = new_text.len();
@@ -596,15 +143,12 @@ impl ContextPickerCompletionProvider {
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
- icon_path: Some(icon_for_completion.path().into()),
+ icon_path: Some(icon_for_completion),
confirm: Some(confirm_completion_callback(
- IconName::Thread.path().into(),
thread_entry.title().clone(),
- excerpt_id,
source_range.start,
new_text_len - 1,
- editor.clone(),
- mention_set,
+ editor,
uri,
)),
}
@@ -612,10 +156,9 @@ impl ContextPickerCompletionProvider {
fn completion_for_rules(
rule: RulesContextEntry,
- excerpt_id: ExcerptId,
source_range: Range<Anchor>,
- editor: Entity<Editor>,
- mention_set: Arc<Mutex<MentionSet>>,
+ editor: WeakEntity<MessageEditor>,
+ cx: &mut App,
) -> Completion {
let uri = MentionUri::Rule {
id: rule.prompt_id.into(),
@@ -623,6 +166,7 @@ impl ContextPickerCompletionProvider {
};
let new_text = format!("{} ", uri.as_link());
let new_text_len = new_text.len();
+ let icon_path = uri.icon_path(cx);
Completion {
replace_range: source_range.clone(),
new_text,
@@ -630,15 +174,12 @@ impl ContextPickerCompletionProvider {
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
- icon_path: Some(RULES_ICON.path().into()),
+ icon_path: Some(icon_path),
confirm: Some(confirm_completion_callback(
- RULES_ICON.path().into(),
- rule.title.clone(),
- excerpt_id,
+ rule.title,
source_range.start,
new_text_len - 1,
- editor.clone(),
- mention_set,
+ editor,
uri,
)),
}
@@ -649,12 +190,10 @@ impl ContextPickerCompletionProvider {
path_prefix: &str,
is_recent: bool,
is_directory: bool,
- excerpt_id: ExcerptId,
source_range: Range<Anchor>,
- editor: Entity<Editor>,
- mention_set: Arc<Mutex<MentionSet>>,
+ message_editor: WeakEntity<MessageEditor>,
project: Entity<Project>,
- cx: &App,
+ cx: &mut App,
) -> Option<Completion> {
let (file_name, directory) =
crate::context_picker::file_context_picker::extract_file_name_and_directory(
@@ -664,28 +203,23 @@ impl ContextPickerCompletionProvider {
let label =
build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
- let full_path = if let Some(directory) = directory {
- format!("{}{}", directory, file_name)
- } else {
- file_name.to_string()
- };
- let crease_icon_path = if is_directory {
- FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
+ let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
+
+ let uri = if is_directory {
+ MentionUri::Directory { abs_path }
} else {
- FileIcons::get_icon(Path::new(&full_path), cx)
- .unwrap_or_else(|| IconName::File.path().into())
+ MentionUri::File { abs_path }
};
+
+ let crease_icon_path = uri.icon_path(cx);
let completion_icon_path = if is_recent {
IconName::HistoryRerun.path().into()
} else {
- crease_icon_path.clone()
+ crease_icon_path
};
- let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
-
- let file_uri = MentionUri::File(abs_path);
- let new_text = format!("{} ", file_uri.as_link());
+ let new_text = format!("{} ", uri.as_link());
let new_text_len = new_text.len();
Some(Completion {
replace_range: source_range.clone(),
@@ -696,24 +230,19 @@ impl ContextPickerCompletionProvider {
icon_path: Some(completion_icon_path),
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
- crease_icon_path,
file_name,
- excerpt_id,
source_range.start,
new_text_len - 1,
- editor,
- mention_set.clone(),
- file_uri,
+ message_editor,
+ uri,
)),
})
}
fn completion_for_symbol(
symbol: Symbol,
- excerpt_id: ExcerptId,
source_range: Range<Anchor>,
- editor: Entity<Editor>,
- mention_set: Arc<Mutex<MentionSet>>,
+ message_editor: WeakEntity<MessageEditor>,
workspace: Entity<Workspace>,
cx: &mut App,
) -> Option<Completion> {
@@ -723,28 +252,26 @@ impl ContextPickerCompletionProvider {
let abs_path = project.read(cx).absolute_path(&symbol.path, cx)?;
let uri = MentionUri::Symbol {
- path: abs_path,
+ abs_path,
name: symbol.name.clone(),
- line_range: symbol.range.start.0.row..symbol.range.end.0.row,
+ line_range: symbol.range.start.0.row..=symbol.range.end.0.row,
};
let new_text = format!("{} ", uri.as_link());
let new_text_len = new_text.len();
+ let icon_path = uri.icon_path(cx);
Some(Completion {
replace_range: source_range.clone(),
new_text,
label,
documentation: None,
source: project::CompletionSource::Custom,
- icon_path: Some(IconName::Code.path().into()),
+ icon_path: Some(icon_path),
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
- IconName::Code.path().into(),
- symbol.name.clone().into(),
- excerpt_id,
+ symbol.name.into(),
source_range.start,
new_text_len - 1,
- editor.clone(),
- mention_set.clone(),
+ message_editor,
uri,
)),
})
@@ -753,293 +280,602 @@ impl ContextPickerCompletionProvider {
fn completion_for_fetch(
source_range: Range<Anchor>,
url_to_fetch: SharedString,
- excerpt_id: ExcerptId,
- editor: Entity<Editor>,
- mention_set: Arc<Mutex<MentionSet>>,
- http_client: Arc<HttpClientWithUrl>,
+ message_editor: WeakEntity<MessageEditor>,
+ cx: &mut App,
) -> Option<Completion> {
- let new_text = format!("@fetch {} ", url_to_fetch.clone());
- let new_text_len = new_text.len();
+ let new_text = format!("@fetch {} ", url_to_fetch);
+ let url_to_fetch = url::Url::parse(url_to_fetch.as_ref())
+ .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
+ .ok()?;
+ let mention_uri = MentionUri::Fetch {
+ url: url_to_fetch.clone(),
+ };
+ let icon_path = mention_uri.icon_path(cx);
Some(Completion {
replace_range: source_range.clone(),
- new_text,
+ new_text: new_text.clone(),
label: CodeLabel::plain(url_to_fetch.to_string(), None),
documentation: None,
source: project::CompletionSource::Custom,
- icon_path: Some(IconName::ToolWeb.path().into()),
+ icon_path: Some(icon_path),
insert_text_mode: None,
- confirm: Some({
- let start = source_range.start;
- let content_len = new_text_len - 1;
- let editor = editor.clone();
- let url_to_fetch = url_to_fetch.clone();
- let source_range = source_range.clone();
- Arc::new(move |_, window, cx| {
- let Some(url) = url::Url::parse(url_to_fetch.as_ref())
- .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
- .notify_app_err(cx)
- else {
- return false;
- };
- let mention_uri = MentionUri::Fetch { url: url.clone() };
-
- let editor = editor.clone();
- let mention_set = mention_set.clone();
- let http_client = http_client.clone();
- let source_range = source_range.clone();
- window.defer(cx, move |window, cx| {
- let url = url.clone();
+ confirm: Some(confirm_completion_callback(
+ url_to_fetch.to_string().into(),
+ source_range.start,
+ new_text.len() - 1,
+ message_editor,
+ mention_uri,
+ )),
+ })
+ }
- let Some(crease_id) = crate::context_picker::insert_crease_for_mention(
- excerpt_id,
- start,
- content_len,
- url.to_string().into(),
- IconName::ToolWeb.path().into(),
- editor.clone(),
- window,
- cx,
- ) else {
- return;
- };
+ pub(crate) fn completion_for_action(
+ action: ContextPickerAction,
+ source_range: Range<Anchor>,
+ message_editor: WeakEntity<MessageEditor>,
+ workspace: &Entity<Workspace>,
+ cx: &mut App,
+ ) -> Option<Completion> {
+ let (new_text, on_action) = match action {
+ ContextPickerAction::AddSelections => {
+ const PLACEHOLDER: &str = "selection ";
+ let selections = selection_ranges(workspace, cx)
+ .into_iter()
+ .enumerate()
+ .map(|(ix, (buffer, range))| {
+ (
+ buffer,
+ range,
+ (PLACEHOLDER.len() * ix)..(PLACEHOLDER.len() * (ix + 1) - 1),
+ )
+ })
+ .collect::<Vec<_>>();
- let editor = editor.clone();
- let mention_set = mention_set.clone();
- let http_client = http_client.clone();
+ let new_text: String = PLACEHOLDER.repeat(selections.len());
+
+ let callback = Arc::new({
+ let source_range = source_range.clone();
+ move |_, window: &mut Window, cx: &mut App| {
+ let selections = selections.clone();
+ let message_editor = message_editor.clone();
let source_range = source_range.clone();
- window
- .spawn(cx, async move |cx| {
- if let Some(content) =
- fetch_url_content(http_client, url.to_string())
- .await
- .notify_async_err(cx)
- {
- mention_set.lock().add_fetch_result(url, content);
- mention_set.lock().insert(crease_id, mention_uri.clone());
- } else {
- // Remove crease if we failed to fetch
- editor
- .update(cx, |editor, cx| {
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- let Some(anchor) = snapshot
- .anchor_in_excerpt(excerpt_id, source_range.start)
- else {
- return;
- };
- editor.display_map.update(cx, |display_map, cx| {
- display_map.unfold_intersecting(
- vec![anchor..anchor],
- true,
- cx,
- );
- });
- editor.remove_creases([crease_id], cx);
- })
- .ok();
- }
- Some(())
- })
- .detach();
- });
- false
- })
- }),
+ window.defer(cx, move |window, cx| {
+ message_editor
+ .update(cx, |message_editor, cx| {
+ message_editor.confirm_mention_for_selection(
+ source_range,
+ selections,
+ window,
+ cx,
+ )
+ })
+ .ok();
+ });
+ false
+ }
+ });
+
+ (new_text, callback)
+ }
+ };
+
+ Some(Completion {
+ replace_range: source_range,
+ new_text,
+ label: CodeLabel::plain(action.label().to_string(), None),
+ icon_path: Some(action.icon().path().into()),
+ documentation: None,
+ source: project::CompletionSource::Custom,
+ insert_text_mode: None,
+ // This ensures that when a user accepts this completion, the
+ // completion menu will still be shown after "@category " is
+ // inserted
+ confirm: Some(on_action),
})
}
-}
-fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
- let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
- let mut label = CodeLabel::default();
+ fn search_slash_commands(
+ &self,
+ query: String,
+ cx: &mut App,
+ ) -> Task<Vec<acp::AvailableCommand>> {
+ let commands = self.available_commands.borrow().clone();
+ if commands.is_empty() {
+ return Task::ready(Vec::new());
+ }
- label.push_str(&file_name, None);
- label.push_str(" ", None);
+ cx.spawn(async move |cx| {
+ let candidates = commands
+ .iter()
+ .enumerate()
+ .map(|(id, command)| StringMatchCandidate::new(id, &command.name))
+ .collect::<Vec<_>>();
+
+ let matches = fuzzy::match_strings(
+ &candidates,
+ &query,
+ false,
+ true,
+ 100,
+ &Arc::new(AtomicBool::default()),
+ cx.background_executor().clone(),
+ )
+ .await;
- if let Some(directory) = directory {
- label.push_str(&directory, comment_id);
+ matches
+ .into_iter()
+ .map(|mat| commands[mat.candidate_id].clone())
+ .collect()
+ })
}
- label.filter_range = 0..label.text().len();
-
- label
-}
-
-impl CompletionProvider for ContextPickerCompletionProvider {
- fn completions(
+ fn search_mentions(
&self,
- excerpt_id: ExcerptId,
- buffer: &Entity<Buffer>,
- buffer_position: Anchor,
- _trigger: CompletionContext,
- _window: &mut Window,
- cx: &mut Context<Editor>,
- ) -> Task<Result<Vec<CompletionResponse>>> {
- let state = buffer.update(cx, |buffer, _cx| {
- let position = buffer_position.to_point(buffer);
- let line_start = Point::new(position.row, 0);
- let offset_to_line = buffer.point_to_offset(line_start);
- let mut lines = buffer.text_for_range(line_start..position).lines();
- let line = lines.next()?;
- MentionCompletion::try_parse(line, offset_to_line)
- });
- let Some(state) = state else {
- return Task::ready(Ok(Vec::new()));
- };
-
+ mode: Option<ContextPickerMode>,
+ query: String,
+ cancellation_flag: Arc<AtomicBool>,
+ cx: &mut App,
+ ) -> Task<Vec<Match>> {
let Some(workspace) = self.workspace.upgrade() else {
- return Task::ready(Ok(Vec::new()));
+ return Task::ready(Vec::default());
};
+ match mode {
+ Some(ContextPickerMode::File) => {
+ let search_files_task = search_files(query, cancellation_flag, &workspace, cx);
+ cx.background_spawn(async move {
+ search_files_task
+ .await
+ .into_iter()
+ .map(Match::File)
+ .collect()
+ })
+ }
- let project = workspace.read(cx).project().clone();
- let http_client = workspace.read(cx).client().http_client();
- let snapshot = buffer.read(cx).snapshot();
- let source_range = snapshot.anchor_before(state.source_range.start)
- ..snapshot.anchor_after(state.source_range.end);
+ Some(ContextPickerMode::Symbol) => {
+ let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx);
+ cx.background_spawn(async move {
+ search_symbols_task
+ .await
+ .into_iter()
+ .map(Match::Symbol)
+ .collect()
+ })
+ }
- let thread_store = self.thread_store.clone();
- let text_thread_store = self.text_thread_store.clone();
- let editor = self.editor.clone();
+ Some(ContextPickerMode::Thread) => {
+ let search_threads_task =
+ search_threads(query, cancellation_flag, &self.history_store, cx);
+ cx.background_spawn(async move {
+ search_threads_task
+ .await
+ .into_iter()
+ .map(Match::Thread)
+ .collect()
+ })
+ }
- let MentionCompletion { mode, argument, .. } = state;
- let query = argument.unwrap_or_else(|| "".to_string());
+ Some(ContextPickerMode::Fetch) => {
+ if !query.is_empty() {
+ Task::ready(vec![Match::Fetch(query.into())])
+ } else {
+ Task::ready(Vec::new())
+ }
+ }
- let (exclude_paths, exclude_threads) = {
- let mention_set = self.mention_set.lock();
+ Some(ContextPickerMode::Rules) => {
+ if let Some(prompt_store) = self.prompt_store.as_ref() {
+ let search_rules_task =
+ search_rules(query, cancellation_flag, prompt_store, cx);
+ cx.background_spawn(async move {
+ search_rules_task
+ .await
+ .into_iter()
+ .map(Match::Rules)
+ .collect::<Vec<_>>()
+ })
+ } else {
+ Task::ready(Vec::new())
+ }
+ }
- let mut excluded_paths = HashSet::default();
- let mut excluded_threads = HashSet::default();
+ None if query.is_empty() => {
+ let mut matches = self.recent_context_picker_entries(&workspace, cx);
- for uri in mention_set.uri_by_crease_id.values() {
- match uri {
- MentionUri::File(path) => {
- excluded_paths.insert(path.clone());
- }
- MentionUri::Thread { id, .. } => {
- excluded_threads.insert(id.clone());
- }
- _ => {}
- }
+ matches.extend(
+ self.available_context_picker_entries(&workspace, cx)
+ .into_iter()
+ .map(|mode| {
+ Match::Entry(EntryMatch {
+ entry: mode,
+ mat: None,
+ })
+ }),
+ );
+
+ Task::ready(matches)
}
+ None => {
+ let executor = cx.background_executor().clone();
- (excluded_paths, excluded_threads)
- };
+ let search_files_task =
+ search_files(query.clone(), cancellation_flag, &workspace, cx);
- let recent_entries = recent_context_picker_entries(
- Some(thread_store.clone()),
- Some(text_thread_store.clone()),
- workspace.clone(),
- &exclude_paths,
- &exclude_threads,
- cx,
- );
+ let entries = self.available_context_picker_entries(&workspace, cx);
+ let entry_candidates = entries
+ .iter()
+ .enumerate()
+ .map(|(ix, entry)| StringMatchCandidate::new(ix, entry.keyword()))
+ .collect::<Vec<_>>();
- let prompt_store = thread_store
- .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone())
- .ok()
- .flatten();
+ cx.background_spawn(async move {
+ let mut matches = search_files_task
+ .await
+ .into_iter()
+ .map(Match::File)
+ .collect::<Vec<_>>();
- let search_task = search(
- mode,
- query,
- Arc::<AtomicBool>::default(),
- recent_entries,
- prompt_store,
- thread_store.clone(),
- text_thread_store.clone(),
- workspace.clone(),
- cx,
+ let entry_matches = fuzzy::match_strings(
+ &entry_candidates,
+ &query,
+ false,
+ true,
+ 100,
+ &Arc::new(AtomicBool::default()),
+ executor,
+ )
+ .await;
+
+ matches.extend(entry_matches.into_iter().map(|mat| {
+ Match::Entry(EntryMatch {
+ entry: entries[mat.candidate_id],
+ mat: Some(mat),
+ })
+ }));
+
+ matches.sort_by(|a, b| {
+ b.score()
+ .partial_cmp(&a.score())
+ .unwrap_or(std::cmp::Ordering::Equal)
+ });
+
+ matches
+ })
+ }
+ }
+ }
+
+ fn recent_context_picker_entries(
+ &self,
+ workspace: &Entity<Workspace>,
+ cx: &mut App,
+ ) -> Vec<Match> {
+ let mut recent = Vec::with_capacity(6);
+
+ let mut mentions = self
+ .message_editor
+ .read_with(cx, |message_editor, _cx| message_editor.mentions())
+ .unwrap_or_default();
+ let workspace = workspace.read(cx);
+ let project = workspace.project().read(cx);
+
+ if let Some(agent_panel) = workspace.panel::<AgentPanel>(cx)
+ && let Some(thread) = agent_panel.read(cx).active_agent_thread(cx)
+ {
+ let thread = thread.read(cx);
+ mentions.insert(MentionUri::Thread {
+ id: thread.session_id().clone(),
+ name: thread.title().into(),
+ });
+ }
+
+ recent.extend(
+ workspace
+ .recent_navigation_history_iter(cx)
+ .filter(|(_, abs_path)| {
+ abs_path.as_ref().is_none_or(|path| {
+ !mentions.contains(&MentionUri::File {
+ abs_path: path.clone(),
+ })
+ })
+ })
+ .take(4)
+ .filter_map(|(project_path, _)| {
+ project
+ .worktree_for_id(project_path.worktree_id, cx)
+ .map(|worktree| {
+ let path_prefix = worktree.read(cx).root_name().into();
+ Match::File(FileMatch {
+ mat: fuzzy::PathMatch {
+ score: 1.,
+ positions: Vec::new(),
+ worktree_id: project_path.worktree_id.to_usize(),
+ path: project_path.path,
+ path_prefix,
+ is_dir: false,
+ distance_to_relative_ancestor: 0,
+ },
+ is_recent: true,
+ })
+ })
+ }),
);
- let mention_set = self.mention_set.clone();
+ if self.prompt_capabilities.get().embedded_context {
+ const RECENT_COUNT: usize = 2;
+ let threads = self
+ .history_store
+ .read(cx)
+ .recently_opened_entries(cx)
+ .into_iter()
+ .filter(|thread| !mentions.contains(&thread.mention_uri()))
+ .take(RECENT_COUNT)
+ .collect::<Vec<_>>();
+
+ recent.extend(threads.into_iter().map(Match::RecentThread));
+ }
- cx.spawn(async move |_, cx| {
- let matches = search_task.await;
- let Some(editor) = editor.upgrade() else {
- return Ok(Vec::new());
- };
+ recent
+ }
- let completions = cx.update(|cx| {
- matches
- .into_iter()
- .filter_map(|mat| match mat {
- Match::File(FileMatch { mat, is_recent }) => {
- let project_path = ProjectPath {
- worktree_id: WorktreeId::from_usize(mat.worktree_id),
- path: mat.path.clone(),
+ fn available_context_picker_entries(
+ &self,
+ workspace: &Entity<Workspace>,
+ cx: &mut App,
+ ) -> Vec<ContextPickerEntry> {
+ let embedded_context = self.prompt_capabilities.get().embedded_context;
+ let mut entries = if embedded_context {
+ vec![
+ ContextPickerEntry::Mode(ContextPickerMode::File),
+ ContextPickerEntry::Mode(ContextPickerMode::Symbol),
+ ContextPickerEntry::Mode(ContextPickerMode::Thread),
+ ]
+ } else {
+ // File is always available, but we don't need a mode entry
+ vec![]
+ };
+
+ let has_selection = workspace
+ .read(cx)
+ .active_item(cx)
+ .and_then(|item| item.downcast::<Editor>())
+ .is_some_and(|editor| {
+ editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
+ });
+ if has_selection {
+ entries.push(ContextPickerEntry::Action(
+ ContextPickerAction::AddSelections,
+ ));
+ }
+
+ if embedded_context {
+ if self.prompt_store.is_some() {
+ entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
+ }
+
+ entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
+ }
+
+ entries
+ }
+}
+
+fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
+ let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
+ let mut label = CodeLabel::default();
+
+ label.push_str(file_name, None);
+ label.push_str(" ", None);
+
+ if let Some(directory) = directory {
+ label.push_str(directory, comment_id);
+ }
+
+ label.filter_range = 0..label.text().len();
+
+ label
+}
+
+impl CompletionProvider for ContextPickerCompletionProvider {
+ fn completions(
+ &self,
+ _excerpt_id: ExcerptId,
+ buffer: &Entity<Buffer>,
+ buffer_position: Anchor,
+ _trigger: CompletionContext,
+ _window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) -> Task<Result<Vec<CompletionResponse>>> {
+ let state = buffer.update(cx, |buffer, _cx| {
+ let position = buffer_position.to_point(buffer);
+ let line_start = Point::new(position.row, 0);
+ let offset_to_line = buffer.point_to_offset(line_start);
+ let mut lines = buffer.text_for_range(line_start..position).lines();
+ let line = lines.next()?;
+ ContextCompletion::try_parse(
+ line,
+ offset_to_line,
+ self.prompt_capabilities.get().embedded_context,
+ )
+ });
+ let Some(state) = state else {
+ return Task::ready(Ok(Vec::new()));
+ };
+
+ let Some(workspace) = self.workspace.upgrade() else {
+ return Task::ready(Ok(Vec::new()));
+ };
+
+ let project = workspace.read(cx).project().clone();
+ let snapshot = buffer.read(cx).snapshot();
+ let source_range = snapshot.anchor_before(state.source_range().start)
+ ..snapshot.anchor_after(state.source_range().end);
+
+ let editor = self.message_editor.clone();
+
+ match state {
+ ContextCompletion::SlashCommand(SlashCommandCompletion {
+ command, argument, ..
+ }) => {
+ let search_task = self.search_slash_commands(command.unwrap_or_default(), cx);
+ cx.background_spawn(async move {
+ let completions = search_task
+ .await
+ .into_iter()
+ .map(|command| {
+ let new_text = if let Some(argument) = argument.as_ref() {
+ format!("/{} {}", command.name, argument)
+ } else {
+ format!("/{} ", command.name)
};
- Self::completion_for_path(
- project_path,
- &mat.path_prefix,
- is_recent,
- mat.is_dir,
- excerpt_id,
- source_range.clone(),
- editor.clone(),
- mention_set.clone(),
- project.clone(),
- cx,
- )
- }
+ let is_missing_argument = argument.is_none() && command.input.is_some();
+ Completion {
+ replace_range: source_range.clone(),
+ new_text,
+ label: CodeLabel::plain(command.name.to_string(), None),
+ documentation: Some(CompletionDocumentation::MultiLinePlainText(
+ command.description.into(),
+ )),
+ source: project::CompletionSource::Custom,
+ icon_path: None,
+ insert_text_mode: None,
+ confirm: Some(Arc::new({
+ let editor = editor.clone();
+ move |intent, _window, cx| {
+ if !is_missing_argument {
+ cx.defer({
+ let editor = editor.clone();
+ move |cx| {
+ editor
+ .update(cx, |_editor, cx| {
+ match intent {
+ CompletionIntent::Complete
+ | CompletionIntent::CompleteWithInsert
+ | CompletionIntent::CompleteWithReplace => {
+ if !is_missing_argument {
+ cx.emit(MessageEditorEvent::Send);
+ }
+ }
+ CompletionIntent::Compose => {}
+ }
+ })
+ .ok();
+ }
+ });
+ }
+ is_missing_argument
+ }
+ })),
+ }
+ })
+ .collect();
- Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
- symbol,
- excerpt_id,
- source_range.clone(),
- editor.clone(),
- mention_set.clone(),
- workspace.clone(),
- cx,
- ),
-
- Match::Thread(ThreadMatch {
- thread, is_recent, ..
- }) => Some(Self::completion_for_thread(
- thread,
- excerpt_id,
- source_range.clone(),
- is_recent,
- editor.clone(),
- mention_set.clone(),
- )),
-
- Match::Rules(user_rules) => Some(Self::completion_for_rules(
- user_rules,
- excerpt_id,
- source_range.clone(),
- editor.clone(),
- mention_set.clone(),
- )),
-
- Match::Fetch(url) => Self::completion_for_fetch(
- source_range.clone(),
- url,
- excerpt_id,
- editor.clone(),
- mention_set.clone(),
- http_client.clone(),
- ),
-
- Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
- entry,
- excerpt_id,
- source_range.clone(),
- editor.clone(),
- mention_set.clone(),
- &workspace,
- cx,
- ),
- })
- .collect()
- })?;
-
- Ok(vec![CompletionResponse {
- completions,
- // Since this does its own filtering (see `filter_completions()` returns false),
- // there is no benefit to computing whether this set of completions is incomplete.
- is_incomplete: true,
- }])
- })
+ Ok(vec![CompletionResponse {
+ completions,
+ display_options: CompletionDisplayOptions {
+ dynamic_width: true,
+ },
+ // Since this does its own filtering (see `filter_completions()` returns false),
+ // there is no benefit to computing whether this set of completions is incomplete.
+ is_incomplete: true,
+ }])
+ })
+ }
+ ContextCompletion::Mention(MentionCompletion { mode, argument, .. }) => {
+ let query = argument.unwrap_or_default();
+ let search_task =
+ self.search_mentions(mode, query, Arc::<AtomicBool>::default(), cx);
+
+ cx.spawn(async move |_, cx| {
+ let matches = search_task.await;
+
+ let completions = cx.update(|cx| {
+ matches
+ .into_iter()
+ .filter_map(|mat| match mat {
+ Match::File(FileMatch { mat, is_recent }) => {
+ let project_path = ProjectPath {
+ worktree_id: WorktreeId::from_usize(mat.worktree_id),
+ path: mat.path.clone(),
+ };
+
+ Self::completion_for_path(
+ project_path,
+ &mat.path_prefix,
+ is_recent,
+ mat.is_dir,
+ source_range.clone(),
+ editor.clone(),
+ project.clone(),
+ cx,
+ )
+ }
+
+ Match::Symbol(SymbolMatch { symbol, .. }) => {
+ Self::completion_for_symbol(
+ symbol,
+ source_range.clone(),
+ editor.clone(),
+ workspace.clone(),
+ cx,
+ )
+ }
+
+ Match::Thread(thread) => Some(Self::completion_for_thread(
+ thread,
+ source_range.clone(),
+ false,
+ editor.clone(),
+ cx,
+ )),
+
+ Match::RecentThread(thread) => Some(Self::completion_for_thread(
+ thread,
+ source_range.clone(),
+ true,
+ editor.clone(),
+ cx,
+ )),
+
+ Match::Rules(user_rules) => Some(Self::completion_for_rules(
+ user_rules,
+ source_range.clone(),
+ editor.clone(),
+ cx,
+ )),
+
+ Match::Fetch(url) => Self::completion_for_fetch(
+ source_range.clone(),
+ url,
+ editor.clone(),
+ cx,
+ ),
+
+ Match::Entry(EntryMatch { entry, .. }) => {
+ Self::completion_for_entry(
+ entry,
+ source_range.clone(),
+ editor.clone(),
+ &workspace,
+ cx,
+ )
+ }
+ })
+ .collect()
+ })?;
+
+ Ok(vec![CompletionResponse {
+ completions,
+ display_options: CompletionDisplayOptions {
+ dynamic_width: true,
+ },
+ // Since this does its own filtering (see `filter_completions()` returns false),
+ // there is no benefit to computing whether this set of completions is incomplete.
+ is_incomplete: true,
+ }])
+ })
+ }
+ }
}
fn is_completion_trigger(
@@ -0,0 +1,554 @@
+use std::{
+ cell::{Cell, RefCell},
+ ops::Range,
+ rc::Rc,
+};
+
+use acp_thread::{AcpThread, AgentThreadEntry};
+use agent_client_protocol::{self as acp, ToolCallId};
+use agent2::HistoryStore;
+use collections::HashMap;
+use editor::{Editor, EditorMode, MinimapVisibility};
+use gpui::{
+ AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
+ ScrollHandle, SharedString, TextStyleRefinement, WeakEntity, Window,
+};
+use language::language_settings::SoftWrap;
+use project::Project;
+use prompt_store::PromptStore;
+use settings::Settings as _;
+use terminal_view::TerminalView;
+use theme::ThemeSettings;
+use ui::{Context, TextSize};
+use workspace::Workspace;
+
+use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
+
+pub struct EntryViewState {
+ workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ entries: Vec<Entry>,
+ prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
+ available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+ agent_name: SharedString,
+}
+
+impl EntryViewState {
+ pub fn new(
+ workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
+ available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+ agent_name: SharedString,
+ ) -> Self {
+ Self {
+ workspace,
+ project,
+ history_store,
+ prompt_store,
+ entries: Vec::new(),
+ prompt_capabilities,
+ available_commands,
+ agent_name,
+ }
+ }
+
+ pub fn entry(&self, index: usize) -> Option<&Entry> {
+ self.entries.get(index)
+ }
+
+ pub fn sync_entry(
+ &mut self,
+ index: usize,
+ thread: &Entity<AcpThread>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(thread_entry) = thread.read(cx).entries().get(index) else {
+ return;
+ };
+
+ match thread_entry {
+ AgentThreadEntry::UserMessage(message) => {
+ let has_id = message.id.is_some();
+ let chunks = message.chunks.clone();
+ if let Some(Entry::UserMessage(editor)) = self.entries.get_mut(index) {
+ if !editor.focus_handle(cx).is_focused(window) {
+ // Only update if we are not editing.
+ // If we are, cancelling the edit will set the message to the newest content.
+ editor.update(cx, |editor, cx| {
+ editor.set_message(chunks, window, cx);
+ });
+ }
+ } else {
+ let message_editor = cx.new(|cx| {
+ let mut editor = MessageEditor::new(
+ self.workspace.clone(),
+ self.project.clone(),
+ self.history_store.clone(),
+ self.prompt_store.clone(),
+ self.prompt_capabilities.clone(),
+ self.available_commands.clone(),
+ self.agent_name.clone(),
+ "Edit message - @ to include context",
+ editor::EditorMode::AutoHeight {
+ min_lines: 1,
+ max_lines: None,
+ },
+ window,
+ cx,
+ );
+ if !has_id {
+ editor.set_read_only(true, cx);
+ }
+ editor.set_message(chunks, window, cx);
+ editor
+ });
+ cx.subscribe(&message_editor, move |_, editor, event, cx| {
+ cx.emit(EntryViewEvent {
+ entry_index: index,
+ view_event: ViewEvent::MessageEditorEvent(editor, *event),
+ })
+ })
+ .detach();
+ self.set_entry(index, Entry::UserMessage(message_editor));
+ }
+ }
+ AgentThreadEntry::ToolCall(tool_call) => {
+ let id = tool_call.id.clone();
+ let terminals = tool_call.terminals().cloned().collect::<Vec<_>>();
+ let diffs = tool_call.diffs().cloned().collect::<Vec<_>>();
+
+ let views = if let Some(Entry::Content(views)) = self.entries.get_mut(index) {
+ views
+ } else {
+ self.set_entry(index, Entry::empty());
+ let Some(Entry::Content(views)) = self.entries.get_mut(index) else {
+ unreachable!()
+ };
+ views
+ };
+
+ let is_tool_call_completed =
+ matches!(tool_call.status, acp_thread::ToolCallStatus::Completed);
+
+ for terminal in terminals {
+ match views.entry(terminal.entity_id()) {
+ collections::hash_map::Entry::Vacant(entry) => {
+ let element = create_terminal(
+ self.workspace.clone(),
+ self.project.clone(),
+ terminal.clone(),
+ window,
+ cx,
+ )
+ .into_any();
+ cx.emit(EntryViewEvent {
+ entry_index: index,
+ view_event: ViewEvent::NewTerminal(id.clone()),
+ });
+ entry.insert(element);
+ }
+ collections::hash_map::Entry::Occupied(_entry) => {
+ if is_tool_call_completed && terminal.read(cx).output().is_none() {
+ cx.emit(EntryViewEvent {
+ entry_index: index,
+ view_event: ViewEvent::TerminalMovedToBackground(id.clone()),
+ });
+ }
+ }
+ }
+ }
+
+ for diff in diffs {
+ views.entry(diff.entity_id()).or_insert_with(|| {
+ let element = create_editor_diff(diff.clone(), window, cx).into_any();
+ cx.emit(EntryViewEvent {
+ entry_index: index,
+ view_event: ViewEvent::NewDiff(id.clone()),
+ });
+ element
+ });
+ }
+ }
+ AgentThreadEntry::AssistantMessage(message) => {
+ let entry = if let Some(Entry::AssistantMessage(entry)) =
+ self.entries.get_mut(index)
+ {
+ entry
+ } else {
+ self.set_entry(
+ index,
+ Entry::AssistantMessage(AssistantMessageEntry::default()),
+ );
+ let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
+ unreachable!()
+ };
+ entry
+ };
+ entry.sync(message);
+ }
+ };
+ }
+
+ fn set_entry(&mut self, index: usize, entry: Entry) {
+ if index == self.entries.len() {
+ self.entries.push(entry);
+ } else {
+ self.entries[index] = entry;
+ }
+ }
+
+ pub fn remove(&mut self, range: Range<usize>) {
+ self.entries.drain(range);
+ }
+
+ pub fn agent_font_size_changed(&mut self, cx: &mut App) {
+ for entry in self.entries.iter() {
+ match entry {
+ Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
+ Entry::Content(response_views) => {
+ for view in response_views.values() {
+ if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
+ diff_editor.update(cx, |diff_editor, cx| {
+ diff_editor.set_text_style_refinement(
+ diff_editor_text_style_refinement(cx),
+ );
+ cx.notify();
+ })
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+impl EventEmitter<EntryViewEvent> for EntryViewState {}
+
+pub struct EntryViewEvent {
+ pub entry_index: usize,
+ pub view_event: ViewEvent,
+}
+
+pub enum ViewEvent {
+ NewDiff(ToolCallId),
+ NewTerminal(ToolCallId),
+ TerminalMovedToBackground(ToolCallId),
+ MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
+}
+
+#[derive(Default, Debug)]
+pub struct AssistantMessageEntry {
+ scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,
+}
+
+impl AssistantMessageEntry {
+ pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> {
+ self.scroll_handles_by_chunk_index.get(&ix).cloned()
+ }
+
+ pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
+ if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
+ let ix = message.chunks.len() - 1;
+ let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
+ handle.scroll_to_bottom();
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum Entry {
+ UserMessage(Entity<MessageEditor>),
+ AssistantMessage(AssistantMessageEntry),
+ Content(HashMap<EntityId, AnyEntity>),
+}
+
+impl Entry {
+ pub fn focus_handle(&self, cx: &App) -> Option<FocusHandle> {
+ match self {
+ Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)),
+ Self::AssistantMessage(_) | Self::Content(_) => None,
+ }
+ }
+
+ pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
+ match self {
+ Self::UserMessage(editor) => Some(editor),
+ Self::AssistantMessage(_) | Self::Content(_) => None,
+ }
+ }
+
+ pub fn editor_for_diff(&self, diff: &Entity<acp_thread::Diff>) -> Option<Entity<Editor>> {
+ self.content_map()?
+ .get(&diff.entity_id())
+ .cloned()
+ .map(|entity| entity.downcast::<Editor>().unwrap())
+ }
+
+ pub fn terminal(
+ &self,
+ terminal: &Entity<acp_thread::Terminal>,
+ ) -> Option<Entity<TerminalView>> {
+ self.content_map()?
+ .get(&terminal.entity_id())
+ .cloned()
+ .map(|entity| entity.downcast::<TerminalView>().unwrap())
+ }
+
+ pub fn scroll_handle_for_assistant_message_chunk(
+ &self,
+ chunk_ix: usize,
+ ) -> Option<ScrollHandle> {
+ match self {
+ Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
+ Self::UserMessage(_) | Self::Content(_) => None,
+ }
+ }
+
+ fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
+ match self {
+ Self::Content(map) => Some(map),
+ _ => None,
+ }
+ }
+
+ fn empty() -> Self {
+ Self::Content(HashMap::default())
+ }
+
+ #[cfg(test)]
+ pub fn has_content(&self) -> bool {
+ match self {
+ Self::Content(map) => !map.is_empty(),
+ Self::UserMessage(_) | Self::AssistantMessage(_) => false,
+ }
+ }
+}
+
+fn create_terminal(
+ workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
+ terminal: Entity<acp_thread::Terminal>,
+ window: &mut Window,
+ cx: &mut App,
+) -> Entity<TerminalView> {
+ cx.new(|cx| {
+ let mut view = TerminalView::new(
+ terminal.read(cx).inner().clone(),
+ workspace.clone(),
+ None,
+ project.downgrade(),
+ window,
+ cx,
+ );
+ view.set_embedded_mode(Some(1000), cx);
+ view
+ })
+}
+
+fn create_editor_diff(
+ diff: Entity<acp_thread::Diff>,
+ window: &mut Window,
+ cx: &mut App,
+) -> Entity<Editor> {
+ cx.new(|cx| {
+ let mut editor = Editor::new(
+ EditorMode::Full {
+ scale_ui_elements_with_buffer_font_size: false,
+ show_active_line_background: false,
+ sized_by_content: true,
+ },
+ diff.read(cx).multibuffer().clone(),
+ None,
+ window,
+ cx,
+ );
+ editor.set_show_gutter(false, cx);
+ editor.disable_inline_diagnostics();
+ editor.disable_expand_excerpt_buttons(cx);
+ editor.set_show_vertical_scrollbar(false, cx);
+ editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
+ editor.set_soft_wrap_mode(SoftWrap::None, cx);
+ editor.scroll_manager.set_forbid_vertical_scroll(true);
+ editor.set_show_indent_guides(false, cx);
+ editor.set_read_only(true);
+ editor.set_show_breakpoints(false, cx);
+ editor.set_show_code_actions(false, cx);
+ editor.set_show_git_diff_gutter(false, cx);
+ editor.set_expand_all_diff_hunks(cx);
+ editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
+ editor
+ })
+}
+
+fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
+ TextStyleRefinement {
+ font_size: Some(
+ TextSize::Small
+ .rems(cx)
+ .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
+ .into(),
+ ),
+ ..Default::default()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{path::Path, rc::Rc};
+
+ use acp_thread::{AgentConnection, StubAgentConnection};
+ use agent_client_protocol as acp;
+ use agent_settings::AgentSettings;
+ use agent2::HistoryStore;
+ use assistant_context::ContextStore;
+ use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
+ use editor::{EditorSettings, RowInfo};
+ use fs::FakeFs;
+ use gpui::{AppContext as _, SemanticVersion, TestAppContext};
+
+ use crate::acp::entry_view_state::EntryViewState;
+ use multi_buffer::MultiBufferRow;
+ use pretty_assertions::assert_matches;
+ use project::Project;
+ use serde_json::json;
+ use settings::{Settings as _, SettingsStore};
+ use theme::ThemeSettings;
+ use util::path;
+ use workspace::Workspace;
+
+ #[gpui::test]
+ async fn test_diff_sync(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/project",
+ json!({
+ "hello.txt": "hi world"
+ }),
+ )
+ .await;
+ let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+ let tool_call = acp::ToolCall {
+ id: acp::ToolCallId("tool".into()),
+ title: "Tool call".into(),
+ kind: acp::ToolKind::Other,
+ status: acp::ToolCallStatus::InProgress,
+ content: vec![acp::ToolCallContent::Diff {
+ diff: acp::Diff {
+ path: "/project/hello.txt".into(),
+ old_text: Some("hi world".into()),
+ new_text: "hello world".into(),
+ },
+ }],
+ locations: vec![],
+ raw_input: None,
+ raw_output: None,
+ };
+ let connection = Rc::new(StubAgentConnection::new());
+ let thread = cx
+ .update(|_, cx| {
+ connection
+ .clone()
+ .new_thread(project.clone(), Path::new(path!("/project")), cx)
+ })
+ .await
+ .unwrap();
+ let session_id = thread.update(cx, |thread, _| thread.session_id().clone());
+
+ cx.update(|_, cx| {
+ connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx)
+ });
+
+ let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
+ let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
+
+ let view_state = cx.new(|_cx| {
+ EntryViewState::new(
+ workspace.downgrade(),
+ project.clone(),
+ history_store,
+ None,
+ Default::default(),
+ Default::default(),
+ "Test Agent".into(),
+ )
+ });
+
+ view_state.update_in(cx, |view_state, window, cx| {
+ view_state.sync_entry(0, &thread, window, cx)
+ });
+
+ let diff = thread.read_with(cx, |thread, _cx| {
+ thread
+ .entries()
+ .get(0)
+ .unwrap()
+ .diffs()
+ .next()
+ .unwrap()
+ .clone()
+ });
+
+ cx.run_until_parked();
+
+ let diff_editor = view_state.read_with(cx, |view_state, _cx| {
+ view_state.entry(0).unwrap().editor_for_diff(&diff).unwrap()
+ });
+ assert_eq!(
+ diff_editor.read_with(cx, |editor, cx| editor.text(cx)),
+ "hi world\nhello world"
+ );
+ let row_infos = diff_editor.read_with(cx, |editor, cx| {
+ let multibuffer = editor.buffer().read(cx);
+ multibuffer
+ .snapshot(cx)
+ .row_infos(MultiBufferRow(0))
+ .collect::<Vec<_>>()
+ });
+ assert_matches!(
+ row_infos.as_slice(),
+ [
+ RowInfo {
+ multibuffer_row: Some(MultiBufferRow(0)),
+ diff_status: Some(DiffHunkStatus {
+ kind: DiffHunkStatusKind::Deleted,
+ ..
+ }),
+ ..
+ },
+ RowInfo {
+ multibuffer_row: Some(MultiBufferRow(1)),
+ diff_status: Some(DiffHunkStatus {
+ kind: DiffHunkStatusKind::Added,
+ ..
+ }),
+ ..
+ }
+ ]
+ );
+ }
+
+ fn init_test(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ language::init(cx);
+ Project::init_settings(cx);
+ AgentSettings::register(cx);
+ workspace::init_settings(cx);
+ ThemeSettings::register(cx);
+ release_channel::init(SemanticVersion::default(), cx);
+ EditorSettings::register(cx);
+ });
+ }
+}
@@ -0,0 +1,2587 @@
+use crate::{
+ acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion},
+ context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
+};
+use acp_thread::{MentionUri, selection_name};
+use agent_client_protocol as acp;
+use agent_servers::{AgentServer, AgentServerDelegate};
+use agent2::HistoryStore;
+use anyhow::{Result, anyhow};
+use assistant_slash_commands::codeblock_fence_for_path;
+use collections::{HashMap, HashSet};
+use editor::{
+ Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
+ EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
+ MultiBuffer, ToOffset,
+ actions::Paste,
+ display_map::{Crease, CreaseId, FoldId, Inlay},
+};
+use futures::{
+ FutureExt as _,
+ future::{Shared, join_all},
+};
+use gpui::{
+ Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
+ EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
+ Subscription, Task, TextStyle, WeakEntity, pulsating_between,
+};
+use language::{Buffer, Language, language_settings::InlayHintKind};
+use language_model::LanguageModelImage;
+use postage::stream::Stream as _;
+use project::{
+ CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
+};
+use prompt_store::{PromptId, PromptStore};
+use rope::Point;
+use settings::Settings;
+use std::{
+ cell::{Cell, RefCell},
+ ffi::OsStr,
+ fmt::Write,
+ ops::{Range, RangeInclusive},
+ path::{Path, PathBuf},
+ rc::Rc,
+ sync::Arc,
+ time::Duration,
+};
+use text::OffsetRangeExt;
+use theme::ThemeSettings;
+use ui::{
+ ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _,
+ FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label,
+ LabelCommon, LabelSize, ParentElement, Render, SelectableButton, Styled, TextSize, TintColor,
+ Toggleable, Window, div, h_flex,
+};
+use util::{ResultExt, debug_panic};
+use workspace::{Workspace, notifications::NotifyResultExt as _};
+use zed_actions::agent::Chat;
+
+pub struct MessageEditor {
+ mention_set: MentionSet,
+ editor: Entity<Editor>,
+ project: Entity<Project>,
+ workspace: WeakEntity<Workspace>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
+ available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+ agent_name: SharedString,
+ _subscriptions: Vec<Subscription>,
+ _parse_slash_command_task: Task<()>,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum MessageEditorEvent {
+ Send,
+ Cancel,
+ Focus,
+ LostFocus,
+}
+
+impl EventEmitter<MessageEditorEvent> for MessageEditor {}
+
+const COMMAND_HINT_INLAY_ID: usize = 0;
+
+impl MessageEditor {
+ pub fn new(
+ workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
+ prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
+ available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+ agent_name: SharedString,
+ placeholder: &str,
+ mode: EditorMode,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let language = Language::new(
+ language::LanguageConfig {
+ completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
+ ..Default::default()
+ },
+ None,
+ );
+ let completion_provider = Rc::new(ContextPickerCompletionProvider::new(
+ cx.weak_entity(),
+ workspace.clone(),
+ history_store.clone(),
+ prompt_store.clone(),
+ prompt_capabilities.clone(),
+ available_commands.clone(),
+ ));
+ let mention_set = MentionSet::default();
+ let editor = cx.new(|cx| {
+ let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
+ let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+
+ let mut editor = Editor::new(mode, buffer, None, window, cx);
+ editor.set_placeholder_text(placeholder, window, cx);
+ editor.set_show_indent_guides(false, cx);
+ editor.set_soft_wrap();
+ editor.set_use_modal_editing(true);
+ editor.set_completion_provider(Some(completion_provider.clone()));
+ editor.set_context_menu_options(ContextMenuOptions {
+ min_entries_visible: 12,
+ max_entries_visible: 12,
+ placement: Some(ContextMenuPlacement::Above),
+ });
+ editor.register_addon(MessageEditorAddon::new());
+ editor
+ });
+
+ cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
+ cx.emit(MessageEditorEvent::Focus)
+ })
+ .detach();
+ cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| {
+ cx.emit(MessageEditorEvent::LostFocus)
+ })
+ .detach();
+
+ let mut has_hint = false;
+ let mut subscriptions = Vec::new();
+
+ subscriptions.push(cx.subscribe_in(&editor, window, {
+ move |this, editor, event, window, cx| {
+ if let EditorEvent::Edited { .. } = event {
+ let snapshot = editor.update(cx, |editor, cx| {
+ let new_hints = this
+ .command_hint(editor.buffer(), cx)
+ .into_iter()
+ .collect::<Vec<_>>();
+ let has_new_hint = !new_hints.is_empty();
+ editor.splice_inlays(
+ if has_hint {
+ &[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
+ } else {
+ &[]
+ },
+ new_hints,
+ cx,
+ );
+ has_hint = has_new_hint;
+
+ editor.snapshot(window, cx)
+ });
+ this.mention_set.remove_invalid(snapshot);
+
+ cx.notify();
+ }
+ }
+ }));
+
+ Self {
+ editor,
+ project,
+ mention_set,
+ workspace,
+ history_store,
+ prompt_store,
+ prompt_capabilities,
+ available_commands,
+ agent_name,
+ _subscriptions: subscriptions,
+ _parse_slash_command_task: Task::ready(()),
+ }
+ }
+
+ fn command_hint(&self, buffer: &Entity<MultiBuffer>, cx: &App) -> Option<Inlay> {
+ let available_commands = self.available_commands.borrow();
+ if available_commands.is_empty() {
+ return None;
+ }
+
+ let snapshot = buffer.read(cx).snapshot(cx);
+ let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
+ if parsed_command.argument.is_some() {
+ return None;
+ }
+
+ let command_name = parsed_command.command?;
+ let available_command = available_commands
+ .iter()
+ .find(|command| command.name == command_name)?;
+
+ let acp::AvailableCommandInput::Unstructured { mut hint } =
+ available_command.input.clone()?;
+
+ let mut hint_pos = parsed_command.source_range.end + 1;
+ if hint_pos > snapshot.len() {
+ hint_pos = snapshot.len();
+ hint.insert(0, ' ');
+ }
+
+ let hint_pos = snapshot.anchor_after(hint_pos);
+
+ Some(Inlay::hint(
+ COMMAND_HINT_INLAY_ID,
+ hint_pos,
+ &InlayHint {
+ position: hint_pos.text_anchor,
+ label: InlayHintLabel::String(hint),
+ kind: Some(InlayHintKind::Parameter),
+ padding_left: false,
+ padding_right: false,
+ tooltip: None,
+ resolve_state: project::ResolveState::Resolved,
+ },
+ ))
+ }
+
+ pub fn insert_thread_summary(
+ &mut self,
+ thread: agent2::DbThreadMetadata,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let start = self.editor.update(cx, |editor, cx| {
+ editor.set_text(format!("{}\n", thread.title), window, cx);
+ editor
+ .buffer()
+ .read(cx)
+ .snapshot(cx)
+ .anchor_before(Point::zero())
+ .text_anchor
+ });
+
+ self.confirm_mention_completion(
+ thread.title.clone(),
+ start,
+ thread.title.len(),
+ MentionUri::Thread {
+ id: thread.id.clone(),
+ name: thread.title.to_string(),
+ },
+ window,
+ cx,
+ )
+ .detach();
+ }
+
+ #[cfg(test)]
+ pub(crate) fn editor(&self) -> &Entity<Editor> {
+ &self.editor
+ }
+
+ #[cfg(test)]
+ pub(crate) fn mention_set(&mut self) -> &mut MentionSet {
+ &mut self.mention_set
+ }
+
+ pub fn is_empty(&self, cx: &App) -> bool {
+ self.editor.read(cx).is_empty(cx)
+ }
+
+ pub fn mentions(&self) -> HashSet<MentionUri> {
+ self.mention_set
+ .mentions
+ .values()
+ .map(|(uri, _)| uri.clone())
+ .collect()
+ }
+
+ pub fn confirm_mention_completion(
+ &mut self,
+ crease_text: SharedString,
+ start: text::Anchor,
+ content_len: usize,
+ mention_uri: MentionUri,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Task<()> {
+ let snapshot = self
+ .editor
+ .update(cx, |editor, cx| editor.snapshot(window, cx));
+ let Some((excerpt_id, _, _)) = snapshot.buffer_snapshot.as_singleton() else {
+ return Task::ready(());
+ };
+ let Some(start_anchor) = snapshot
+ .buffer_snapshot
+ .anchor_in_excerpt(*excerpt_id, start)
+ else {
+ return Task::ready(());
+ };
+ let end_anchor = snapshot
+ .buffer_snapshot
+ .anchor_before(start_anchor.to_offset(&snapshot.buffer_snapshot) + content_len + 1);
+
+ let crease = if let MentionUri::File { abs_path } = &mention_uri
+ && let Some(extension) = abs_path.extension()
+ && let Some(extension) = extension.to_str()
+ && Img::extensions().contains(&extension)
+ && !extension.contains("svg")
+ {
+ let Some(project_path) = self
+ .project
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ log::error!("project path not found");
+ return Task::ready(());
+ };
+ let image = self
+ .project
+ .update(cx, |project, cx| project.open_image(project_path, cx));
+ let image = cx
+ .spawn(async move |_, cx| {
+ let image = image.await.map_err(|e| e.to_string())?;
+ let image = image
+ .update(cx, |image, _| image.image.clone())
+ .map_err(|e| e.to_string())?;
+ Ok(image)
+ })
+ .shared();
+ insert_crease_for_mention(
+ *excerpt_id,
+ start,
+ content_len,
+ mention_uri.name().into(),
+ IconName::Image.path().into(),
+ Some(image),
+ self.editor.clone(),
+ window,
+ cx,
+ )
+ } else {
+ insert_crease_for_mention(
+ *excerpt_id,
+ start,
+ content_len,
+ crease_text,
+ mention_uri.icon_path(cx),
+ None,
+ self.editor.clone(),
+ window,
+ cx,
+ )
+ };
+ let Some((crease_id, tx)) = crease else {
+ return Task::ready(());
+ };
+
+ let task = match mention_uri.clone() {
+ MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx),
+ MentionUri::Directory { abs_path } => self.confirm_mention_for_directory(abs_path, cx),
+ MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
+ MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
+ MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx),
+ MentionUri::Symbol {
+ abs_path,
+ line_range,
+ ..
+ } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
+ MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
+ MentionUri::PastedImage => {
+ debug_panic!("pasted image URI should not be included in completions");
+ Task::ready(Err(anyhow!(
+ "pasted imaged URI should not be included in completions"
+ )))
+ }
+ MentionUri::Selection { .. } => {
+ // Handled elsewhere
+ debug_panic!("unexpected selection URI");
+ Task::ready(Err(anyhow!("unexpected selection URI")))
+ }
+ };
+ let task = cx
+ .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
+ .shared();
+ self.mention_set
+ .mentions
+ .insert(crease_id, (mention_uri, task.clone()));
+
+ // Notify the user if we failed to load the mentioned context
+ cx.spawn_in(window, async move |this, cx| {
+ let result = task.await.notify_async_err(cx);
+ drop(tx);
+ if result.is_none() {
+ this.update(cx, |this, cx| {
+ this.editor.update(cx, |editor, cx| {
+ // Remove mention
+ editor.edit([(start_anchor..end_anchor, "")], cx);
+ });
+ this.mention_set.mentions.remove(&crease_id);
+ })
+ .ok();
+ }
+ })
+ }
+
+ fn confirm_mention_for_file(
+ &mut self,
+ abs_path: PathBuf,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let Some(project_path) = self
+ .project
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ return Task::ready(Err(anyhow!("project path not found")));
+ };
+ let extension = abs_path
+ .extension()
+ .and_then(OsStr::to_str)
+ .unwrap_or_default();
+
+ if Img::extensions().contains(&extension) && !extension.contains("svg") {
+ if !self.prompt_capabilities.get().image {
+ return Task::ready(Err(anyhow!("This model does not support images yet")));
+ }
+ let task = self
+ .project
+ .update(cx, |project, cx| project.open_image(project_path, cx));
+ return cx.spawn(async move |_, cx| {
+ let image = task.await?;
+ let image = image.update(cx, |image, _| image.image.clone())?;
+ let format = image.format;
+ let image = cx
+ .update(|cx| LanguageModelImage::from_image(image, cx))?
+ .await;
+ if let Some(image) = image {
+ Ok(Mention::Image(MentionImage {
+ data: image.source,
+ format,
+ }))
+ } else {
+ Err(anyhow!("Failed to convert image"))
+ }
+ });
+ }
+
+ let buffer = self
+ .project
+ .update(cx, |project, cx| project.open_buffer(project_path, cx));
+ cx.spawn(async move |_, cx| {
+ let buffer = buffer.await?;
+ let mention = buffer.update(cx, |buffer, cx| Mention::Text {
+ content: buffer.text(),
+ tracked_buffers: vec![cx.entity()],
+ })?;
+ anyhow::Ok(mention)
+ })
+ }
+
+ fn confirm_mention_for_directory(
+ &mut self,
+ abs_path: PathBuf,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> {
+ let mut files = Vec::new();
+
+ for entry in worktree.child_entries(path) {
+ if entry.is_dir() {
+ files.extend(collect_files_in_path(worktree, &entry.path));
+ } else if entry.is_file() {
+ files.push((entry.path.clone(), worktree.full_path(&entry.path)));
+ }
+ }
+
+ files
+ }
+
+ let Some(project_path) = self
+ .project
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ return Task::ready(Err(anyhow!("project path not found")));
+ };
+ let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
+ return Task::ready(Err(anyhow!("project entry not found")));
+ };
+ let directory_path = entry.path.clone();
+ let worktree_id = project_path.worktree_id;
+ let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) else {
+ return Task::ready(Err(anyhow!("worktree not found")));
+ };
+ let project = self.project.clone();
+ cx.spawn(async move |_, cx| {
+ let file_paths = worktree.read_with(cx, |worktree, _cx| {
+ collect_files_in_path(worktree, &directory_path)
+ })?;
+ let descendants_future = cx.update(|cx| {
+ join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
+ let rel_path = worktree_path
+ .strip_prefix(&directory_path)
+ .log_err()
+ .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
+
+ let open_task = project.update(cx, |project, cx| {
+ project.buffer_store().update(cx, |buffer_store, cx| {
+ let project_path = ProjectPath {
+ worktree_id,
+ path: worktree_path,
+ };
+ buffer_store.open_buffer(project_path, cx)
+ })
+ });
+
+ // TODO: report load errors instead of just logging
+ let rope_task = cx.spawn(async move |cx| {
+ let buffer = open_task.await.log_err()?;
+ let rope = buffer
+ .read_with(cx, |buffer, _cx| buffer.as_rope().clone())
+ .log_err()?;
+ Some((rope, buffer))
+ });
+
+ cx.background_spawn(async move {
+ let (rope, buffer) = rope_task.await?;
+ Some((rel_path, full_path, rope.to_string(), buffer))
+ })
+ }))
+ })?;
+
+ let contents = cx
+ .background_spawn(async move {
+ let (contents, tracked_buffers) = descendants_future
+ .await
+ .into_iter()
+ .flatten()
+ .map(|(rel_path, full_path, rope, buffer)| {
+ ((rel_path, full_path, rope), buffer)
+ })
+ .unzip();
+ Mention::Text {
+ content: render_directory_contents(contents),
+ tracked_buffers,
+ }
+ })
+ .await;
+ anyhow::Ok(contents)
+ })
+ }
+
+ fn confirm_mention_for_fetch(
+ &mut self,
+ url: url::Url,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let http_client = match self
+ .workspace
+ .update(cx, |workspace, _| workspace.client().http_client())
+ {
+ Ok(http_client) => http_client,
+ Err(e) => return Task::ready(Err(e)),
+ };
+ cx.background_executor().spawn(async move {
+ let content = fetch_url_content(http_client, url.to_string()).await?;
+ Ok(Mention::Text {
+ content,
+ tracked_buffers: Vec::new(),
+ })
+ })
+ }
+
+ fn confirm_mention_for_symbol(
+ &mut self,
+ abs_path: PathBuf,
+ line_range: RangeInclusive<u32>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let Some(project_path) = self
+ .project
+ .read(cx)
+ .project_path_for_absolute_path(&abs_path, cx)
+ else {
+ return Task::ready(Err(anyhow!("project path not found")));
+ };
+ let buffer = self
+ .project
+ .update(cx, |project, cx| project.open_buffer(project_path, cx));
+ cx.spawn(async move |_, cx| {
+ let buffer = buffer.await?;
+ let mention = buffer.update(cx, |buffer, cx| {
+ let start = Point::new(*line_range.start(), 0).min(buffer.max_point());
+ let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());
+ let content = buffer.text_for_range(start..end).collect();
+ Mention::Text {
+ content,
+ tracked_buffers: vec![cx.entity()],
+ }
+ })?;
+ anyhow::Ok(mention)
+ })
+ }
+
+ fn confirm_mention_for_rule(
+ &mut self,
+ id: PromptId,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let Some(prompt_store) = self.prompt_store.clone() else {
+ return Task::ready(Err(anyhow!("missing prompt store")));
+ };
+ let prompt = prompt_store.read(cx).load(id, cx);
+ cx.spawn(async move |_, _| {
+ let prompt = prompt.await?;
+ Ok(Mention::Text {
+ content: prompt,
+ tracked_buffers: Vec::new(),
+ })
+ })
+ }
+
+ pub fn confirm_mention_for_selection(
+ &mut self,
+ source_range: Range<text::Anchor>,
+ selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
+ let Some((&excerpt_id, _, _)) = snapshot.as_singleton() else {
+ return;
+ };
+ let Some(start) = snapshot.anchor_in_excerpt(excerpt_id, source_range.start) else {
+ return;
+ };
+
+ let offset = start.to_offset(&snapshot);
+
+ for (buffer, selection_range, range_to_fold) in selections {
+ let range = snapshot.anchor_after(offset + range_to_fold.start)
+ ..snapshot.anchor_after(offset + range_to_fold.end);
+
+ let abs_path = buffer
+ .read(cx)
+ .project_path(cx)
+ .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx));
+ let snapshot = buffer.read(cx).snapshot();
+
+ let text = snapshot
+ .text_for_range(selection_range.clone())
+ .collect::<String>();
+ let point_range = selection_range.to_point(&snapshot);
+ let line_range = point_range.start.row..=point_range.end.row;
+
+ let uri = MentionUri::Selection {
+ abs_path: abs_path.clone(),
+ line_range: line_range.clone(),
+ };
+ let crease = crate::context_picker::crease_for_mention(
+ selection_name(abs_path.as_deref(), &line_range).into(),
+ uri.icon_path(cx),
+ range,
+ self.editor.downgrade(),
+ );
+
+ let crease_id = self.editor.update(cx, |editor, cx| {
+ let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
+ editor.fold_creases(vec![crease], false, window, cx);
+ crease_ids.first().copied().unwrap()
+ });
+
+ self.mention_set.mentions.insert(
+ crease_id,
+ (
+ uri,
+ Task::ready(Ok(Mention::Text {
+ content: text,
+ tracked_buffers: vec![buffer],
+ }))
+ .shared(),
+ ),
+ );
+ }
+ }
+
+ fn confirm_mention_for_thread(
+ &mut self,
+ id: acp::SessionId,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let server = Rc::new(agent2::NativeAgentServer::new(
+ self.project.read(cx).fs().clone(),
+ self.history_store.clone(),
+ ));
+ let delegate = AgentServerDelegate::new(
+ self.project.read(cx).agent_server_store().clone(),
+ self.project.clone(),
+ None,
+ None,
+ );
+ let connection = server.connect(None, delegate, cx);
+ cx.spawn(async move |_, cx| {
+ let (agent, _) = connection.await?;
+ let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
+ let summary = agent
+ .0
+ .update(cx, |agent, cx| agent.thread_summary(id, cx))?
+ .await?;
+ anyhow::Ok(Mention::Text {
+ content: summary.to_string(),
+ tracked_buffers: Vec::new(),
+ })
+ })
+ }
+
+ fn confirm_mention_for_text_thread(
+ &mut self,
+ path: PathBuf,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<Mention>> {
+ let context = self.history_store.update(cx, |text_thread_store, cx| {
+ text_thread_store.load_text_thread(path.as_path().into(), cx)
+ });
+ cx.spawn(async move |_, cx| {
+ let context = context.await?;
+ let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
+ Ok(Mention::Text {
+ content: xml,
+ tracked_buffers: Vec::new(),
+ })
+ })
+ }
+
+ fn validate_slash_commands(
+ text: &str,
+ available_commands: &[acp::AvailableCommand],
+ agent_name: &str,
+ ) -> Result<()> {
+ if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
+ if let Some(command_name) = parsed_command.command {
+ // Check if this command is in the list of available commands from the server
+ let is_supported = available_commands
+ .iter()
+ .any(|cmd| cmd.name == command_name);
+
+ if !is_supported {
+ return Err(anyhow!(
+ "The /{} command is not supported by {}.\n\nAvailable commands: {}",
+ command_name,
+ agent_name,
+ if available_commands.is_empty() {
+ "none".to_string()
+ } else {
+ available_commands
+ .iter()
+ .map(|cmd| format!("/{}", cmd.name))
+ .collect::<Vec<_>>()
+ .join(", ")
+ }
+ ));
+ }
+ }
+ }
+ Ok(())
+ }
+
+ pub fn contents(
+ &self,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
+ // Check for unsupported slash commands before spawning async task
+ let text = self.editor.read(cx).text(cx);
+ let available_commands = self.available_commands.borrow().clone();
+ if let Err(err) =
+ Self::validate_slash_commands(&text, &available_commands, &self.agent_name)
+ {
+ return Task::ready(Err(err));
+ }
+
+ let contents = self
+ .mention_set
+ .contents(&self.prompt_capabilities.get(), cx);
+ let editor = self.editor.clone();
+
+ cx.spawn(async move |_, cx| {
+ let contents = contents.await?;
+ let mut all_tracked_buffers = Vec::new();
+
+ let result = editor.update(cx, |editor, cx| {
+ let mut ix = 0;
+ let mut chunks: Vec<acp::ContentBlock> = Vec::new();
+ let text = editor.text(cx);
+ editor.display_map.update(cx, |map, cx| {
+ let snapshot = map.snapshot(cx);
+ for (crease_id, crease) in snapshot.crease_snapshot.creases() {
+ let Some((uri, mention)) = contents.get(&crease_id) else {
+ continue;
+ };
+
+ let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
+ if crease_range.start > ix {
+ //todo(): Custom slash command ContentBlock?
+ // let chunk = if prevent_slash_commands
+ // && ix == 0
+ // && parse_slash_command(&text[ix..]).is_some()
+ // {
+ // format!(" {}", &text[ix..crease_range.start]).into()
+ // } else {
+ // text[ix..crease_range.start].into()
+ // };
+ let chunk = text[ix..crease_range.start].into();
+ chunks.push(chunk);
+ }
+ let chunk = match mention {
+ Mention::Text {
+ content,
+ tracked_buffers,
+ } => {
+ all_tracked_buffers.extend(tracked_buffers.iter().cloned());
+ acp::ContentBlock::Resource(acp::EmbeddedResource {
+ annotations: None,
+ resource: acp::EmbeddedResourceResource::TextResourceContents(
+ acp::TextResourceContents {
+ mime_type: None,
+ text: content.clone(),
+ uri: uri.to_uri().to_string(),
+ },
+ ),
+ })
+ }
+ Mention::Image(mention_image) => {
+ let uri = match uri {
+ MentionUri::File { .. } => Some(uri.to_uri().to_string()),
+ MentionUri::PastedImage => None,
+ other => {
+ debug_panic!(
+ "unexpected mention uri for image: {:?}",
+ other
+ );
+ None
+ }
+ };
+ acp::ContentBlock::Image(acp::ImageContent {
+ annotations: None,
+ data: mention_image.data.to_string(),
+ mime_type: mention_image.format.mime_type().into(),
+ uri,
+ })
+ }
+ Mention::UriOnly => {
+ acp::ContentBlock::ResourceLink(acp::ResourceLink {
+ name: uri.name(),
+ uri: uri.to_uri().to_string(),
+ annotations: None,
+ description: None,
+ mime_type: None,
+ size: None,
+ title: None,
+ })
+ }
+ };
+ chunks.push(chunk);
+ ix = crease_range.end;
+ }
+
+ if ix < text.len() {
+ //todo(): Custom slash command ContentBlock?
+ // let last_chunk = if prevent_slash_commands
+ // && ix == 0
+ // && parse_slash_command(&text[ix..]).is_some()
+ // {
+ // format!(" {}", text[ix..].trim_end())
+ // } else {
+ // text[ix..].trim_end().to_owned()
+ // };
+ let last_chunk = text[ix..].trim_end().to_owned();
+ if !last_chunk.is_empty() {
+ chunks.push(last_chunk.into());
+ }
+ }
+ });
+ Ok((chunks, all_tracked_buffers))
+ })?;
+ result
+ })
+ }
+
+ pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.editor.update(cx, |editor, cx| {
+ editor.clear(window, cx);
+ editor.remove_creases(
+ self.mention_set
+ .mentions
+ .drain()
+ .map(|(crease_id, _)| crease_id),
+ cx,
+ )
+ });
+ }
+
+ fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
+ if self.is_empty(cx) {
+ return;
+ }
+ cx.emit(MessageEditorEvent::Send)
+ }
+
+ fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+ cx.emit(MessageEditorEvent::Cancel)
+ }
+
+ fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
+ if !self.prompt_capabilities.get().image {
+ return;
+ }
+
+ let images = cx
+ .read_from_clipboard()
+ .map(|item| {
+ item.into_entries()
+ .filter_map(|entry| {
+ if let ClipboardEntry::Image(image) = entry {
+ Some(image)
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>()
+ })
+ .unwrap_or_default();
+
+ if images.is_empty() {
+ return;
+ }
+ cx.stop_propagation();
+
+ let replacement_text = MentionUri::PastedImage.as_link().to_string();
+ for image in images {
+ let (excerpt_id, text_anchor, multibuffer_anchor) =
+ self.editor.update(cx, |message_editor, cx| {
+ let snapshot = message_editor.snapshot(window, cx);
+ let (excerpt_id, _, buffer_snapshot) =
+ snapshot.buffer_snapshot.as_singleton().unwrap();
+
+ let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
+ let multibuffer_anchor = snapshot
+ .buffer_snapshot
+ .anchor_in_excerpt(*excerpt_id, text_anchor);
+ message_editor.edit(
+ [(
+ multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
+ format!("{replacement_text} "),
+ )],
+ cx,
+ );
+ (*excerpt_id, text_anchor, multibuffer_anchor)
+ });
+
+ let content_len = replacement_text.len();
+ let Some(start_anchor) = multibuffer_anchor else {
+ continue;
+ };
+ let end_anchor = self.editor.update(cx, |editor, cx| {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
+ });
+ let image = Arc::new(image);
+ let Some((crease_id, tx)) = insert_crease_for_mention(
+ excerpt_id,
+ text_anchor,
+ content_len,
+ MentionUri::PastedImage.name().into(),
+ IconName::Image.path().into(),
+ Some(Task::ready(Ok(image.clone())).shared()),
+ self.editor.clone(),
+ window,
+ cx,
+ ) else {
+ continue;
+ };
+ let task = cx
+ .spawn_in(window, {
+ async move |_, cx| {
+ let format = image.format;
+ let image = cx
+ .update(|_, cx| LanguageModelImage::from_image(image, cx))
+ .map_err(|e| e.to_string())?
+ .await;
+ drop(tx);
+ if let Some(image) = image {
+ Ok(Mention::Image(MentionImage {
+ data: image.source,
+ format,
+ }))
+ } else {
+ Err("Failed to convert image".into())
+ }
+ }
+ })
+ .shared();
+
+ self.mention_set
+ .mentions
+ .insert(crease_id, (MentionUri::PastedImage, task.clone()));
+
+ cx.spawn_in(window, async move |this, cx| {
+ if task.await.notify_async_err(cx).is_none() {
+ this.update(cx, |this, cx| {
+ this.editor.update(cx, |editor, cx| {
+ editor.edit([(start_anchor..end_anchor, "")], cx);
+ });
+ this.mention_set.mentions.remove(&crease_id);
+ })
+ .ok();
+ }
+ })
+ .detach();
+ }
+ }
+
+ pub fn insert_dragged_files(
+ &mut self,
+ paths: Vec<project::ProjectPath>,
+ added_worktrees: Vec<Entity<Worktree>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let buffer = self.editor.read(cx).buffer().clone();
+ let Some(buffer) = buffer.read(cx).as_singleton() else {
+ return;
+ };
+ let mut tasks = Vec::new();
+ for path in paths {
+ let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
+ continue;
+ };
+ let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
+ continue;
+ };
+ let path_prefix = abs_path
+ .file_name()
+ .unwrap_or(path.path.as_os_str())
+ .display()
+ .to_string();
+ let (file_name, _) =
+ crate::context_picker::file_context_picker::extract_file_name_and_directory(
+ &path.path,
+ &path_prefix,
+ );
+
+ let uri = if entry.is_dir() {
+ MentionUri::Directory { abs_path }
+ } else {
+ MentionUri::File { abs_path }
+ };
+
+ let new_text = format!("{} ", uri.as_link());
+ let content_len = new_text.len() - 1;
+
+ let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
+
+ self.editor.update(cx, |message_editor, cx| {
+ message_editor.edit(
+ [(
+ multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
+ new_text,
+ )],
+ cx,
+ );
+ });
+ tasks.push(self.confirm_mention_completion(
+ file_name,
+ anchor,
+ content_len,
+ uri,
+ window,
+ cx,
+ ));
+ }
+ cx.spawn(async move |_, _| {
+ join_all(tasks).await;
+ drop(added_worktrees);
+ })
+ .detach();
+ }
+
+ pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let buffer = self.editor.read(cx).buffer().clone();
+ let Some(buffer) = buffer.read(cx).as_singleton() else {
+ return;
+ };
+ let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
+ let Some(workspace) = self.workspace.upgrade() else {
+ return;
+ };
+ let Some(completion) = ContextPickerCompletionProvider::completion_for_action(
+ ContextPickerAction::AddSelections,
+ anchor..anchor,
+ cx.weak_entity(),
+ &workspace,
+ cx,
+ ) else {
+ return;
+ };
+ self.editor.update(cx, |message_editor, cx| {
+ message_editor.edit(
+ [(
+ multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
+ completion.new_text,
+ )],
+ cx,
+ );
+ });
+ if let Some(confirm) = completion.confirm {
+ confirm(CompletionIntent::Complete, window, cx);
+ }
+ }
+
+ pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context<Self>) {
+ self.editor.update(cx, |message_editor, cx| {
+ message_editor.set_read_only(read_only);
+ cx.notify()
+ })
+ }
+
+ pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
+ self.editor.update(cx, |editor, cx| {
+ editor.set_mode(mode);
+ cx.notify()
+ });
+ }
+
+ pub fn set_message(
+ &mut self,
+ message: Vec<acp::ContentBlock>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.clear(window, cx);
+
+ let mut text = String::new();
+ let mut mentions = Vec::new();
+
+ for chunk in message {
+ match chunk {
+ acp::ContentBlock::Text(text_content) => {
+ text.push_str(&text_content.text);
+ }
+ acp::ContentBlock::Resource(acp::EmbeddedResource {
+ resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
+ ..
+ }) => {
+ let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() else {
+ continue;
+ };
+ let start = text.len();
+ write!(&mut text, "{}", mention_uri.as_link()).ok();
+ let end = text.len();
+ mentions.push((
+ start..end,
+ mention_uri,
+ Mention::Text {
+ content: resource.text,
+ tracked_buffers: Vec::new(),
+ },
+ ));
+ }
+ acp::ContentBlock::ResourceLink(resource) => {
+ if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
+ let start = text.len();
+ write!(&mut text, "{}", mention_uri.as_link()).ok();
+ let end = text.len();
+ mentions.push((start..end, mention_uri, Mention::UriOnly));
+ }
+ }
+ acp::ContentBlock::Image(acp::ImageContent {
+ uri,
+ data,
+ mime_type,
+ annotations: _,
+ }) => {
+ let mention_uri = if let Some(uri) = uri {
+ MentionUri::parse(&uri)
+ } else {
+ Ok(MentionUri::PastedImage)
+ };
+ let Some(mention_uri) = mention_uri.log_err() else {
+ continue;
+ };
+ let Some(format) = ImageFormat::from_mime_type(&mime_type) else {
+ log::error!("failed to parse MIME type for image: {mime_type:?}");
+ continue;
+ };
+ let start = text.len();
+ write!(&mut text, "{}", mention_uri.as_link()).ok();
+ let end = text.len();
+ mentions.push((
+ start..end,
+ mention_uri,
+ Mention::Image(MentionImage {
+ data: data.into(),
+ format,
+ }),
+ ));
+ }
+ acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {}
+ }
+ }
+
+ let snapshot = self.editor.update(cx, |editor, cx| {
+ editor.set_text(text, window, cx);
+ editor.buffer().read(cx).snapshot(cx)
+ });
+
+ for (range, mention_uri, mention) in mentions {
+ let anchor = snapshot.anchor_before(range.start);
+ let Some((crease_id, tx)) = insert_crease_for_mention(
+ anchor.excerpt_id,
+ anchor.text_anchor,
+ range.end - range.start,
+ mention_uri.name().into(),
+ mention_uri.icon_path(cx),
+ None,
+ self.editor.clone(),
+ window,
+ cx,
+ ) else {
+ continue;
+ };
+ drop(tx);
+
+ self.mention_set.mentions.insert(
+ crease_id,
+ (mention_uri.clone(), Task::ready(Ok(mention)).shared()),
+ );
+ }
+ cx.notify();
+ }
+
+ pub fn text(&self, cx: &App) -> String {
+ self.editor.read(cx).text(cx)
+ }
+
+ #[cfg(test)]
+ pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
+ self.editor.update(cx, |editor, cx| {
+ editor.set_text(text, window, cx);
+ });
+ }
+}
+
+fn render_directory_contents(entries: Vec<(Arc<Path>, PathBuf, String)>) -> String {
+ let mut output = String::new();
+ for (_relative_path, full_path, content) in entries {
+ let fence = codeblock_fence_for_path(Some(&full_path), None);
+ write!(output, "\n{fence}\n{content}\n```").unwrap();
+ }
+ output
+}
+
+impl Focusable for MessageEditor {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.editor.focus_handle(cx)
+ }
+}
+
+impl Render for MessageEditor {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .key_context("MessageEditor")
+ .on_action(cx.listener(Self::send))
+ .on_action(cx.listener(Self::cancel))
+ .capture_action(cx.listener(Self::paste))
+ .flex_1()
+ .child({
+ let settings = ThemeSettings::get_global(cx);
+ let font_size = TextSize::Small
+ .rems(cx)
+ .to_pixels(settings.agent_font_size(cx));
+ let line_height = settings.buffer_line_height.value() * font_size;
+
+ let text_style = TextStyle {
+ color: cx.theme().colors().text,
+ font_family: settings.buffer_font.family.clone(),
+ font_fallbacks: settings.buffer_font.fallbacks.clone(),
+ font_features: settings.buffer_font.features.clone(),
+ font_size: font_size.into(),
+ line_height: line_height.into(),
+ ..Default::default()
+ };
+
+ EditorElement::new(
+ &self.editor,
+ EditorStyle {
+ background: cx.theme().colors().editor_background,
+ local_player: cx.theme().players().local(),
+ text: text_style,
+ syntax: cx.theme().syntax().clone(),
+ inlay_hints_style: editor::make_inlay_hints_style(cx),
+ ..Default::default()
+ },
+ )
+ })
+ }
+}
+
+pub(crate) fn insert_crease_for_mention(
+ excerpt_id: ExcerptId,
+ anchor: text::Anchor,
+ content_len: usize,
+ crease_label: SharedString,
+ crease_icon: SharedString,
+ // abs_path: Option<Arc<Path>>,
+ image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
+ editor: Entity<Editor>,
+ window: &mut Window,
+ cx: &mut App,
+) -> Option<(CreaseId, postage::barrier::Sender)> {
+ let (tx, rx) = postage::barrier::channel();
+
+ let crease_id = editor.update(cx, |editor, cx| {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+
+ let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
+
+ let start = start.bias_right(&snapshot);
+ let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
+
+ let placeholder = FoldPlaceholder {
+ render: render_mention_fold_button(
+ crease_label,
+ crease_icon,
+ start..end,
+ rx,
+ image,
+ cx.weak_entity(),
+ cx,
+ ),
+ merge_adjacent: false,
+ ..Default::default()
+ };
+
+ let crease = Crease::Inline {
+ range: start..end,
+ placeholder,
+ render_toggle: None,
+ render_trailer: None,
+ metadata: None,
+ };
+
+ let ids = editor.insert_creases(vec![crease.clone()], cx);
+ editor.fold_creases(vec![crease], false, window, cx);
+
+ Some(ids[0])
+ })?;
+
+ Some((crease_id, tx))
+}
+
+fn render_mention_fold_button(
+ label: SharedString,
+ icon: SharedString,
+ range: Range<Anchor>,
+ mut loading_finished: postage::barrier::Receiver,
+ image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
+ editor: WeakEntity<Editor>,
+ cx: &mut App,
+) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
+ let loading = cx.new(|cx| {
+ let loading = cx.spawn(async move |this, cx| {
+ loading_finished.recv().await;
+ this.update(cx, |this: &mut LoadingContext, cx| {
+ this.loading = None;
+ cx.notify();
+ })
+ .ok();
+ });
+ LoadingContext {
+ id: cx.entity_id(),
+ label,
+ icon,
+ range,
+ editor,
+ loading: Some(loading),
+ image: image_task.clone(),
+ }
+ });
+ Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
+}
+
+struct LoadingContext {
+ id: EntityId,
+ label: SharedString,
+ icon: SharedString,
+ range: Range<Anchor>,
+ editor: WeakEntity<Editor>,
+ loading: Option<Task<()>>,
+ image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
+}
+
+impl Render for LoadingContext {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let is_in_text_selection = self
+ .editor
+ .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
+ .unwrap_or_default();
+ ButtonLike::new(("loading-context", self.id))
+ .style(ButtonStyle::Filled)
+ .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+ .toggle_state(is_in_text_selection)
+ .when_some(self.image.clone(), |el, image_task| {
+ el.hoverable_tooltip(move |_, cx| {
+ let image = image_task.peek().cloned().transpose().ok().flatten();
+ let image_task = image_task.clone();
+ cx.new::<ImageHover>(|cx| ImageHover {
+ image,
+ _task: cx.spawn(async move |this, cx| {
+ if let Ok(image) = image_task.clone().await {
+ this.update(cx, |this, cx| {
+ if this.image.replace(image).is_none() {
+ cx.notify();
+ }
+ })
+ .ok();
+ }
+ }),
+ })
+ .into()
+ })
+ })
+ .child(
+ h_flex()
+ .gap_1()
+ .child(
+ Icon::from_path(self.icon.clone())
+ .size(IconSize::XSmall)
+ .color(Color::Muted),
+ )
+ .child(
+ Label::new(self.label.clone())
+ .size(LabelSize::Small)
+ .buffer_font(cx)
+ .single_line(),
+ )
+ .map(|el| {
+ if self.loading.is_some() {
+ el.with_animation(
+ "loading-context-crease",
+ Animation::new(Duration::from_secs(2))
+ .repeat()
+ .with_easing(pulsating_between(0.4, 0.8)),
+ |label, delta| label.opacity(delta),
+ )
+ .into_any()
+ } else {
+ el.into_any()
+ }
+ }),
+ )
+ }
+}
+
+struct ImageHover {
+ image: Option<Arc<Image>>,
+ _task: Task<()>,
+}
+
+impl Render for ImageHover {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ if let Some(image) = self.image.clone() {
+ gpui::img(image).max_w_96().max_h_96().into_any_element()
+ } else {
+ gpui::Empty.into_any_element()
+ }
+ }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Mention {
+ Text {
+ content: String,
+ tracked_buffers: Vec<Entity<Buffer>>,
+ },
+ Image(MentionImage),
+ UriOnly,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct MentionImage {
+ pub data: SharedString,
+ pub format: ImageFormat,
+}
+
+#[derive(Default)]
+pub struct MentionSet {
+ mentions: HashMap<CreaseId, (MentionUri, Shared<Task<Result<Mention, String>>>)>,
+}
+
+impl MentionSet {
+ fn contents(
+ &self,
+ prompt_capabilities: &acp::PromptCapabilities,
+ cx: &mut App,
+ ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
+ if !prompt_capabilities.embedded_context {
+ let mentions = self
+ .mentions
+ .iter()
+ .map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly)))
+ .collect();
+
+ return Task::ready(Ok(mentions));
+ }
+
+ let mentions = self.mentions.clone();
+ cx.spawn(async move |_cx| {
+ let mut contents = HashMap::default();
+ for (crease_id, (mention_uri, task)) in mentions {
+ contents.insert(
+ crease_id,
+ (mention_uri, task.await.map_err(|e| anyhow!("{e}"))?),
+ );
+ }
+ Ok(contents)
+ })
+ }
+
+ fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
+ for (crease_id, crease) in snapshot.crease_snapshot.creases() {
+ if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
+ self.mentions.remove(&crease_id);
+ }
+ }
+ }
+}
+
+pub struct MessageEditorAddon {}
+
+impl MessageEditorAddon {
+ pub fn new() -> Self {
+ Self {}
+ }
+}
+
+impl Addon for MessageEditorAddon {
+ fn to_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
+ fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
+ Some(self)
+ }
+
+ fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) {
+ let settings = agent_settings::AgentSettings::get_global(cx);
+ if settings.use_modifier_to_send {
+ key_context.add("use_modifier_to_send");
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{
+ cell::{Cell, RefCell},
+ ops::Range,
+ path::Path,
+ rc::Rc,
+ sync::Arc,
+ };
+
+ use acp_thread::MentionUri;
+ use agent_client_protocol as acp;
+ use agent2::HistoryStore;
+ use assistant_context::ContextStore;
+ use editor::{AnchorRangeExt as _, Editor, EditorMode};
+ use fs::FakeFs;
+ use futures::StreamExt as _;
+ use gpui::{
+ AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext,
+ };
+ use lsp::{CompletionContext, CompletionTriggerKind};
+ use project::{CompletionIntent, Project, ProjectPath};
+ use serde_json::json;
+ use text::Point;
+ use ui::{App, Context, IntoElement, Render, SharedString, Window};
+ use util::{path, uri};
+ use workspace::{AppState, Item, Workspace};
+
+ use crate::acp::{
+ message_editor::{Mention, MessageEditor},
+ thread_view::tests::init_test,
+ };
+
+ #[gpui::test]
+ async fn test_at_mention_removal(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree("/project", json!({"file": ""})).await;
+ let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+ let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
+ let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
+
+ let message_editor = cx.update(|window, cx| {
+ cx.new(|cx| {
+ MessageEditor::new(
+ workspace.downgrade(),
+ project.clone(),
+ history_store.clone(),
+ None,
+ Default::default(),
+ Default::default(),
+ "Test Agent".into(),
+ "Test",
+ EditorMode::AutoHeight {
+ min_lines: 1,
+ max_lines: None,
+ },
+ window,
+ cx,
+ )
+ })
+ });
+ let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
+
+ cx.run_until_parked();
+
+ let excerpt_id = editor.update(cx, |editor, cx| {
+ editor
+ .buffer()
+ .read(cx)
+ .excerpt_ids()
+ .into_iter()
+ .next()
+ .unwrap()
+ });
+ let completions = editor.update_in(cx, |editor, window, cx| {
+ editor.set_text("Hello @file ", window, cx);
+ let buffer = editor.buffer().read(cx).as_singleton().unwrap();
+ let completion_provider = editor.completion_provider().unwrap();
+ completion_provider.completions(
+ excerpt_id,
+ &buffer,
+ text::Anchor::MAX,
+ CompletionContext {
+ trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
+ trigger_character: Some("@".into()),
+ },
+ window,
+ cx,
+ )
+ });
+ let [_, completion]: [_; 2] = completions
+ .await
+ .unwrap()
+ .into_iter()
+ .flat_map(|response| response.completions)
+ .collect::<Vec<_>>()
+ .try_into()
+ .unwrap();
+
+ editor.update_in(cx, |editor, window, cx| {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ let start = snapshot
+ .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
+ .unwrap();
+ let end = snapshot
+ .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
+ .unwrap();
+ editor.edit([(start..end, completion.new_text)], cx);
+ (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
+ });
+
+ cx.run_until_parked();
+
+ // Backspace over the inserted crease (and the following space).
+ editor.update_in(cx, |editor, window, cx| {
+ editor.backspace(&Default::default(), window, cx);
+ editor.backspace(&Default::default(), window, cx);
+ });
+
+ let (content, _) = message_editor
+ .update(cx, |message_editor, cx| message_editor.contents(cx))
+ .await
+ .unwrap();
+
+ // We don't send a resource link for the deleted crease.
+ pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
+ }
+
+ #[gpui::test]
+ async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/test",
+ json!({
+ ".zed": {
+ "tasks.json": r#"[{"label": "test", "command": "echo"}]"#
+ },
+ "src": {
+ "main.rs": "fn main() {}",
+ },
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
+ let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
+ let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
+ let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
+ // Start with no available commands - simulating Claude which doesn't support slash commands
+ let available_commands = Rc::new(RefCell::new(vec![]));
+
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let workspace_handle = workspace.downgrade();
+ let message_editor = workspace.update_in(cx, |_, window, cx| {
+ cx.new(|cx| {
+ MessageEditor::new(
+ workspace_handle.clone(),
+ project.clone(),
+ history_store.clone(),
+ None,
+ prompt_capabilities.clone(),
+ available_commands.clone(),
+ "Claude Code".into(),
+ "Test",
+ EditorMode::AutoHeight {
+ min_lines: 1,
+ max_lines: None,
+ },
+ window,
+ cx,
+ )
+ })
+ });
+ let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
+
+ // Test that slash commands fail when no available_commands are set (empty list means no commands supported)
+ editor.update_in(cx, |editor, window, cx| {
+ editor.set_text("/file test.txt", window, cx);
+ });
+
+ let contents_result = message_editor
+ .update(cx, |message_editor, cx| message_editor.contents(cx))
+ .await;
+
+ // Should fail because available_commands is empty (no commands supported)
+ assert!(contents_result.is_err());
+ let error_message = contents_result.unwrap_err().to_string();
+ assert!(error_message.contains("not supported by Claude Code"));
+ assert!(error_message.contains("Available commands: none"));
+
+ // Now simulate Claude providing its list of available commands (which doesn't include file)
+ available_commands.replace(vec![acp::AvailableCommand {
+ name: "help".to_string(),
+ description: "Get help".to_string(),
+ input: None,
+ }]);
+
+ // Test that unsupported slash commands trigger an error when we have a list of available commands
+ editor.update_in(cx, |editor, window, cx| {
+ editor.set_text("/file test.txt", window, cx);
+ });
+
+ let contents_result = message_editor
+ .update(cx, |message_editor, cx| message_editor.contents(cx))
+ .await;
+
+ assert!(contents_result.is_err());
+ let error_message = contents_result.unwrap_err().to_string();
+ assert!(error_message.contains("not supported by Claude Code"));
+ assert!(error_message.contains("/file"));
+ assert!(error_message.contains("Available commands: /help"));
+
+ // Test that supported commands work fine
+ editor.update_in(cx, |editor, window, cx| {
+ editor.set_text("/help", window, cx);
+ });
+
+ let contents_result = message_editor
+ .update(cx, |message_editor, cx| message_editor.contents(cx))
+ .await;
+
+ // Should succeed because /help is in available_commands
+ assert!(contents_result.is_ok());
+
+ // Test that regular text works fine
+ editor.update_in(cx, |editor, window, cx| {
+ editor.set_text("Hello Claude!", window, cx);
+ });
+
+ let (content, _) = message_editor
+ .update(cx, |message_editor, cx| message_editor.contents(cx))
+ .await
+ .unwrap();
+
+ assert_eq!(content.len(), 1);
+ if let acp::ContentBlock::Text(text) = &content[0] {
+ assert_eq!(text.text, "Hello Claude!");
+ } else {
+ panic!("Expected ContentBlock::Text");
+ }
+
+ // Test that @ mentions still work
+ editor.update_in(cx, |editor, window, cx| {
+ editor.set_text("Check this @", window, cx);
+ });
+
+ // The @ mention functionality should not be affected
+ let (content, _) = message_editor
+ .update(cx, |message_editor, cx| message_editor.contents(cx))
+ .await
+ .unwrap();
+
+ assert_eq!(content.len(), 1);
+ if let acp::ContentBlock::Text(text) = &content[0] {
+ assert_eq!(text.text, "Check this @");
+ } else {
+ panic!("Expected ContentBlock::Text");
+ }
+ }
+
+ struct MessageEditorItem(Entity<MessageEditor>);
+
+ impl Item for MessageEditorItem {
+ type Event = ();
+
+ fn include_in_nav_history() -> bool {
+ false
+ }
+
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ "Test".into()
+ }
+ }
+
+ impl EventEmitter<()> for MessageEditorItem {}
+
+ impl Focusable for MessageEditorItem {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.0.read(cx).focus_handle(cx)
+ }
+ }
+
+ impl Render for MessageEditorItem {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ self.0.clone().into_any_element()
+ }
+ }
+
+ #[gpui::test]
+ async fn test_completion_provider_commands(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let app_state = cx.update(AppState::test);
+
+ cx.update(|cx| {
+ language::init(cx);
+ editor::init(cx);
+ workspace::init(app_state.clone(), cx);
+ Project::init_settings(cx);
+ });
+
+ let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
+ let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let workspace = window.root(cx).unwrap();
+
+ let mut cx = VisualTestContext::from_window(*window, cx);
+
+ let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
+ let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
+ let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
+ let available_commands = Rc::new(RefCell::new(vec![
+ acp::AvailableCommand {
+ name: "quick-math".to_string(),
+ description: "2 + 2 = 4 - 1 = 3".to_string(),
+ input: None,
+ },
+ acp::AvailableCommand {
+ name: "say-hello".to_string(),
+ description: "Say hello to whoever you want".to_string(),
+ input: Some(acp::AvailableCommandInput::Unstructured {
+ hint: "<name>".to_string(),
+ }),
+ },
+ ]));
+
+ let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
+ let workspace_handle = cx.weak_entity();
+ let message_editor = cx.new(|cx| {
+ MessageEditor::new(
+ workspace_handle,
+ project.clone(),
+ history_store.clone(),
+ None,
+ prompt_capabilities.clone(),
+ available_commands.clone(),
+ "Test Agent".into(),
+ "Test",
+ EditorMode::AutoHeight {
+ max_lines: None,
+ min_lines: 1,
+ },
+ window,
+ cx,
+ )
+ });
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.add_item(
+ Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
+ true,
+ true,
+ None,
+ window,
+ cx,
+ );
+ });
+ message_editor.read(cx).focus_handle(cx).focus(window);
+ message_editor.read(cx).editor().clone()
+ });
+
+ cx.simulate_input("/");
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert_eq!(editor.text(cx), "/");
+ assert!(editor.has_visible_completions_menu());
+
+ assert_eq!(
+ current_completion_labels_with_documentation(editor),
+ &[
+ ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
+ ("say-hello".into(), "Say hello to whoever you want".into())
+ ]
+ );
+ editor.set_text("", window, cx);
+ });
+
+ cx.simulate_input("/qui");
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert_eq!(editor.text(cx), "/qui");
+ assert!(editor.has_visible_completions_menu());
+
+ assert_eq!(
+ current_completion_labels_with_documentation(editor),
+ &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
+ );
+ editor.set_text("", window, cx);
+ });
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert!(editor.has_visible_completions_menu());
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ cx.run_until_parked();
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert_eq!(editor.display_text(cx), "/quick-math ");
+ assert!(!editor.has_visible_completions_menu());
+ editor.set_text("", window, cx);
+ });
+
+ cx.simulate_input("/say");
+
+ editor.update_in(&mut cx, |editor, _window, cx| {
+ assert_eq!(editor.display_text(cx), "/say");
+ assert!(editor.has_visible_completions_menu());
+
+ assert_eq!(
+ current_completion_labels_with_documentation(editor),
+ &[("say-hello".into(), "Say hello to whoever you want".into())]
+ );
+ });
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert!(editor.has_visible_completions_menu());
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ cx.run_until_parked();
+
+ editor.update_in(&mut cx, |editor, _window, cx| {
+ assert_eq!(editor.text(cx), "/say-hello ");
+ assert_eq!(editor.display_text(cx), "/say-hello <name>");
+ assert!(editor.has_visible_completions_menu());
+
+ assert_eq!(
+ current_completion_labels_with_documentation(editor),
+ &[("say-hello".into(), "Say hello to whoever you want".into())]
+ );
+ });
+
+ cx.simulate_input("GPT5");
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert!(editor.has_visible_completions_menu());
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ cx.run_until_parked();
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert_eq!(editor.text(cx), "/say-hello GPT5");
+ assert_eq!(editor.display_text(cx), "/say-hello GPT5");
+ assert!(!editor.has_visible_completions_menu());
+
+ // Delete argument
+ for _ in 0..4 {
+ editor.backspace(&editor::actions::Backspace, window, cx);
+ }
+ });
+
+ cx.run_until_parked();
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert_eq!(editor.text(cx), "/say-hello ");
+ // Hint is visible because argument was deleted
+ assert_eq!(editor.display_text(cx), "/say-hello <name>");
+
+ // Delete last command letter
+ editor.backspace(&editor::actions::Backspace, window, cx);
+ editor.backspace(&editor::actions::Backspace, window, cx);
+ });
+
+ cx.run_until_parked();
+
+ editor.update_in(&mut cx, |editor, _window, cx| {
+ // Hint goes away once command no longer matches an available one
+ assert_eq!(editor.text(cx), "/say-hell");
+ assert_eq!(editor.display_text(cx), "/say-hell");
+ assert!(!editor.has_visible_completions_menu());
+ });
+ }
+
+ #[gpui::test]
+ async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let app_state = cx.update(AppState::test);
+
+ cx.update(|cx| {
+ language::init(cx);
+ editor::init(cx);
+ workspace::init(app_state.clone(), cx);
+ Project::init_settings(cx);
+ });
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ path!("/dir"),
+ json!({
+ "editor": "",
+ "a": {
+ "one.txt": "1",
+ "two.txt": "2",
+ "three.txt": "3",
+ "four.txt": "4"
+ },
+ "b": {
+ "five.txt": "5",
+ "six.txt": "6",
+ "seven.txt": "7",
+ "eight.txt": "8",
+ },
+ "x.png": "",
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
+ let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let workspace = window.root(cx).unwrap();
+
+ let worktree = project.update(cx, |project, cx| {
+ let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
+ assert_eq!(worktrees.len(), 1);
+ worktrees.pop().unwrap()
+ });
+ let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
+
+ let mut cx = VisualTestContext::from_window(*window, cx);
+
+ let paths = vec![
+ path!("a/one.txt"),
+ path!("a/two.txt"),
+ path!("a/three.txt"),
+ path!("a/four.txt"),
+ path!("b/five.txt"),
+ path!("b/six.txt"),
+ path!("b/seven.txt"),
+ path!("b/eight.txt"),
+ ];
+
+ let mut opened_editors = Vec::new();
+ for path in paths {
+ let buffer = workspace
+ .update_in(&mut cx, |workspace, window, cx| {
+ workspace.open_path(
+ ProjectPath {
+ worktree_id,
+ path: Path::new(path).into(),
+ },
+ None,
+ false,
+ window,
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+ opened_editors.push(buffer);
+ }
+
+ let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
+ let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
+ let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
+
+ let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
+ let workspace_handle = cx.weak_entity();
+ let message_editor = cx.new(|cx| {
+ MessageEditor::new(
+ workspace_handle,
+ project.clone(),
+ history_store.clone(),
+ None,
+ prompt_capabilities.clone(),
+ Default::default(),
+ "Test Agent".into(),
+ "Test",
+ EditorMode::AutoHeight {
+ max_lines: None,
+ min_lines: 1,
+ },
+ window,
+ cx,
+ )
+ });
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.add_item(
+ Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
+ true,
+ true,
+ None,
+ window,
+ cx,
+ );
+ });
+ message_editor.read(cx).focus_handle(cx).focus(window);
+ let editor = message_editor.read(cx).editor().clone();
+ (message_editor, editor)
+ });
+
+ cx.simulate_input("Lorem @");
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert_eq!(editor.text(cx), "Lorem @");
+ assert!(editor.has_visible_completions_menu());
+
+ assert_eq!(
+ current_completion_labels(editor),
+ &[
+ "eight.txt dir/b/",
+ "seven.txt dir/b/",
+ "six.txt dir/b/",
+ "five.txt dir/b/",
+ ]
+ );
+ editor.set_text("", window, cx);
+ });
+
+ prompt_capabilities.set(acp::PromptCapabilities {
+ image: true,
+ audio: true,
+ embedded_context: true,
+ });
+
+ cx.simulate_input("Lorem ");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Lorem ");
+ assert!(!editor.has_visible_completions_menu());
+ });
+
+ cx.simulate_input("@");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Lorem @");
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(
+ current_completion_labels(editor),
+ &[
+ "eight.txt dir/b/",
+ "seven.txt dir/b/",
+ "six.txt dir/b/",
+ "five.txt dir/b/",
+ "Files & Directories",
+ "Symbols",
+ "Threads",
+ "Fetch"
+ ]
+ );
+ });
+
+ // Select and confirm "File"
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert!(editor.has_visible_completions_menu());
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx);
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ cx.run_until_parked();
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Lorem @file ");
+ assert!(editor.has_visible_completions_menu());
+ });
+
+ cx.simulate_input("one");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(editor.text(cx), "Lorem @file one");
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(current_completion_labels(editor), vec!["one.txt dir/a/"]);
+ });
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ assert!(editor.has_visible_completions_menu());
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ let url_one = uri!("file:///dir/a/one.txt");
+ editor.update(&mut cx, |editor, cx| {
+ let text = editor.text(cx);
+ assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
+ assert!(!editor.has_visible_completions_menu());
+ assert_eq!(fold_ranges(editor, cx).len(), 1);
+ });
+
+ let all_prompt_capabilities = acp::PromptCapabilities {
+ image: true,
+ audio: true,
+ embedded_context: true,
+ };
+
+ let contents = message_editor
+ .update(&mut cx, |message_editor, cx| {
+ message_editor
+ .mention_set()
+ .contents(&all_prompt_capabilities, cx)
+ })
+ .await
+ .unwrap()
+ .into_values()
+ .collect::<Vec<_>>();
+
+ {
+ let [(uri, Mention::Text { content, .. })] = contents.as_slice() else {
+ panic!("Unexpected mentions");
+ };
+ pretty_assertions::assert_eq!(content, "1");
+ pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
+ }
+
+ let contents = message_editor
+ .update(&mut cx, |message_editor, cx| {
+ message_editor
+ .mention_set()
+ .contents(&acp::PromptCapabilities::default(), cx)
+ })
+ .await
+ .unwrap()
+ .into_values()
+ .collect::<Vec<_>>();
+
+ {
+ let [(uri, Mention::UriOnly)] = contents.as_slice() else {
+ panic!("Unexpected mentions");
+ };
+ pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
+ }
+
+ cx.simulate_input(" ");
+
+ editor.update(&mut cx, |editor, cx| {
+ let text = editor.text(cx);
+ assert_eq!(text, format!("Lorem [@one.txt]({url_one}) "));
+ assert!(!editor.has_visible_completions_menu());
+ assert_eq!(fold_ranges(editor, cx).len(), 1);
+ });
+
+ cx.simulate_input("Ipsum ");
+
+ editor.update(&mut cx, |editor, cx| {
+ let text = editor.text(cx);
+ assert_eq!(text, format!("Lorem [@one.txt]({url_one}) Ipsum "),);
+ assert!(!editor.has_visible_completions_menu());
+ assert_eq!(fold_ranges(editor, cx).len(), 1);
+ });
+
+ cx.simulate_input("@file ");
+
+ editor.update(&mut cx, |editor, cx| {
+ let text = editor.text(cx);
+ assert_eq!(text, format!("Lorem [@one.txt]({url_one}) Ipsum @file "),);
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(fold_ranges(editor, cx).len(), 1);
+ });
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ cx.run_until_parked();
+
+ let contents = message_editor
+ .update(&mut cx, |message_editor, cx| {
+ message_editor
+ .mention_set()
+ .contents(&all_prompt_capabilities, cx)
+ })
+ .await
+ .unwrap()
+ .into_values()
+ .collect::<Vec<_>>();
+
+ let url_eight = uri!("file:///dir/b/eight.txt");
+
+ {
+ let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
+ panic!("Unexpected mentions");
+ };
+ pretty_assertions::assert_eq!(content, "8");
+ pretty_assertions::assert_eq!(uri, &url_eight.parse::<MentionUri>().unwrap());
+ }
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) ")
+ );
+ assert!(!editor.has_visible_completions_menu());
+ assert_eq!(fold_ranges(editor, cx).len(), 2);
+ });
+
+ let plain_text_language = Arc::new(language::Language::new(
+ language::LanguageConfig {
+ name: "Plain Text".into(),
+ matcher: language::LanguageMatcher {
+ path_suffixes: vec!["txt".to_string()],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ None,
+ ));
+
+ // Register the language and fake LSP
+ let language_registry = project.read_with(&cx, |project, _| project.languages().clone());
+ language_registry.add(plain_text_language);
+
+ let mut fake_language_servers = language_registry.register_fake_lsp(
+ "Plain Text",
+ language::FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ workspace_symbol_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ );
+
+ // Open the buffer to trigger LSP initialization
+ let buffer = project
+ .update(&mut cx, |project, cx| {
+ project.open_local_buffer(path!("/dir/a/one.txt"), cx)
+ })
+ .await
+ .unwrap();
+
+ // Register the buffer with language servers
+ let _handle = project.update(&mut cx, |project, cx| {
+ project.register_buffer_with_language_servers(&buffer, cx)
+ });
+
+ cx.run_until_parked();
+
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ fake_language_server.set_request_handler::<lsp::WorkspaceSymbolRequest, _, _>(
+ move |_, _| async move {
+ Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![
+ #[allow(deprecated)]
+ lsp::SymbolInformation {
+ name: "MySymbol".into(),
+ location: lsp::Location {
+ uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
+ range: lsp::Range::new(
+ lsp::Position::new(0, 0),
+ lsp::Position::new(0, 1),
+ ),
+ },
+ kind: lsp::SymbolKind::CONSTANT,
+ tags: None,
+ container_name: None,
+ deprecated: None,
+ },
+ ])))
+ },
+ );
+
+ cx.simulate_input("@symbol ");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) @symbol ")
+ );
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(current_completion_labels(editor), &["MySymbol"]);
+ });
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ let contents = message_editor
+ .update(&mut cx, |message_editor, cx| {
+ message_editor
+ .mention_set()
+ .contents(&all_prompt_capabilities, cx)
+ })
+ .await
+ .unwrap()
+ .into_values()
+ .collect::<Vec<_>>();
+
+ {
+ let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else {
+ panic!("Unexpected mentions");
+ };
+ pretty_assertions::assert_eq!(content, "1");
+ pretty_assertions::assert_eq!(
+ uri,
+ &format!("{url_one}?symbol=MySymbol#L1:1")
+ .parse::<MentionUri>()
+ .unwrap()
+ );
+ }
+
+ cx.run_until_parked();
+
+ editor.read_with(&cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
+ );
+ });
+
+ // Try to mention an "image" file that will fail to load
+ cx.simulate_input("@file x.png");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
+ );
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
+ });
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ // Getting the message contents fails
+ message_editor
+ .update(&mut cx, |message_editor, cx| {
+ message_editor
+ .mention_set()
+ .contents(&all_prompt_capabilities, cx)
+ })
+ .await
+ .expect_err("Should fail to load x.png");
+
+ cx.run_until_parked();
+
+ // Mention was removed
+ editor.read_with(&cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
+ );
+ });
+
+ // Once more
+ cx.simulate_input("@file x.png");
+
+ editor.update(&mut cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) @file x.png")
+ );
+ assert!(editor.has_visible_completions_menu());
+ assert_eq!(current_completion_labels(editor), &["x.png dir/"]);
+ });
+
+ editor.update_in(&mut cx, |editor, window, cx| {
+ editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
+ });
+
+ // This time don't immediately get the contents, just let the confirmed completion settle
+ cx.run_until_parked();
+
+ // Mention was removed
+ editor.read_with(&cx, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) [@MySymbol]({url_one}?symbol=MySymbol#L1:1) ")
+ );
+ });
+
+ // Now getting the contents succeeds, because the invalid mention was removed
+ let contents = message_editor
+ .update(&mut cx, |message_editor, cx| {
+ message_editor
+ .mention_set()
+ .contents(&all_prompt_capabilities, cx)
+ })
+ .await
+ .unwrap();
+ assert_eq!(contents.len(), 3);
+ }
+
+ fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec<Range<Point>> {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ editor.display_map.update(cx, |display_map, cx| {
+ display_map
+ .snapshot(cx)
+ .folds_in_range(0..snapshot.len())
+ .map(|fold| fold.range.to_point(&snapshot))
+ .collect()
+ })
+ }
+
+ fn current_completion_labels(editor: &Editor) -> Vec<String> {
+ let completions = editor.current_completions().expect("Missing completions");
+ completions
+ .into_iter()
+ .map(|completion| completion.label.text)
+ .collect::<Vec<_>>()
+ }
+
+ fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
+ let completions = editor.current_completions().expect("Missing completions");
+ completions
+ .into_iter()
+ .map(|completion| {
+ (
+ completion.label.text,
+ completion
+ .documentation
+ .map(|d| d.text().to_string())
+ .unwrap_or_default(),
+ )
+ })
+ .collect::<Vec<_>>()
+ }
+}
@@ -1,88 +0,0 @@
-pub struct MessageHistory<T> {
- items: Vec<T>,
- current: Option<usize>,
-}
-
-impl<T> Default for MessageHistory<T> {
- fn default() -> Self {
- MessageHistory {
- items: Vec::new(),
- current: None,
- }
- }
-}
-
-impl<T> MessageHistory<T> {
- pub fn push(&mut self, message: T) {
- self.current.take();
- self.items.push(message);
- }
-
- pub fn reset_position(&mut self) {
- self.current.take();
- }
-
- pub fn prev(&mut self) -> Option<&T> {
- if self.items.is_empty() {
- return None;
- }
-
- let new_ix = self
- .current
- .get_or_insert(self.items.len())
- .saturating_sub(1);
-
- self.current = Some(new_ix);
- self.items.get(new_ix)
- }
-
- pub fn next(&mut self) -> Option<&T> {
- let current = self.current.as_mut()?;
- *current += 1;
-
- self.items.get(*current).or_else(|| {
- self.current.take();
- None
- })
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_prev_next() {
- let mut history = MessageHistory::default();
-
- // Test empty history
- assert_eq!(history.prev(), None);
- assert_eq!(history.next(), None);
-
- // Add some messages
- history.push("first");
- history.push("second");
- history.push("third");
-
- // Test prev navigation
- assert_eq!(history.prev(), Some(&"third"));
- assert_eq!(history.prev(), Some(&"second"));
- assert_eq!(history.prev(), Some(&"first"));
- assert_eq!(history.prev(), Some(&"first"));
-
- assert_eq!(history.next(), Some(&"second"));
-
- // Test mixed navigation
- history.push("fourth");
- assert_eq!(history.prev(), Some(&"fourth"));
- assert_eq!(history.prev(), Some(&"third"));
- assert_eq!(history.next(), Some(&"fourth"));
- assert_eq!(history.next(), None);
-
- // Test that push resets navigation
- history.prev();
- history.prev();
- history.push("fifth");
- assert_eq!(history.prev(), Some(&"fifth"));
- }
-}
@@ -0,0 +1,230 @@
+use acp_thread::AgentSessionModes;
+use agent_client_protocol as acp;
+use agent_servers::AgentServer;
+use fs::Fs;
+use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
+use std::{rc::Rc, sync::Arc};
+use ui::{
+ Button, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
+ prelude::*,
+};
+
+use crate::{CycleModeSelector, ToggleProfileSelector};
+
+pub struct ModeSelector {
+ connection: Rc<dyn AgentSessionModes>,
+ agent_server: Rc<dyn AgentServer>,
+ menu_handle: PopoverMenuHandle<ContextMenu>,
+ focus_handle: FocusHandle,
+ fs: Arc<dyn Fs>,
+ setting_mode: bool,
+}
+
+impl ModeSelector {
+ pub fn new(
+ session_modes: Rc<dyn AgentSessionModes>,
+ agent_server: Rc<dyn AgentServer>,
+ fs: Arc<dyn Fs>,
+ focus_handle: FocusHandle,
+ ) -> Self {
+ Self {
+ connection: session_modes,
+ agent_server,
+ menu_handle: PopoverMenuHandle::default(),
+ fs,
+ setting_mode: false,
+ focus_handle,
+ }
+ }
+
+ pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
+ self.menu_handle.clone()
+ }
+
+ pub fn cycle_mode(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
+ let all_modes = self.connection.all_modes();
+ let current_mode = self.connection.current_mode();
+
+ let current_index = all_modes
+ .iter()
+ .position(|mode| mode.id.0 == current_mode.0)
+ .unwrap_or(0);
+
+ let next_index = (current_index + 1) % all_modes.len();
+ self.set_mode(all_modes[next_index].id.clone(), cx);
+ }
+
+ pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) {
+ let task = self.connection.set_mode(mode, cx);
+ self.setting_mode = true;
+ cx.notify();
+
+ cx.spawn(async move |this: WeakEntity<ModeSelector>, cx| {
+ if let Err(err) = task.await {
+ log::error!("Failed to set session mode: {:?}", err);
+ }
+ this.update(cx, |this, cx| {
+ this.setting_mode = false;
+ cx.notify();
+ })
+ .ok();
+ })
+ .detach();
+ }
+
+ fn build_context_menu(
+ &self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Entity<ContextMenu> {
+ let weak_self = cx.weak_entity();
+
+ ContextMenu::build(window, cx, move |mut menu, _window, cx| {
+ let all_modes = self.connection.all_modes();
+ let current_mode = self.connection.current_mode();
+ let default_mode = self.agent_server.default_mode(cx);
+
+ for mode in all_modes {
+ let is_selected = &mode.id == ¤t_mode;
+ let is_default = Some(&mode.id) == default_mode.as_ref();
+ let entry = ContextMenuEntry::new(mode.name.clone())
+ .toggleable(IconPosition::End, is_selected);
+
+ let entry = if let Some(description) = &mode.description {
+ entry.documentation_aside(ui::DocumentationSide::Left, {
+ let description = description.clone();
+
+ move |cx| {
+ v_flex()
+ .gap_1()
+ .child(Label::new(description.clone()))
+ .child(
+ h_flex()
+ .pt_1()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
+ .gap_0p5()
+ .text_sm()
+ .text_color(Color::Muted.color(cx))
+ .child("Hold")
+ .child(div().pt_0p5().children(ui::render_modifiers(
+ &gpui::Modifiers::secondary_key(),
+ PlatformStyle::platform(),
+ None,
+ Some(ui::TextSize::Default.rems(cx).into()),
+ true,
+ )))
+ .child(div().map(|this| {
+ if is_default {
+ this.child("to also unset as default")
+ } else {
+ this.child("to also set as default")
+ }
+ })),
+ )
+ .into_any_element()
+ }
+ })
+ } else {
+ entry
+ };
+
+ menu.push_item(entry.handler({
+ let mode_id = mode.id.clone();
+ let weak_self = weak_self.clone();
+ move |window, cx| {
+ weak_self
+ .update(cx, |this, cx| {
+ if window.modifiers().secondary() {
+ this.agent_server.set_default_mode(
+ if is_default {
+ None
+ } else {
+ Some(mode_id.clone())
+ },
+ this.fs.clone(),
+ cx,
+ );
+ }
+
+ this.set_mode(mode_id.clone(), cx);
+ })
+ .ok();
+ }
+ }));
+ }
+
+ menu.key_context("ModeSelector")
+ })
+ }
+}
+
+impl Render for ModeSelector {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let current_mode_id = self.connection.current_mode();
+ let current_mode_name = self
+ .connection
+ .all_modes()
+ .iter()
+ .find(|mode| mode.id == current_mode_id)
+ .map(|mode| mode.name.clone())
+ .unwrap_or_else(|| "Unknown".into());
+
+ let this = cx.entity();
+
+ let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
+ .label_size(LabelSize::Small)
+ .style(ButtonStyle::Subtle)
+ .color(Color::Muted)
+ .icon(IconName::ChevronDown)
+ .icon_size(IconSize::XSmall)
+ .icon_position(IconPosition::End)
+ .icon_color(Color::Muted)
+ .disabled(self.setting_mode);
+
+ PopoverMenu::new("mode-selector")
+ .trigger_with_tooltip(
+ trigger_button,
+ Tooltip::element({
+ let focus_handle = self.focus_handle.clone();
+ move |window, cx| {
+ v_flex()
+ .gap_1()
+ .child(
+ h_flex()
+ .pb_1()
+ .gap_2()
+ .justify_between()
+ .border_b_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(Label::new("Cycle Through Modes"))
+ .children(KeyBinding::for_action_in(
+ &CycleModeSelector,
+ &focus_handle,
+ window,
+ cx,
+ )),
+ )
+ .child(
+ h_flex()
+ .gap_2()
+ .justify_between()
+ .child(Label::new("Toggle Mode Menu"))
+ .children(KeyBinding::for_action_in(
+ &ToggleProfileSelector,
+ &focus_handle,
+ window,
+ cx,
+ )),
+ )
+ .into_any()
+ }
+ }),
+ )
+ .anchor(gpui::Corner::BottomRight)
+ .with_handle(self.menu_handle.clone())
+ .menu(move |window, cx| {
+ Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
+ })
+ }
+}
@@ -73,11 +73,8 @@ impl AcpModelPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate.models = models.ok();
this.delegate.selected_model = selected_model.ok();
- this.delegate.update_matches(this.query(cx), window, cx)
- })?
- .await;
-
- Ok(())
+ this.refresh(window, cx)
+ })
}
refresh(&this, &session_id, cx).await.log_err();
@@ -195,8 +192,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
}
}
- fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
- cx.emit(DismissEvent);
+ fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ cx.defer_in(window, |picker, window, cx| {
+ picker.set_query("", window, cx);
+ });
}
fn render_match(
@@ -330,7 +329,7 @@ async fn fuzzy_search(
.collect::<Vec<_>>();
let mut matches = match_strings(
&candidates,
- &query,
+ query,
false,
true,
100,
@@ -5,7 +5,8 @@ use agent_client_protocol as acp;
use gpui::{Entity, FocusHandle};
use picker::popover_menu::PickerPopoverMenu;
use ui::{
- ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, Tooltip, Window, prelude::*,
+ ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, TintColor, Tooltip, Window,
+ prelude::*,
};
use zed_actions::agent::ToggleModelSelector;
@@ -36,6 +37,14 @@ impl AcpModelSelectorPopover {
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
self.menu_handle.toggle(window, cx);
}
+
+ pub fn active_model_name(&self, cx: &App) -> Option<SharedString> {
+ self.selector
+ .read(cx)
+ .delegate
+ .active_model()
+ .map(|model| model.name.clone())
+ }
}
impl Render for AcpModelSelectorPopover {
@@ -50,15 +59,22 @@ impl Render for AcpModelSelectorPopover {
let focus_handle = self.focus_handle.clone();
+ let color = if self.menu_handle.is_deployed() {
+ Color::Accent
+ } else {
+ Color::Muted
+ };
+
PickerPopoverMenu::new(
self.selector.clone(),
ButtonLike::new("active-model")
.when_some(model_icon, |this, icon| {
- this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall))
+ this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
})
+ .selected_style(ButtonStyle::Tinted(TintColor::Accent))
.child(
Label::new(model_name)
- .color(Color::Muted)
+ .color(color)
.size(LabelSize::Small)
.ml_0p5(),
)
@@ -0,0 +1,825 @@
+use crate::acp::AcpThreadView;
+use crate::{AgentPanel, RemoveSelectedThread};
+use agent2::{HistoryEntry, HistoryStore};
+use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
+use editor::{Editor, EditorEvent};
+use fuzzy::StringMatchCandidate;
+use gpui::{
+ App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
+ UniformListScrollHandle, WeakEntity, Window, uniform_list,
+};
+use std::{fmt::Display, ops::Range};
+use text::Bias;
+use time::{OffsetDateTime, UtcOffset};
+use ui::{
+ HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
+ Tooltip, prelude::*,
+};
+
+pub struct AcpThreadHistory {
+ pub(crate) history_store: Entity<HistoryStore>,
+ scroll_handle: UniformListScrollHandle,
+ selected_index: usize,
+ hovered_index: Option<usize>,
+ search_editor: Entity<Editor>,
+ search_query: SharedString,
+
+ visible_items: Vec<ListItemType>,
+
+ scrollbar_visibility: bool,
+ scrollbar_state: ScrollbarState,
+ local_timezone: UtcOffset,
+
+ _update_task: Task<()>,
+ _subscriptions: Vec<gpui::Subscription>,
+}
+
+enum ListItemType {
+ BucketSeparator(TimeBucket),
+ Entry {
+ entry: HistoryEntry,
+ format: EntryTimeFormat,
+ },
+ SearchResult {
+ entry: HistoryEntry,
+ positions: Vec<usize>,
+ },
+}
+
+impl ListItemType {
+ fn history_entry(&self) -> Option<&HistoryEntry> {
+ match self {
+ ListItemType::Entry { entry, .. } => Some(entry),
+ ListItemType::SearchResult { entry, .. } => Some(entry),
+ _ => None,
+ }
+ }
+}
+
+pub enum ThreadHistoryEvent {
+ Open(HistoryEntry),
+}
+
+impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
+
+impl AcpThreadHistory {
+ pub(crate) fn new(
+ history_store: Entity<agent2::HistoryStore>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let search_editor = cx.new(|cx| {
+ let mut editor = Editor::single_line(window, cx);
+ editor.set_placeholder_text("Search threads...", window, cx);
+ editor
+ });
+
+ let search_editor_subscription =
+ cx.subscribe(&search_editor, |this, search_editor, event, cx| {
+ if let EditorEvent::BufferEdited = event {
+ let query = search_editor.read(cx).text(cx);
+ if this.search_query != query {
+ this.search_query = query.into();
+ this.update_visible_items(false, cx);
+ }
+ }
+ });
+
+ let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
+ this.update_visible_items(true, cx);
+ });
+
+ let scroll_handle = UniformListScrollHandle::default();
+ let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
+
+ let mut this = Self {
+ history_store,
+ scroll_handle,
+ selected_index: 0,
+ hovered_index: None,
+ visible_items: Default::default(),
+ search_editor,
+ scrollbar_visibility: true,
+ scrollbar_state,
+ local_timezone: UtcOffset::from_whole_seconds(
+ chrono::Local::now().offset().local_minus_utc(),
+ )
+ .unwrap(),
+ search_query: SharedString::default(),
+ _subscriptions: vec![search_editor_subscription, history_store_subscription],
+ _update_task: Task::ready(()),
+ };
+ this.update_visible_items(false, cx);
+ this
+ }
+
+ fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
+ let entries = self
+ .history_store
+ .update(cx, |store, _| store.entries().collect());
+ let new_list_items = if self.search_query.is_empty() {
+ self.add_list_separators(entries, cx)
+ } else {
+ self.filter_search_results(entries, cx)
+ };
+ let selected_history_entry = if preserve_selected_item {
+ self.selected_history_entry().cloned()
+ } else {
+ None
+ };
+
+ self._update_task = cx.spawn(async move |this, cx| {
+ let new_visible_items = new_list_items.await;
+ this.update(cx, |this, cx| {
+ let new_selected_index = if let Some(history_entry) = selected_history_entry {
+ let history_entry_id = history_entry.id();
+ new_visible_items
+ .iter()
+ .position(|visible_entry| {
+ visible_entry
+ .history_entry()
+ .is_some_and(|entry| entry.id() == history_entry_id)
+ })
+ .unwrap_or(0)
+ } else {
+ 0
+ };
+
+ this.visible_items = new_visible_items;
+ this.set_selected_index(new_selected_index, Bias::Right, cx);
+ cx.notify();
+ })
+ .ok();
+ });
+ }
+
+ fn add_list_separators(&self, entries: Vec<HistoryEntry>, cx: &App) -> Task<Vec<ListItemType>> {
+ cx.background_spawn(async move {
+ let mut items = Vec::with_capacity(entries.len() + 1);
+ let mut bucket = None;
+ let today = Local::now().naive_local().date();
+
+ for entry in entries.into_iter() {
+ let entry_date = entry
+ .updated_at()
+ .with_timezone(&Local)
+ .naive_local()
+ .date();
+ let entry_bucket = TimeBucket::from_dates(today, entry_date);
+
+ if Some(entry_bucket) != bucket {
+ bucket = Some(entry_bucket);
+ items.push(ListItemType::BucketSeparator(entry_bucket));
+ }
+
+ items.push(ListItemType::Entry {
+ entry,
+ format: entry_bucket.into(),
+ });
+ }
+ items
+ })
+ }
+
+ fn filter_search_results(
+ &self,
+ entries: Vec<HistoryEntry>,
+ cx: &App,
+ ) -> Task<Vec<ListItemType>> {
+ let query = self.search_query.clone();
+ cx.background_spawn({
+ let executor = cx.background_executor().clone();
+ async move {
+ let mut candidates = Vec::with_capacity(entries.len());
+
+ for (idx, entry) in entries.iter().enumerate() {
+ candidates.push(StringMatchCandidate::new(idx, entry.title()));
+ }
+
+ const MAX_MATCHES: usize = 100;
+
+ let matches = fuzzy::match_strings(
+ &candidates,
+ &query,
+ false,
+ true,
+ MAX_MATCHES,
+ &Default::default(),
+ executor,
+ )
+ .await;
+
+ matches
+ .into_iter()
+ .map(|search_match| ListItemType::SearchResult {
+ entry: entries[search_match.candidate_id].clone(),
+ positions: search_match.positions,
+ })
+ .collect()
+ }
+ })
+ }
+
+ fn search_produced_no_matches(&self) -> bool {
+ self.visible_items.is_empty() && !self.search_query.is_empty()
+ }
+
+ fn selected_history_entry(&self) -> Option<&HistoryEntry> {
+ self.get_history_entry(self.selected_index)
+ }
+
+ fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> {
+ self.visible_items.get(visible_items_ix)?.history_entry()
+ }
+
+ fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
+ if self.visible_items.len() == 0 {
+ self.selected_index = 0;
+ return;
+ }
+ while matches!(
+ self.visible_items.get(index),
+ None | Some(ListItemType::BucketSeparator(..))
+ ) {
+ index = match bias {
+ Bias::Left => {
+ if index == 0 {
+ self.visible_items.len() - 1
+ } else {
+ index - 1
+ }
+ }
+ Bias::Right => {
+ if index >= self.visible_items.len() - 1 {
+ 0
+ } else {
+ index + 1
+ }
+ }
+ };
+ }
+ self.selected_index = index;
+ self.scroll_handle
+ .scroll_to_item(index, ScrollStrategy::Top);
+ cx.notify()
+ }
+
+ pub fn select_previous(
+ &mut self,
+ _: &menu::SelectPrevious,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.selected_index == 0 {
+ self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
+ } else {
+ self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
+ }
+ }
+
+ pub fn select_next(
+ &mut self,
+ _: &menu::SelectNext,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.selected_index == self.visible_items.len() - 1 {
+ self.set_selected_index(0, Bias::Right, cx);
+ } else {
+ self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
+ }
+ }
+
+ fn select_first(
+ &mut self,
+ _: &menu::SelectFirst,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.set_selected_index(0, Bias::Right, cx);
+ }
+
+ fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
+ self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
+ }
+
+ fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
+ self.confirm_entry(self.selected_index, cx);
+ }
+
+ fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
+ let Some(entry) = self.get_history_entry(ix) else {
+ return;
+ };
+ cx.emit(ThreadHistoryEvent::Open(entry.clone()));
+ }
+
+ fn remove_selected_thread(
+ &mut self,
+ _: &RemoveSelectedThread,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.remove_thread(self.selected_index, cx)
+ }
+
+ fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
+ let Some(entry) = self.get_history_entry(visible_item_ix) else {
+ return;
+ };
+
+ let task = match entry {
+ HistoryEntry::AcpThread(thread) => self
+ .history_store
+ .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
+ HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| {
+ this.delete_text_thread(context.path.clone(), cx)
+ }),
+ };
+ task.detach_and_log_err(cx);
+ }
+
+ fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
+ if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
+ return None;
+ }
+
+ Some(
+ div()
+ .occlude()
+ .id("thread-history-scroll")
+ .h_full()
+ .bg(cx.theme().colors().panel_background.opacity(0.8))
+ .border_l_1()
+ .border_color(cx.theme().colors().border_variant)
+ .absolute()
+ .right_1()
+ .top_0()
+ .bottom_0()
+ .w_4()
+ .pl_1()
+ .cursor_default()
+ .on_mouse_move(cx.listener(|_, _, _window, cx| {
+ cx.notify();
+ cx.stop_propagation()
+ }))
+ .on_hover(|_, _window, cx| {
+ cx.stop_propagation();
+ })
+ .on_any_mouse_down(|_, _window, cx| {
+ cx.stop_propagation();
+ })
+ .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
+ cx.notify();
+ }))
+ .children(Scrollbar::vertical(self.scrollbar_state.clone())),
+ )
+ }
+
+ fn render_list_items(
+ &mut self,
+ range: Range<usize>,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Vec<AnyElement> {
+ self.visible_items
+ .get(range.clone())
+ .into_iter()
+ .flatten()
+ .enumerate()
+ .map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
+ .collect()
+ }
+
+ fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
+ match item {
+ ListItemType::Entry { entry, format } => self
+ .render_history_entry(entry, *format, ix, Vec::default(), cx)
+ .into_any(),
+ ListItemType::SearchResult { entry, positions } => self.render_history_entry(
+ entry,
+ EntryTimeFormat::DateAndTime,
+ ix,
+ positions.clone(),
+ cx,
+ ),
+ ListItemType::BucketSeparator(bucket) => div()
+ .px(DynamicSpacing::Base06.rems(cx))
+ .pt_2()
+ .pb_1()
+ .child(
+ Label::new(bucket.to_string())
+ .size(LabelSize::XSmall)
+ .color(Color::Muted),
+ )
+ .into_any_element(),
+ }
+ }
+
+ fn render_history_entry(
+ &self,
+ entry: &HistoryEntry,
+ format: EntryTimeFormat,
+ ix: usize,
+ highlight_positions: Vec<usize>,
+ cx: &Context<Self>,
+ ) -> AnyElement {
+ let selected = ix == self.selected_index;
+ let hovered = Some(ix) == self.hovered_index;
+ let timestamp = entry.updated_at().timestamp();
+ let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
+
+ h_flex()
+ .w_full()
+ .pb_1()
+ .child(
+ ListItem::new(ix)
+ .rounded()
+ .toggle_state(selected)
+ .spacing(ListItemSpacing::Sparse)
+ .start_slot(
+ h_flex()
+ .w_full()
+ .gap_2()
+ .justify_between()
+ .child(
+ HighlightedLabel::new(entry.title(), highlight_positions)
+ .size(LabelSize::Small)
+ .truncate(),
+ )
+ .child(
+ Label::new(thread_timestamp)
+ .color(Color::Muted)
+ .size(LabelSize::XSmall),
+ ),
+ )
+ .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
+ if *is_hovered {
+ this.hovered_index = Some(ix);
+ } else if this.hovered_index == Some(ix) {
+ this.hovered_index = None;
+ }
+
+ cx.notify();
+ }))
+ .end_slot::<IconButton>(if hovered {
+ Some(
+ IconButton::new("delete", IconName::Trash)
+ .shape(IconButtonShape::Square)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .tooltip(move |window, cx| {
+ Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
+ })
+ .on_click(
+ cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)),
+ ),
+ )
+ } else {
+ None
+ })
+ .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))),
+ )
+ .into_any_element()
+ }
+}
+
+impl Focusable for AcpThreadHistory {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.search_editor.focus_handle(cx)
+ }
+}
+
+impl Render for AcpThreadHistory {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ v_flex()
+ .key_context("ThreadHistory")
+ .size_full()
+ .on_action(cx.listener(Self::select_previous))
+ .on_action(cx.listener(Self::select_next))
+ .on_action(cx.listener(Self::select_first))
+ .on_action(cx.listener(Self::select_last))
+ .on_action(cx.listener(Self::confirm))
+ .on_action(cx.listener(Self::remove_selected_thread))
+ .when(!self.history_store.read(cx).is_empty(cx), |parent| {
+ parent.child(
+ h_flex()
+ .h(px(41.)) // Match the toolbar perfectly
+ .w_full()
+ .py_1()
+ .px_2()
+ .gap_2()
+ .justify_between()
+ .border_b_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Icon::new(IconName::MagnifyingGlass)
+ .color(Color::Muted)
+ .size(IconSize::Small),
+ )
+ .child(self.search_editor.clone()),
+ )
+ })
+ .child({
+ let view = v_flex()
+ .id("list-container")
+ .relative()
+ .overflow_hidden()
+ .flex_grow();
+
+ if self.history_store.read(cx).is_empty(cx) {
+ view.justify_center()
+ .child(
+ h_flex().w_full().justify_center().child(
+ Label::new("You don't have any past threads yet.")
+ .size(LabelSize::Small),
+ ),
+ )
+ } else if self.search_produced_no_matches() {
+ view.justify_center().child(
+ h_flex().w_full().justify_center().child(
+ Label::new("No threads match your search.").size(LabelSize::Small),
+ ),
+ )
+ } else {
+ view.pr_5()
+ .child(
+ uniform_list(
+ "thread-history",
+ self.visible_items.len(),
+ cx.processor(|this, range: Range<usize>, window, cx| {
+ this.render_list_items(range, window, cx)
+ }),
+ )
+ .p_1()
+ .track_scroll(self.scroll_handle.clone())
+ .flex_grow(),
+ )
+ .when_some(self.render_scrollbar(cx), |div, scrollbar| {
+ div.child(scrollbar)
+ })
+ }
+ })
+ }
+}
+
+#[derive(IntoElement)]
+pub struct AcpHistoryEntryElement {
+ entry: HistoryEntry,
+ thread_view: WeakEntity<AcpThreadView>,
+ selected: bool,
+ hovered: bool,
+ on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
+}
+
+impl AcpHistoryEntryElement {
+ pub fn new(entry: HistoryEntry, thread_view: WeakEntity<AcpThreadView>) -> Self {
+ Self {
+ entry,
+ thread_view,
+ selected: false,
+ hovered: false,
+ on_hover: Box::new(|_, _, _| {}),
+ }
+ }
+
+ pub fn hovered(mut self, hovered: bool) -> Self {
+ self.hovered = hovered;
+ self
+ }
+
+ pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
+ self.on_hover = Box::new(on_hover);
+ self
+ }
+}
+
+impl RenderOnce for AcpHistoryEntryElement {
+ fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
+ let id = self.entry.id();
+ let title = self.entry.title();
+ let timestamp = self.entry.updated_at();
+
+ let formatted_time = {
+ let now = chrono::Utc::now();
+ let duration = now.signed_duration_since(timestamp);
+
+ if duration.num_days() > 0 {
+ format!("{}d", duration.num_days())
+ } else if duration.num_hours() > 0 {
+ format!("{}h ago", duration.num_hours())
+ } else if duration.num_minutes() > 0 {
+ format!("{}m ago", duration.num_minutes())
+ } else {
+ "Just now".to_string()
+ }
+ };
+
+ ListItem::new(id)
+ .rounded()
+ .toggle_state(self.selected)
+ .spacing(ListItemSpacing::Sparse)
+ .start_slot(
+ h_flex()
+ .w_full()
+ .gap_2()
+ .justify_between()
+ .child(Label::new(title).size(LabelSize::Small).truncate())
+ .child(
+ Label::new(formatted_time)
+ .color(Color::Muted)
+ .size(LabelSize::XSmall),
+ ),
+ )
+ .on_hover(self.on_hover)
+ .end_slot::<IconButton>(if self.hovered || self.selected {
+ Some(
+ IconButton::new("delete", IconName::Trash)
+ .shape(IconButtonShape::Square)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .tooltip(move |window, cx| {
+ Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
+ })
+ .on_click({
+ let thread_view = self.thread_view.clone();
+ let entry = self.entry.clone();
+
+ move |_event, _window, cx| {
+ if let Some(thread_view) = thread_view.upgrade() {
+ thread_view.update(cx, |thread_view, cx| {
+ thread_view.delete_history_entry(entry.clone(), cx);
+ });
+ }
+ }
+ }),
+ )
+ } else {
+ None
+ })
+ .on_click({
+ let thread_view = self.thread_view.clone();
+ let entry = self.entry;
+
+ move |_event, window, cx| {
+ if let Some(workspace) = thread_view
+ .upgrade()
+ .and_then(|view| view.read(cx).workspace().upgrade())
+ {
+ match &entry {
+ HistoryEntry::AcpThread(thread_metadata) => {
+ if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
+ panel.update(cx, |panel, cx| {
+ panel.load_agent_thread(
+ thread_metadata.clone(),
+ window,
+ cx,
+ );
+ });
+ }
+ }
+ HistoryEntry::TextThread(context) => {
+ if let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) {
+ panel.update(cx, |panel, cx| {
+ panel
+ .open_saved_prompt_editor(
+ context.path.clone(),
+ window,
+ cx,
+ )
+ .detach_and_log_err(cx);
+ });
+ }
+ }
+ }
+ }
+ }
+ })
+ }
+}
+
+#[derive(Clone, Copy)]
+pub enum EntryTimeFormat {
+ DateAndTime,
+ TimeOnly,
+}
+
+impl EntryTimeFormat {
+ fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String {
+ let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
+
+ match self {
+ EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp(
+ timestamp,
+ OffsetDateTime::now_utc(),
+ timezone,
+ time_format::TimestampFormat::EnhancedAbsolute,
+ ),
+ EntryTimeFormat::TimeOnly => time_format::format_time(timestamp),
+ }
+ }
+}
+
+impl From<TimeBucket> for EntryTimeFormat {
+ fn from(bucket: TimeBucket) -> Self {
+ match bucket {
+ TimeBucket::Today => EntryTimeFormat::TimeOnly,
+ TimeBucket::Yesterday => EntryTimeFormat::TimeOnly,
+ TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime,
+ TimeBucket::PastWeek => EntryTimeFormat::DateAndTime,
+ TimeBucket::All => EntryTimeFormat::DateAndTime,
+ }
+ }
+}
+
+#[derive(PartialEq, Eq, Clone, Copy, Debug)]
+enum TimeBucket {
+ Today,
+ Yesterday,
+ ThisWeek,
+ PastWeek,
+ All,
+}
+
+impl TimeBucket {
+ fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {
+ if date == reference {
+ return TimeBucket::Today;
+ }
+
+ if date == reference - TimeDelta::days(1) {
+ return TimeBucket::Yesterday;
+ }
+
+ let week = date.iso_week();
+
+ if reference.iso_week() == week {
+ return TimeBucket::ThisWeek;
+ }
+
+ let last_week = (reference - TimeDelta::days(7)).iso_week();
+
+ if week == last_week {
+ return TimeBucket::PastWeek;
+ }
+
+ TimeBucket::All
+ }
+}
+
+impl Display for TimeBucket {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ TimeBucket::Today => write!(f, "Today"),
+ TimeBucket::Yesterday => write!(f, "Yesterday"),
+ TimeBucket::ThisWeek => write!(f, "This Week"),
+ TimeBucket::PastWeek => write!(f, "Past Week"),
+ TimeBucket::All => write!(f, "All"),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use chrono::NaiveDate;
+
+ #[test]
+ fn test_time_bucket_from_dates() {
+ let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
+
+ let date = today;
+ assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today);
+
+ let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap();
+ assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday);
+
+ let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap();
+ assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
+
+ let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap();
+ assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
+
+ let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap();
+ assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
+
+ let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap();
+ assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
+
+ // All: not in this week or last week
+ let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
+ assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All);
+
+ // Test year boundary cases
+ let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
+
+ let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap();
+ assert_eq!(
+ TimeBucket::from_dates(new_year, date),
+ TimeBucket::Yesterday
+ );
+
+ let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap();
+ assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek);
+ }
+}
@@ -1,84 +1,285 @@
use acp_thread::{
AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
- LoadError, MentionUri, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, UserMessageId,
+ AuthRequired, LoadError, MentionUri, RetryStatus, ThreadStatus, ToolCall, ToolCallContent,
+ ToolCallStatus, UserMessageId,
};
use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog;
-use agent::{TextThreadStore, ThreadStore};
-use agent_client_protocol as acp;
-use agent_servers::AgentServer;
-use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
+use agent_client_protocol::{self as acp, PromptCapabilities};
+use agent_servers::{AgentServer, AgentServerDelegate};
+use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
+use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
+use anyhow::{Context as _, Result, anyhow, bail};
+use arrayvec::ArrayVec;
use audio::{Audio, Sound};
use buffer_diff::BufferDiff;
+use client::zed_urls;
+use cloud_llm_client::PlanV1;
use collections::{HashMap, HashSet};
use editor::scroll::Autoscroll;
-use editor::{
- AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
- EditorStyle, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects,
-};
+use editor::{Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects};
use file_icons::FileIcons;
+use fs::Fs;
+use futures::FutureExt as _;
use gpui::{
- Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
- FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay,
- SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement,
- Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop,
- linear_gradient, list, percentage, point, prelude::*, pulsating_between,
+ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
+ CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
+ ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
+ Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window,
+ WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*,
+ pulsating_between,
};
-use language::language_settings::SoftWrap;
-use language::{Buffer, Language};
+use language::Buffer;
+
+use language_model::LanguageModelRegistry;
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
-use parking_lot::Mutex;
-use project::{CompletionIntent, Project};
-use prompt_store::PromptId;
+use project::{Project, ProjectEntryId};
+use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::{Settings as _, SettingsStore};
-use std::fmt::Write as _;
-use std::path::PathBuf;
-use std::{
- cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
- time::Duration,
-};
-use terminal_view::TerminalView;
-use text::{Anchor, BufferSnapshot};
-use theme::ThemeSettings;
+use std::cell::{Cell, RefCell};
+use std::path::Path;
+use std::sync::Arc;
+use std::time::Instant;
+use std::{collections::BTreeMap, rc::Rc, time::Duration};
+use terminal_view::terminal_panel::TerminalPanel;
+use text::Anchor;
+use theme::{AgentFontSize, ThemeSettings};
use ui::{
- Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState,
- Tooltip, prelude::*,
+ Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
+ PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
-use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage, ToggleModelSelector};
+use zed_actions::agent::{Chat, ToggleModelSelector};
use zed_actions::assistant::OpenRulesLibrary;
+use super::entry_view_state::EntryViewState;
use crate::acp::AcpModelSelectorPopover;
-use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
-use crate::acp::message_history::MessageHistory;
+use crate::acp::ModeSelector;
+use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent};
+use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
use crate::agent_diff::AgentDiff;
-use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
-use crate::ui::{AgentNotification, AgentNotificationEvent};
+use crate::profile_selector::{ProfileProvider, ProfileSelector};
+
+use crate::ui::preview::UsageCallout;
+use crate::ui::{
+ AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip,
+};
use crate::{
- AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
+ AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
+ CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, OpenHistory, RejectAll,
+ RejectOnce, ToggleBurnMode, ToggleProfileSelector,
};
-const RESPONSE_PADDING_X: Pixels = px(19.);
+pub const MIN_EDITOR_LINES: usize = 4;
+pub const MAX_EDITOR_LINES: usize = 8;
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum ThreadFeedback {
+ Positive,
+ Negative,
+}
+
+#[derive(Debug)]
+enum ThreadError {
+ PaymentRequired,
+ ModelRequestLimitReached(cloud_llm_client::Plan),
+ ToolUseLimitReached,
+ Refusal,
+ AuthenticationRequired(SharedString),
+ Other(SharedString),
+}
+
+impl ThreadError {
+ fn from_err(error: anyhow::Error, agent: &Rc<dyn AgentServer>) -> Self {
+ if error.is::<language_model::PaymentRequiredError>() {
+ Self::PaymentRequired
+ } else if error.is::<language_model::ToolUseLimitReachedError>() {
+ Self::ToolUseLimitReached
+ } else if let Some(error) =
+ error.downcast_ref::<language_model::ModelRequestLimitReachedError>()
+ {
+ Self::ModelRequestLimitReached(error.plan)
+ } else if let Some(acp_error) = error.downcast_ref::<acp::Error>()
+ && acp_error.code == acp::ErrorCode::AUTH_REQUIRED.code
+ {
+ Self::AuthenticationRequired(acp_error.message.clone().into())
+ } else {
+ let string = error.to_string();
+ // TODO: we should have Gemini return better errors here.
+ if agent.clone().downcast::<agent_servers::Gemini>().is_some()
+ && string.contains("Could not load the default credentials")
+ || string.contains("API key not valid")
+ || string.contains("Request had invalid authentication credentials")
+ {
+ Self::AuthenticationRequired(string.into())
+ } else {
+ Self::Other(error.to_string().into())
+ }
+ }
+ }
+}
+
+impl ProfileProvider for Entity<agent2::Thread> {
+ fn profile_id(&self, cx: &App) -> AgentProfileId {
+ self.read(cx).profile().clone()
+ }
+
+ fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
+ self.update(cx, |thread, _cx| {
+ thread.set_profile(profile_id);
+ });
+ }
+
+ fn profiles_supported(&self, cx: &App) -> bool {
+ self.read(cx)
+ .model()
+ .is_some_and(|model| model.supports_tools())
+ }
+}
+
+#[derive(Default)]
+struct ThreadFeedbackState {
+ feedback: Option<ThreadFeedback>,
+ comments_editor: Option<Entity<Editor>>,
+}
+
+impl ThreadFeedbackState {
+ pub fn submit(
+ &mut self,
+ thread: Entity<AcpThread>,
+ feedback: ThreadFeedback,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ let Some(telemetry) = thread.read(cx).connection().telemetry() else {
+ return;
+ };
+
+ if self.feedback == Some(feedback) {
+ return;
+ }
+
+ self.feedback = Some(feedback);
+ match feedback {
+ ThreadFeedback::Positive => {
+ self.comments_editor = None;
+ }
+ ThreadFeedback::Negative => {
+ self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx));
+ }
+ }
+ let session_id = thread.read(cx).session_id().clone();
+ let agent_name = telemetry.agent_name();
+ let task = telemetry.thread_data(&session_id, cx);
+ let rating = match feedback {
+ ThreadFeedback::Positive => "positive",
+ ThreadFeedback::Negative => "negative",
+ };
+ cx.background_spawn(async move {
+ let thread = task.await?;
+ telemetry::event!(
+ "Agent Thread Rated",
+ session_id = session_id,
+ rating = rating,
+ agent = agent_name,
+ thread = thread
+ );
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) {
+ let Some(telemetry) = thread.read(cx).connection().telemetry() else {
+ return;
+ };
+
+ let Some(comments) = self
+ .comments_editor
+ .as_ref()
+ .map(|editor| editor.read(cx).text(cx))
+ .filter(|text| !text.trim().is_empty())
+ else {
+ return;
+ };
+
+ self.comments_editor.take();
+
+ let session_id = thread.read(cx).session_id().clone();
+ let agent_name = telemetry.agent_name();
+ let task = telemetry.thread_data(&session_id, cx);
+ cx.background_spawn(async move {
+ let thread = task.await?;
+ telemetry::event!(
+ "Agent Thread Feedback Comments",
+ session_id = session_id,
+ comments = comments,
+ agent = agent_name,
+ thread = thread
+ );
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ pub fn clear(&mut self) {
+ *self = Self::default()
+ }
+
+ pub fn dismiss_comments(&mut self) {
+ self.comments_editor.take();
+ }
+
+ fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> {
+ let buffer = cx.new(|cx| {
+ let empty_string = String::new();
+ MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
+ });
+
+ let editor = cx.new(|cx| {
+ let mut editor = Editor::new(
+ editor::EditorMode::AutoHeight {
+ min_lines: 1,
+ max_lines: Some(4),
+ },
+ buffer,
+ None,
+ window,
+ cx,
+ );
+ editor.set_placeholder_text(
+ "What went wrong? Share your feedback so we can improve.",
+ window,
+ cx,
+ );
+ editor
+ });
+
+ editor.read(cx).focus_handle(cx).focus(window);
+ editor
+ }
+}
pub struct AcpThreadView {
agent: Rc<dyn AgentServer>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
- thread_store: Entity<ThreadStore>,
- text_thread_store: Entity<TextThreadStore>,
thread_state: ThreadState,
- diff_editors: HashMap<EntityId, Entity<Editor>>,
- terminal_views: HashMap<EntityId, Entity<TerminalView>>,
- message_editor: Entity<Editor>,
+ login: Option<task::SpawnInTerminal>,
+ history_store: Entity<HistoryStore>,
+ hovered_recent_history_item: Option<usize>,
+ entry_view_state: Entity<EntryViewState>,
+ message_editor: Entity<MessageEditor>,
+ focus_handle: FocusHandle,
model_selector: Option<Entity<AcpModelSelectorPopover>>,
- message_set_from_history: Option<BufferSnapshot>,
- _message_editor_subscription: Subscription,
- mention_set: Arc<Mutex<MentionSet>>,
+ profile_selector: Option<Entity<ProfileSelector>>,
notifications: Vec<WindowHandle<AgentNotification>>,
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
- last_error: Option<Entity<Markdown>>,
+ thread_retry_status: Option<RetryStatus>,
+ thread_error: Option<ThreadError>,
+ thread_feedback: ThreadFeedbackState,
list_state: ListState,
scrollbar_state: ScrollbarState,
auth_task: Option<Task<()>>,
@@ -87,167 +288,218 @@ pub struct AcpThreadView {
edits_expanded: bool,
plan_expanded: bool,
editor_expanded: bool,
- terminal_expanded: bool,
- message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
+ should_be_following: bool,
+ editing_message: Option<usize>,
+ prompt_capabilities: Rc<Cell<PromptCapabilities>>,
+ available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+ is_loading_contents: bool,
+ new_server_version_available: Option<SharedString>,
_cancel_task: Option<Task<()>>,
- _subscriptions: [Subscription; 1],
+ _subscriptions: [Subscription; 4],
}
enum ThreadState {
- Loading {
- _task: Task<()>,
- },
+ Loading(Entity<LoadingView>),
Ready {
thread: Entity<AcpThread>,
- _subscription: [Subscription; 2],
+ title_editor: Option<Entity<Editor>>,
+ mode_selector: Option<Entity<ModeSelector>>,
+ _subscriptions: Vec<Subscription>,
},
LoadError(LoadError),
Unauthenticated {
connection: Rc<dyn AgentConnection>,
+ description: Option<Entity<Markdown>>,
+ configuration_view: Option<AnyView>,
+ pending_auth_method: Option<acp::AuthMethodId>,
+ _subscription: Option<Subscription>,
},
- ServerExited {
- status: ExitStatus,
- },
+}
+
+struct LoadingView {
+ title: SharedString,
+ _load_task: Task<()>,
+ _update_title_task: Task<anyhow::Result<()>>,
}
impl AcpThreadView {
pub fn new(
agent: Rc<dyn AgentServer>,
+ resume_thread: Option<DbThreadMetadata>,
+ summarize_thread: Option<DbThreadMetadata>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
- thread_store: Entity<ThreadStore>,
- text_thread_store: Entity<TextThreadStore>,
- message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
- min_lines: usize,
- max_lines: Option<usize>,
+ history_store: Entity<HistoryStore>,
+ prompt_store: Option<Entity<PromptStore>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
- let language = Language::new(
- language::LanguageConfig {
- completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
- ..Default::default()
- },
- None,
- );
-
- let mention_set = Arc::new(Mutex::new(MentionSet::default()));
+ let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
+ let available_commands = Rc::new(RefCell::new(vec![]));
+
+ let placeholder = if agent.name() == "Zed Agent" {
+ format!("Message the {} — @ to include context", agent.name())
+ } else if agent.name() == "Claude Code" || !available_commands.borrow().is_empty() {
+ format!(
+ "Message {} — @ to include context, / for commands",
+ agent.name()
+ )
+ } else {
+ format!("Message {} — @ to include context", agent.name())
+ };
let message_editor = cx.new(|cx| {
- let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
- let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
-
- let mut editor = Editor::new(
+ let mut editor = MessageEditor::new(
+ workspace.clone(),
+ project.clone(),
+ history_store.clone(),
+ prompt_store.clone(),
+ prompt_capabilities.clone(),
+ available_commands.clone(),
+ agent.name(),
+ &placeholder,
editor::EditorMode::AutoHeight {
- min_lines,
- max_lines: max_lines,
+ min_lines: MIN_EDITOR_LINES,
+ max_lines: Some(MAX_EDITOR_LINES),
},
- buffer,
- None,
window,
cx,
);
- editor.set_placeholder_text("Message the agent - @ to include files", cx);
- editor.set_show_indent_guides(false, cx);
- editor.set_soft_wrap();
- editor.set_use_modal_editing(true);
- editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
- mention_set.clone(),
- workspace.clone(),
- thread_store.downgrade(),
- text_thread_store.downgrade(),
- cx.weak_entity(),
- ))));
- editor.set_context_menu_options(ContextMenuOptions {
- min_entries_visible: 12,
- max_entries_visible: 12,
- placement: Some(ContextMenuPlacement::Above),
- });
+ if let Some(entry) = summarize_thread {
+ editor.insert_thread_summary(entry, window, cx);
+ }
editor
});
- let message_editor_subscription =
- cx.subscribe(&message_editor, |this, editor, event, cx| {
- if let editor::EditorEvent::BufferEdited = &event {
- let buffer = editor
- .read(cx)
- .buffer()
- .read(cx)
- .as_singleton()
- .unwrap()
- .read(cx)
- .snapshot();
- if let Some(message) = this.message_set_from_history.clone()
- && message.version() != buffer.version()
- {
- this.message_set_from_history = None;
- }
-
- if this.message_set_from_history.is_none() {
- this.message_history.borrow_mut().reset_position();
- }
- }
- });
-
- let mention_set = mention_set.clone();
-
let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
- let subscription = cx.observe_global_in::<SettingsStore>(window, Self::settings_changed);
+ let entry_view_state = cx.new(|_| {
+ EntryViewState::new(
+ workspace.clone(),
+ project.clone(),
+ history_store.clone(),
+ prompt_store.clone(),
+ prompt_capabilities.clone(),
+ available_commands.clone(),
+ agent.name(),
+ )
+ });
+
+ let subscriptions = [
+ cx.observe_global_in::<SettingsStore>(window, Self::agent_font_size_changed),
+ cx.observe_global_in::<AgentFontSize>(window, Self::agent_font_size_changed),
+ cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
+ cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
+ ];
Self {
agent: agent.clone(),
workspace: workspace.clone(),
project: project.clone(),
- thread_store,
- text_thread_store,
- thread_state: Self::initial_state(agent, workspace, project, window, cx),
+ entry_view_state,
+ thread_state: Self::initial_state(agent, resume_thread, workspace, project, window, cx),
+ login: None,
message_editor,
model_selector: None,
- message_set_from_history: None,
- _message_editor_subscription: message_editor_subscription,
- mention_set,
+ profile_selector: None,
+
notifications: Vec::new(),
notification_subscriptions: HashMap::default(),
- diff_editors: Default::default(),
- terminal_views: Default::default(),
list_state: list_state.clone(),
scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
- last_error: None,
+ thread_retry_status: None,
+ thread_error: None,
+ thread_feedback: Default::default(),
auth_task: None,
expanded_tool_calls: HashSet::default(),
expanded_thinking_blocks: HashSet::default(),
+ editing_message: None,
edits_expanded: false,
plan_expanded: false,
+ prompt_capabilities,
+ available_commands,
editor_expanded: false,
- terminal_expanded: true,
- message_history,
- _subscriptions: [subscription],
+ should_be_following: false,
+ history_store,
+ hovered_recent_history_item: None,
+ is_loading_contents: false,
+ _subscriptions: subscriptions,
_cancel_task: None,
+ focus_handle: cx.focus_handle(),
+ new_server_version_available: None,
}
}
+ fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.thread_state = Self::initial_state(
+ self.agent.clone(),
+ None,
+ self.workspace.clone(),
+ self.project.clone(),
+ window,
+ cx,
+ );
+ self.available_commands.replace(vec![]);
+ self.new_server_version_available.take();
+ cx.notify();
+ }
+
fn initial_state(
agent: Rc<dyn AgentServer>,
+ resume_thread: Option<DbThreadMetadata>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> ThreadState {
- let root_dir = project
- .read(cx)
- .visible_worktrees(cx)
- .next()
- .map(|worktree| worktree.read(cx).abs_path())
- .unwrap_or_else(|| paths::home_dir().as_path().into());
+ if project.read(cx).is_via_collab()
+ && agent.clone().downcast::<NativeAgentServer>().is_none()
+ {
+ return ThreadState::LoadError(LoadError::Other(
+ "External agents are not yet supported in shared projects.".into(),
+ ));
+ }
+ let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
+ // Pick the first non-single-file worktree for the root directory if there are any,
+ // and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees.
+ worktrees.sort_by(|l, r| {
+ l.read(cx)
+ .is_single_file()
+ .cmp(&r.read(cx).is_single_file())
+ });
+ let root_dir = worktrees
+ .into_iter()
+ .filter_map(|worktree| {
+ if worktree.read(cx).is_single_file() {
+ Some(worktree.read(cx).abs_path().parent()?.into())
+ } else {
+ Some(worktree.read(cx).abs_path())
+ }
+ })
+ .next();
+ let (status_tx, mut status_rx) = watch::channel("Loading…".into());
+ let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
+ let delegate = AgentServerDelegate::new(
+ project.read(cx).agent_server_store().clone(),
+ project.clone(),
+ Some(status_tx),
+ Some(new_version_available_tx),
+ );
- let connect_task = agent.connect(&root_dir, &project, cx);
+ let connect_task = agent.connect(root_dir.as_deref(), delegate, cx);
let load_task = cx.spawn_in(window, async move |this, cx| {
let connection = match connect_task.await {
- Ok(connection) => connection,
+ Ok((connection, login)) => {
+ this.update(cx, |this, _| this.login = login).ok();
+ connection
+ }
Err(err) => {
- this.update(cx, |this, cx| {
- this.handle_load_error(err, cx);
+ this.update_in(cx, |this, window, cx| {
+ if err.downcast_ref::<LoadError>().is_some() {
+ this.handle_load_error(err, window, cx);
+ } else {
+ this.handle_thread_error(err, cx);
+ }
cx.notify();
})
.log_err();
@@ -255,53 +507,79 @@ impl AcpThreadView {
}
};
- // this.update_in(cx, |_this, _window, cx| {
- // let status = connection.exit_status(cx);
- // cx.spawn(async move |this, cx| {
- // let status = status.await.ok();
- // this.update(cx, |this, cx| {
- // this.thread_state = ThreadState::ServerExited { status };
- // cx.notify();
- // })
- // .ok();
- // })
- // .detach();
- // })
- // .ok();
-
- let result = match connection
+ let result = if let Some(native_agent) = connection
.clone()
- .new_thread(project.clone(), &root_dir, cx)
- .await
+ .downcast::<agent2::NativeAgentConnection>()
+ && let Some(resume) = resume_thread.clone()
{
- Err(e) => {
- let mut cx = cx.clone();
- if e.is::<acp_thread::AuthRequired>() {
- this.update(&mut cx, |this, cx| {
- this.thread_state = ThreadState::Unauthenticated { connection };
- cx.notify();
+ cx.update(|_, cx| {
+ native_agent
+ .0
+ .update(cx, |agent, cx| agent.open_thread(resume.id, cx))
+ })
+ .log_err()
+ } else {
+ let root_dir = if let Some(acp_agent) = connection
+ .clone()
+ .downcast::<agent_servers::AcpConnection>()
+ {
+ acp_agent.root_dir().into()
+ } else {
+ root_dir.unwrap_or(paths::home_dir().as_path().into())
+ };
+ cx.update(|_, cx| {
+ connection
+ .clone()
+ .new_thread(project.clone(), &root_dir, cx)
+ })
+ .log_err()
+ };
+
+ let Some(result) = result else {
+ return;
+ };
+
+ let result = match result.await {
+ Err(e) => match e.downcast::<acp_thread::AuthRequired>() {
+ Ok(err) => {
+ cx.update(|window, cx| {
+ Self::handle_auth_required(this, err, agent, connection, window, cx)
})
- .ok();
+ .log_err();
return;
- } else {
- Err(e)
}
- }
+ Err(err) => Err(err),
+ },
Ok(thread) => Ok(thread),
};
this.update_in(cx, |this, window, cx| {
match result {
Ok(thread) => {
- let thread_subscription =
- cx.subscribe_in(&thread, window, Self::handle_thread_event);
-
let action_log = thread.read(cx).action_log().clone();
- let action_log_subscription =
- cx.observe(&action_log, |_, _, cx| cx.notify());
- this.list_state
- .splice(0..0, thread.read(cx).entries().len());
+ this.prompt_capabilities
+ .set(thread.read(cx).prompt_capabilities());
+
+ let count = thread.read(cx).entries().len();
+ this.entry_view_state.update(cx, |view_state, cx| {
+ for ix in 0..count {
+ view_state.sync_entry(ix, &thread, window, cx);
+ }
+ this.list_state.splice_focusable(
+ 0..0,
+ (0..count).map(|ix| view_state.entry(ix)?.focus_handle(cx)),
+ );
+ });
+
+ if let Some(resume) = resume_thread {
+ this.history_store.update(cx, |history, cx| {
+ history.push_recently_opened_entry(
+ HistoryEntryId::AcpThread(resume.id),
+ cx,
+ );
+ });
+ }
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
@@ -323,55 +601,236 @@ impl AcpThreadView {
})
});
+ let mode_selector = thread
+ .read(cx)
+ .connection()
+ .session_modes(thread.read(cx).session_id(), cx)
+ .map(|session_modes| {
+ let fs = this.project.read(cx).fs().clone();
+ let focus_handle = this.focus_handle(cx);
+ cx.new(|_cx| {
+ ModeSelector::new(
+ session_modes,
+ this.agent.clone(),
+ fs,
+ focus_handle,
+ )
+ })
+ });
+
+ let mut subscriptions = vec![
+ cx.subscribe_in(&thread, window, Self::handle_thread_event),
+ cx.observe(&action_log, |_, _, cx| cx.notify()),
+ ];
+
+ let title_editor =
+ if thread.update(cx, |thread, cx| thread.can_set_title(cx)) {
+ let editor = cx.new(|cx| {
+ let mut editor = Editor::single_line(window, cx);
+ editor.set_text(thread.read(cx).title(), window, cx);
+ editor
+ });
+ subscriptions.push(cx.subscribe_in(
+ &editor,
+ window,
+ Self::handle_title_editor_event,
+ ));
+ Some(editor)
+ } else {
+ None
+ };
+
this.thread_state = ThreadState::Ready {
thread,
- _subscription: [thread_subscription, action_log_subscription],
+ title_editor,
+ mode_selector,
+ _subscriptions: subscriptions,
};
+ this.message_editor.focus_handle(cx).focus(window);
+
+ this.profile_selector = this.as_native_thread(cx).map(|thread| {
+ cx.new(|cx| {
+ ProfileSelector::new(
+ <dyn Fs>::global(cx),
+ Arc::new(thread.clone()),
+ this.focus_handle(cx),
+ cx,
+ )
+ })
+ });
cx.notify();
}
Err(err) => {
- this.handle_load_error(err, cx);
+ this.handle_load_error(err, window, cx);
}
};
})
.log_err();
});
- ThreadState::Loading { _task: load_task }
+ cx.spawn(async move |this, cx| {
+ while let Ok(new_version) = new_version_available_rx.recv().await {
+ if let Some(new_version) = new_version {
+ this.update(cx, |this, cx| {
+ this.new_server_version_available = Some(new_version.into());
+ cx.notify();
+ })
+ .log_err();
+ }
+ }
+ })
+ .detach();
+
+ let loading_view = cx.new(|cx| {
+ let update_title_task = cx.spawn(async move |this, cx| {
+ loop {
+ let status = status_rx.recv().await?;
+ this.update(cx, |this: &mut LoadingView, cx| {
+ this.title = status;
+ cx.notify();
+ })?;
+ }
+ });
+
+ LoadingView {
+ title: "Loading…".into(),
+ _load_task: load_task,
+ _update_title_task: update_title_task,
+ }
+ });
+
+ ThreadState::Loading(loading_view)
+ }
+
+ fn handle_auth_required(
+ this: WeakEntity<Self>,
+ err: AuthRequired,
+ agent: Rc<dyn AgentServer>,
+ connection: Rc<dyn AgentConnection>,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ let agent_name = agent.name();
+ let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id {
+ let registry = LanguageModelRegistry::global(cx);
+
+ let sub = window.subscribe(®istry, cx, {
+ let provider_id = provider_id.clone();
+ let this = this.clone();
+ move |_, ev, window, cx| {
+ if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
+ && &provider_id == updated_provider_id
+ && LanguageModelRegistry::global(cx)
+ .read(cx)
+ .provider(&provider_id)
+ .map_or(false, |provider| provider.is_authenticated(cx))
+ {
+ this.update(cx, |this, cx| {
+ this.reset(window, cx);
+ })
+ .ok();
+ }
+ }
+ });
+
+ let view = registry.read(cx).provider(&provider_id).map(|provider| {
+ provider.configuration_view(
+ language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()),
+ window,
+ cx,
+ )
+ });
+
+ (view, Some(sub))
+ } else {
+ (None, None)
+ };
+
+ this.update(cx, |this, cx| {
+ this.thread_state = ThreadState::Unauthenticated {
+ pending_auth_method: None,
+ connection,
+ configuration_view,
+ description: err
+ .description
+ .clone()
+ .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx))),
+ _subscription: subscription,
+ };
+ if this.message_editor.focus_handle(cx).is_focused(window) {
+ this.focus_handle.focus(window)
+ }
+ cx.notify();
+ })
+ .ok();
}
- fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context<Self>) {
+ fn handle_load_error(
+ &mut self,
+ err: anyhow::Error,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
if let Some(load_err) = err.downcast_ref::<LoadError>() {
self.thread_state = ThreadState::LoadError(load_err.clone());
} else {
self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
}
+ if self.message_editor.focus_handle(cx).is_focused(window) {
+ self.focus_handle.focus(window)
+ }
cx.notify();
}
+ pub fn workspace(&self) -> &WeakEntity<Workspace> {
+ &self.workspace
+ }
+
pub fn thread(&self) -> Option<&Entity<AcpThread>> {
match &self.thread_state {
ThreadState::Ready { thread, .. } => Some(thread),
ThreadState::Unauthenticated { .. }
| ThreadState::Loading { .. }
- | ThreadState::LoadError(..)
- | ThreadState::ServerExited { .. } => None,
+ | ThreadState::LoadError { .. } => None,
+ }
+ }
+
+ pub fn mode_selector(&self) -> Option<&Entity<ModeSelector>> {
+ match &self.thread_state {
+ ThreadState::Ready { mode_selector, .. } => mode_selector.as_ref(),
+ ThreadState::Unauthenticated { .. }
+ | ThreadState::Loading { .. }
+ | ThreadState::LoadError { .. } => None,
}
}
pub fn title(&self, cx: &App) -> SharedString {
match &self.thread_state {
- ThreadState::Ready { thread, .. } => thread.read(cx).title(),
- ThreadState::Loading { .. } => "Loading…".into(),
- ThreadState::LoadError(_) => "Failed to load".into(),
- ThreadState::Unauthenticated { .. } => "Not authenticated".into(),
- ThreadState::ServerExited { .. } => "Server exited unexpectedly".into(),
+ ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
+ ThreadState::Loading(loading_view) => loading_view.read(cx).title.clone(),
+ ThreadState::LoadError(error) => match error {
+ LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
+ LoadError::FailedToInstall(_) => {
+ format!("Failed to Install {}", self.agent.name()).into()
+ }
+ LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(),
+ LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(),
+ },
+ }
+ }
+
+ pub fn title_editor(&self) -> Option<Entity<Editor>> {
+ if let ThreadState::Ready { title_editor, .. } = &self.thread_state {
+ title_editor.clone()
+ } else {
+ None
}
}
- pub fn cancel(&mut self, cx: &mut Context<Self>) {
- self.last_error.take();
+ pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
+ self.thread_error.take();
+ self.thread_retry_status.take();
if let Some(thread) = self.thread() {
self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
@@ -23,9 +23,8 @@ use gpui::{
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry,
ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla,
ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful,
- StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
- UnderlineStyle, WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, percentage,
- pulsating_between,
+ StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle,
+ WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, pulsating_between,
};
use language::{Buffer, Language, LanguageRegistry};
use language_model::{
@@ -46,8 +45,8 @@ use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings;
use ui::{
- Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
- Tooltip, prelude::*,
+ Banner, CommonAnimationExt, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar,
+ ScrollbarState, TextSize, Tooltip, prelude::*,
};
use util::ResultExt as _;
use util::markdown::MarkdownCodeBlock;
@@ -491,7 +490,7 @@ fn render_markdown_code_block(
.on_click({
let active_thread = active_thread.clone();
let parsed_markdown = parsed_markdown.clone();
- let code_block_range = metadata.content_range.clone();
+ let code_block_range = metadata.content_range;
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.copied_code_block_ids.insert((message_id, ix));
@@ -532,7 +531,6 @@ fn render_markdown_code_block(
"Expand Code"
}))
.on_click({
- let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
this.toggle_codeblock_expanded(message_id, ix);
@@ -780,13 +778,11 @@ impl ActiveThread {
let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.));
- let workspace_subscription = if let Some(workspace) = workspace.upgrade() {
- Some(cx.observe_release(&workspace, |this, _, cx| {
+ let workspace_subscription = workspace.upgrade().map(|workspace| {
+ cx.observe_release(&workspace, |this, _, cx| {
this.dismiss_notifications(cx);
- }))
- } else {
- None
- };
+ })
+ });
let mut this = Self {
language_registry,
@@ -916,7 +912,7 @@ impl ActiveThread {
) {
let rendered = self
.rendered_tool_uses
- .entry(tool_use_id.clone())
+ .entry(tool_use_id)
.or_insert_with(|| RenderedToolUse {
label: cx.new(|cx| {
Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
@@ -1005,8 +1001,22 @@ impl ActiveThread {
// Don't notify for intermediate tool use
}
Ok(StopReason::Refusal) => {
+ let model_name = self
+ .thread
+ .read(cx)
+ .configured_model()
+ .map(|configured| configured.model.name().0.to_string())
+ .unwrap_or_else(|| "The model".to_string());
+ let refusal_message = format!(
+ "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.",
+ model_name
+ );
+ self.last_error = Some(ThreadError::Message {
+ header: SharedString::from("Request Refused"),
+ message: SharedString::from(refusal_message),
+ });
self.notify_with_sound(
- "Language model refused to respond",
+ format!("{} refused to respond", model_name),
IconName::Warning,
window,
cx,
@@ -1044,12 +1054,12 @@ impl ActiveThread {
);
}
ThreadEvent::StreamedAssistantText(message_id, text) => {
- if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
+ if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(message_id) {
rendered_message.append_text(text, cx);
}
}
ThreadEvent::StreamedAssistantThinking(message_id, text) => {
- if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
+ if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(message_id) {
rendered_message.append_thinking(text, cx);
}
}
@@ -1072,8 +1082,8 @@ impl ActiveThread {
}
ThreadEvent::MessageEdited(message_id) => {
self.clear_last_error();
- if let Some(index) = self.messages.iter().position(|id| id == message_id) {
- if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
+ if let Some(index) = self.messages.iter().position(|id| id == message_id)
+ && let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
thread.message(*message_id).map(|message| {
let mut rendered_message = RenderedMessage {
language_registry: self.language_registry.clone(),
@@ -1084,14 +1094,14 @@ impl ActiveThread {
}
rendered_message
})
- }) {
- self.list_state.splice(index..index + 1, 1);
- self.rendered_messages_by_id
- .insert(*message_id, rendered_message);
- self.scroll_to_bottom(cx);
- self.save_thread(cx);
- cx.notify();
- }
+ })
+ {
+ self.list_state.splice(index..index + 1, 1);
+ self.rendered_messages_by_id
+ .insert(*message_id, rendered_message);
+ self.scroll_to_bottom(cx);
+ self.save_thread(cx);
+ cx.notify();
}
}
ThreadEvent::MessageDeleted(message_id) => {
@@ -1218,7 +1228,7 @@ impl ActiveThread {
match AgentSettings::get_global(cx).notify_when_agent_waiting {
NotifyWhenAgentWaiting::PrimaryScreen => {
if let Some(primary) = cx.primary_display() {
- self.pop_up(icon, caption.into(), title.clone(), window, primary, cx);
+ self.pop_up(icon, caption.into(), title, window, primary, cx);
}
}
NotifyWhenAgentWaiting::AllScreens => {
@@ -1272,62 +1282,61 @@ impl ActiveThread {
})
})
.log_err()
+ && let Some(pop_up) = screen_window.entity(cx).log_err()
{
- if let Some(pop_up) = screen_window.entity(cx).log_err() {
- self.notification_subscriptions
- .entry(screen_window)
- .or_insert_with(Vec::new)
- .push(cx.subscribe_in(&pop_up, window, {
- |this, _, event, window, cx| match event {
- AgentNotificationEvent::Accepted => {
- let handle = window.window_handle();
- cx.activate(true);
-
- let workspace_handle = this.workspace.clone();
-
- // If there are multiple Zed windows, activate the correct one.
- cx.defer(move |cx| {
- handle
- .update(cx, |_view, window, _cx| {
- window.activate_window();
-
- if let Some(workspace) = workspace_handle.upgrade() {
- workspace.update(_cx, |workspace, cx| {
- workspace.focus_panel::<AgentPanel>(window, cx);
- });
- }
- })
- .log_err();
- });
+ self.notification_subscriptions
+ .entry(screen_window)
+ .or_insert_with(Vec::new)
+ .push(cx.subscribe_in(&pop_up, window, {
+ |this, _, event, window, cx| match event {
+ AgentNotificationEvent::Accepted => {
+ let handle = window.window_handle();
+ cx.activate(true);
+
+ let workspace_handle = this.workspace.clone();
+
+ // If there are multiple Zed windows, activate the correct one.
+ cx.defer(move |cx| {
+ handle
+ .update(cx, |_view, window, _cx| {
+ window.activate_window();
+
+ if let Some(workspace) = workspace_handle.upgrade() {
+ workspace.update(_cx, |workspace, cx| {
+ workspace.focus_panel::<AgentPanel>(window, cx);
+ });
+ }
+ })
+ .log_err();
+ });
- this.dismiss_notifications(cx);
- }
- AgentNotificationEvent::Dismissed => {
- this.dismiss_notifications(cx);
- }
+ this.dismiss_notifications(cx);
}
- }));
-
- self.notifications.push(screen_window);
-
- // If the user manually refocuses the original window, dismiss the popup.
- self.notification_subscriptions
- .entry(screen_window)
- .or_insert_with(Vec::new)
- .push({
- let pop_up_weak = pop_up.downgrade();
-
- cx.observe_window_activation(window, move |_, window, cx| {
- if window.is_window_active() {
- if let Some(pop_up) = pop_up_weak.upgrade() {
- pop_up.update(cx, |_, cx| {
- cx.emit(AgentNotificationEvent::Dismissed);
- });
- }
- }
- })
- });
- }
+ AgentNotificationEvent::Dismissed => {
+ this.dismiss_notifications(cx);
+ }
+ }
+ }));
+
+ self.notifications.push(screen_window);
+
+ // If the user manually refocuses the original window, dismiss the popup.
+ self.notification_subscriptions
+ .entry(screen_window)
+ .or_insert_with(Vec::new)
+ .push({
+ let pop_up_weak = pop_up.downgrade();
+
+ cx.observe_window_activation(window, move |_, window, cx| {
+ if window.is_window_active()
+ && let Some(pop_up) = pop_up_weak.upgrade()
+ {
+ pop_up.update(cx, |_, cx| {
+ cx.emit(AgentNotificationEvent::Dismissed);
+ });
+ }
+ })
+ });
}
}
@@ -1374,12 +1383,12 @@ impl ActiveThread {
editor.focus_handle(cx).focus(window);
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
});
- let buffer_edited_subscription = cx.subscribe(&editor, |this, _, event, cx| match event {
- EditorEvent::BufferEdited => {
- this.update_editing_message_token_count(true, cx);
- }
- _ => {}
- });
+ let buffer_edited_subscription =
+ cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| {
+ if event == &EditorEvent::BufferEdited {
+ this.update_editing_message_token_count(true, cx);
+ }
+ });
let context_picker_menu_handle = PopoverMenuHandle::default();
let context_strip = cx.new(|cx| {
@@ -1599,11 +1608,6 @@ impl ActiveThread {
return;
};
- if model.provider.must_accept_terms(cx) {
- cx.notify();
- return;
- }
-
let edited_text = state.editor.read(cx).text(cx);
let creases = state.editor.update(cx, extract_message_creases);
@@ -1738,6 +1742,7 @@ impl ActiveThread {
);
editor.set_placeholder_text(
"What went wrong? Share your feedback so we can improve.",
+ window,
cx,
);
editor
@@ -1766,7 +1771,7 @@ impl ActiveThread {
.thread
.read(cx)
.message(message_id)
- .map(|msg| msg.to_string())
+ .map(|msg| msg.to_message_content())
.unwrap_or_default();
telemetry::event!(
@@ -2113,7 +2118,7 @@ impl ActiveThread {
.gap_1()
.children(message_content)
.when_some(editing_message_state, |this, state| {
- let focus_handle = state.editor.focus_handle(cx).clone();
+ let focus_handle = state.editor.focus_handle(cx);
this.child(
h_flex()
@@ -2174,7 +2179,6 @@ impl ActiveThread {
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tooltip({
- let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Regenerate",
@@ -2247,9 +2251,7 @@ impl ActiveThread {
let after_editing_message = self
.editing_message
.as_ref()
- .map_or(false, |(editing_message_id, _)| {
- message_id > *editing_message_id
- });
+ .is_some_and(|(editing_message_id, _)| message_id > *editing_message_id);
let backdrop = div()
.id(("backdrop", ix))
@@ -2269,13 +2271,12 @@ impl ActiveThread {
let mut error = None;
if let Some(last_restore_checkpoint) =
self.thread.read(cx).last_restore_checkpoint()
+ && last_restore_checkpoint.message_id() == message_id
{
- if last_restore_checkpoint.message_id() == message_id {
- match last_restore_checkpoint {
- LastRestoreCheckpoint::Pending { .. } => is_pending = true,
- LastRestoreCheckpoint::Error { error: err, .. } => {
- error = Some(err.clone());
- }
+ match last_restore_checkpoint {
+ LastRestoreCheckpoint::Pending { .. } => is_pending = true,
+ LastRestoreCheckpoint::Error { error: err, .. } => {
+ error = Some(err.clone());
}
}
}
@@ -2316,7 +2317,7 @@ impl ActiveThread {
.into_any_element()
} else if let Some(error) = error {
restore_checkpoint_button
- .tooltip(Tooltip::text(error.to_string()))
+ .tooltip(Tooltip::text(error))
.into_any_element()
} else {
restore_checkpoint_button.into_any_element()
@@ -2357,7 +2358,6 @@ impl ActiveThread {
this.submit_feedback_message(message_id, cx);
cx.notify();
}))
- .on_action(cx.listener(Self::confirm_editing_message))
.mb_2()
.mx_4()
.p_2()
@@ -2473,7 +2473,7 @@ impl ActiveThread {
message_id,
index,
content.clone(),
- &scroll_handle,
+ scroll_handle,
Some(index) == pending_thinking_segment_index,
window,
cx,
@@ -2597,7 +2597,7 @@ impl ActiveThread {
.id(("message-container", ix))
.py_1()
.px_2p5()
- .child(Banner::new().severity(ui::Severity::Warning).child(message))
+ .child(Banner::new().severity(Severity::Warning).child(message))
}
fn render_message_thinking_segment(
@@ -2661,15 +2661,7 @@ impl ActiveThread {
Icon::new(IconName::ArrowCircle)
.color(Color::Accent)
.size(IconSize::Small)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(
- percentage(delta),
- ))
- },
- )
+ .with_rotate_animation(2)
}),
),
)
@@ -2845,17 +2837,11 @@ impl ActiveThread {
}
ToolUseStatus::Pending
| ToolUseStatus::InputStillStreaming
- | ToolUseStatus::Running => {
- let icon = Icon::new(IconName::ArrowCircle)
- .color(Color::Accent)
- .size(IconSize::Small);
- icon.with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- )
- .into_any_element()
- }
+ | ToolUseStatus::Running => Icon::new(IconName::ArrowCircle)
+ .color(Color::Accent)
+ .size(IconSize::Small)
+ .with_rotate_animation(2)
+ .into_any_element(),
ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
ToolUseStatus::Error(_) => {
let icon = Icon::new(IconName::Close)
@@ -2944,15 +2930,7 @@ impl ActiveThread {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Accent)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(percentage(
- delta,
- )))
- },
- ),
+ .with_rotate_animation(2),
)
.child(
Label::new("Running…")
@@ -3608,7 +3586,7 @@ pub(crate) fn open_active_thread_as_markdown(
}
let buffer = project.update(cx, |project, cx| {
- project.create_local_buffer(&markdown, Some(markdown_language), cx)
+ project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
});
let buffer =
cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone()));
@@ -4020,7 +3998,7 @@ mod tests {
cx.run_until_parked();
- // Verify that the previous completion was cancelled
+ // Verify that the previous completion was canceled
assert_eq!(cancellation_events.lock().unwrap().len(), 1);
// Verify that a new request was started after cancellation
@@ -3,19 +3,21 @@ mod configure_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
-use std::{sync::Arc, time::Duration};
+use std::{ops::Range, sync::Arc};
use agent_settings::AgentSettings;
+use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
-use cloud_llm_client::Plan;
+use cloud_llm_client::{Plan, PlanV1, PlanV2};
use collections::HashMap;
use context_server::ContextServerId;
+use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest;
use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
- Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
- Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
+ Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
+ Hsla, ScrollHandle, Subscription, Task, WeakEntity,
};
use language::LanguageRegistry;
use language_model::{
@@ -23,29 +25,36 @@ use language_model::{
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
+ agent_server_store::{
+ AgentServerCommand, AgentServerStore, AllAgentServersSettings, CLAUDE_CODE_NAME,
+ CustomAgentServerSettings, GEMINI_NAME,
+ },
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
-use settings::{Settings, update_settings_file};
+use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
- Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
- Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
+ Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
+ Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip,
+ prelude::*,
};
use util::ResultExt as _;
-use workspace::Workspace;
+use workspace::{Workspace, create_and_open_local_file};
use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::{
- AddContextServer,
+ AddContextServer, ExternalAgent, NewExternalAgentThread,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
+ placeholder_command,
};
pub struct AgentConfiguration {
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
+ agent_server_store: Entity<AgentServerStore>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
@@ -56,11 +65,13 @@ pub struct AgentConfiguration {
_registry_subscription: Subscription,
scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
+ _check_for_gemini: Task<()>,
}
impl AgentConfiguration {
pub fn new(
fs: Arc<dyn Fs>,
+ agent_server_store: Entity<AgentServerStore>,
context_server_store: Entity<ContextServerStore>,
tools: Entity<ToolWorkingSet>,
language_registry: Arc<LanguageRegistry>,
@@ -93,27 +104,21 @@ impl AgentConfiguration {
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
- let mut expanded_provider_configurations = HashMap::default();
- if LanguageModelRegistry::read_global(cx)
- .provider(&ZED_CLOUD_PROVIDER_ID)
- .map_or(false, |cloud_provider| cloud_provider.must_accept_terms(cx))
- {
- expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
- }
-
let mut this = Self {
fs,
language_registry,
workspace,
focus_handle,
configuration_views_by_provider: HashMap::default(),
+ agent_server_store,
context_server_store,
expanded_context_server_tools: HashMap::default(),
- expanded_provider_configurations,
+ expanded_provider_configurations: HashMap::default(),
tools,
_registry_subscription: registry_subscription,
scroll_handle,
scrollbar_state,
+ _check_for_gemini: Task::ready(()),
};
this.build_provider_configuration_views(window, cx);
this
@@ -137,7 +142,11 @@ impl AgentConfiguration {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let configuration_view = provider.configuration_view(window, cx);
+ let configuration_view = provider.configuration_view(
+ language_model::ConfigurationViewTargetAgent::ZedAgent,
+ window,
+ cx,
+ );
self.configuration_views_by_provider
.insert(provider.id(), configuration_view);
}
@@ -161,8 +170,8 @@ impl AgentConfiguration {
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut Context<Self>,
) -> impl IntoElement + use<> {
- let provider_id = provider.id().0.clone();
- let provider_name = provider.name().0.clone();
+ let provider_id = provider.id().0;
+ let provider_name = provider.name().0;
let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}"));
let configuration_view = self
@@ -188,7 +197,7 @@ impl AgentConfiguration {
let is_signed_in = self
.workspace
.read_with(cx, |workspace, _| {
- workspace.client().status().borrow().is_connected()
+ !workspace.client().status().borrow().is_signed_out()
})
.unwrap_or(false);
@@ -215,7 +224,6 @@ impl AgentConfiguration {
.child(
h_flex()
.id(provider_id_string.clone())
- .cursor_pointer()
.px_2()
.py_0p5()
.w_full()
@@ -235,10 +243,7 @@ impl AgentConfiguration {
h_flex()
.w_full()
.gap_1()
- .child(
- Label::new(provider_name.clone())
- .size(LabelSize::Large),
- )
+ .child(Label::new(provider_name.clone()))
.map(|this| {
if is_zed_provider && is_signed_in {
this.child(
@@ -265,7 +270,7 @@ impl AgentConfiguration {
.closed_icon(IconName::ChevronDown),
)
.on_click(cx.listener({
- let provider_id = provider.id().clone();
+ let provider_id = provider.id();
move |this, _event, _window, _cx| {
let is_expanded = this
.expanded_provider_configurations
@@ -283,7 +288,7 @@ impl AgentConfiguration {
"Start New Thread",
)
.icon_position(IconPosition::Start)
- .icon(IconName::Plus)
+ .icon(IconName::Thread)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
@@ -300,6 +305,7 @@ impl AgentConfiguration {
)
.child(
div()
+ .w_full()
.px_2()
.when(is_expanded, |parent| match configuration_view {
Some(configuration_view) => parent.child(configuration_view),
@@ -332,6 +338,7 @@ impl AgentConfiguration {
.gap_0p5()
.child(
h_flex()
+ .pr_1()
.w_full()
.gap_2()
.justify_between()
@@ -381,7 +388,7 @@ impl AgentConfiguration {
),
)
.child(
- Label::new("Add at least one provider to use AI-powered features.")
+ Label::new("Add at least one provider to use AI-powered features with Zed's native agent.")
.color(Color::Muted),
),
),
@@ -465,7 +472,7 @@ impl AgentConfiguration {
"modifier-send",
"Use modifier to submit a message",
Some(
- "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(),
+ "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
),
use_modifier_to_send,
move |state, _window, cx| {
@@ -508,9 +515,15 @@ impl AgentConfiguration {
.blend(cx.theme().colors().text_accent.opacity(0.2));
let (plan_name, label_color, bg_color) = match plan {
- Plan::ZedFree => ("Free", Color::Default, free_chip_bg),
- Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
- Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
+ Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree) => {
+ ("Free", Color::Default, free_chip_bg)
+ }
+ Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial) => {
+ ("Pro Trial", Color::Accent, pro_chip_bg)
+ }
+ Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro) => {
+ ("Pro", Color::Accent, pro_chip_bg)
+ }
};
Chip::new(plan_name.to_string())
@@ -522,6 +535,14 @@ impl AgentConfiguration {
}
}
+ fn card_item_bg_color(&self, cx: &mut Context<Self>) -> Hsla {
+ cx.theme().colors().background.opacity(0.25)
+ }
+
+ fn card_item_border_color(&self, cx: &mut Context<Self>) -> Hsla {
+ cx.theme().colors().border.opacity(0.6)
+ }
+
fn render_context_servers_section(
&mut self,
window: &mut Window,
@@ -539,7 +560,12 @@ impl AgentConfiguration {
v_flex()
.gap_0p5()
.child(Headline::new("Model Context Protocol (MCP) Servers"))
- .child(Label::new("Connect to context servers through the Model Context Protocol, either using Zed extensions or directly.").color(Color::Muted)),
+ .child(
+ Label::new(
+ "All context servers connected through the Model Context Protocol.",
+ )
+ .color(Color::Muted),
+ ),
)
.children(
context_server_ids.into_iter().map(|context_server_id| {
@@ -549,7 +575,7 @@ impl AgentConfiguration {
.child(
h_flex()
.justify_between()
- .gap_2()
+ .gap_1p5()
.child(
h_flex().w_full().child(
Button::new("add-context-server", "Add Custom Server")
@@ -640,8 +666,6 @@ impl AgentConfiguration {
.map_or([].as_slice(), |tools| tools.as_slice());
let tool_count = tools.len();
- let border_color = cx.theme().colors().border.opacity(0.6);
-
let (source_icon, source_tooltip) = if is_from_extension {
(
IconName::ZedMcpExtension,
@@ -659,10 +683,9 @@ impl AgentConfiguration {
Icon::new(IconName::LoadCircle)
.size(IconSize::XSmall)
.color(Color::Accent)
- .with_animation(
- SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
- Animation::new(Duration::from_secs(3)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+ .with_keyed_rotate_animation(
+ SharedString::from(format!("{}-starting", context_server_id.0)),
+ 3,
)
.into_any_element(),
"Server is starting.",
@@ -784,8 +807,8 @@ impl AgentConfiguration {
.id(item_id.clone())
.border_1()
.rounded_md()
- .border_color(border_color)
- .bg(cx.theme().colors().background.opacity(0.2))
+ .border_color(self.card_item_border_color(cx))
+ .bg(self.card_item_bg_color(cx))
.overflow_hidden()
.child(
h_flex()
@@ -793,7 +816,11 @@ impl AgentConfiguration {
.justify_between()
.when(
error.is_some() || are_tools_expanded && tool_count >= 1,
- |element| element.border_b_1().border_color(border_color),
+ |element| {
+ element
+ .border_b_1()
+ .border_color(self.card_item_border_color(cx))
+ },
)
.child(
h_flex()
@@ -860,7 +887,6 @@ impl AgentConfiguration {
.on_click({
let context_server_manager =
self.context_server_store.clone();
- let context_server_id = context_server_id.clone();
let fs = self.fs.clone();
move |state, _window, cx| {
@@ -953,7 +979,7 @@ impl AgentConfiguration {
}
parent.child(v_flex().py_1p5().px_1().gap_1().children(
- tools.into_iter().enumerate().map(|(ix, tool)| {
+ tools.iter().enumerate().map(|(ix, tool)| {
h_flex()
.id(("tool-item", ix))
.px_1()
@@ -976,6 +1002,149 @@ impl AgentConfiguration {
))
})
}
+
+ fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
+ let custom_settings = cx
+ .global::<SettingsStore>()
+ .get::<AllAgentServersSettings>(None)
+ .custom
+ .clone();
+ let user_defined_agents = self
+ .agent_server_store
+ .read(cx)
+ .external_agents()
+ .filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME)
+ .cloned()
+ .collect::<Vec<_>>();
+ let user_defined_agents = user_defined_agents
+ .into_iter()
+ .map(|name| {
+ self.render_agent_server(
+ IconName::Ai,
+ name.clone(),
+ ExternalAgent::Custom {
+ name: name.clone().into(),
+ command: custom_settings
+ .get(&name.0)
+ .map(|settings| settings.command.clone())
+ .unwrap_or(placeholder_command()),
+ },
+ cx,
+ )
+ .into_any_element()
+ })
+ .collect::<Vec<_>>();
+
+ v_flex()
+ .border_b_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ v_flex()
+ .p(DynamicSpacing::Base16.rems(cx))
+ .pr(DynamicSpacing::Base20.rems(cx))
+ .gap_2()
+ .child(
+ v_flex()
+ .gap_0p5()
+ .child(
+ h_flex()
+ .pr_1()
+ .w_full()
+ .gap_2()
+ .justify_between()
+ .child(Headline::new("External Agents"))
+ .child(
+ Button::new("add-agent", "Add Agent")
+ .icon_position(IconPosition::Start)
+ .icon(IconName::Plus)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .label_size(LabelSize::Small)
+ .on_click(
+ move |_, window, cx| {
+ if let Some(workspace) = window.root().flatten() {
+ let workspace = workspace.downgrade();
+ window
+ .spawn(cx, async |cx| {
+ open_new_agent_servers_entry_in_settings_editor(
+ workspace,
+ cx,
+ ).await
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+ ),
+ )
+ )
+ .child(
+ Label::new(
+ "All agents connected through the Agent Client Protocol.",
+ )
+ .color(Color::Muted),
+ ),
+ )
+ .child(self.render_agent_server(
+ IconName::AiGemini,
+ "Gemini CLI",
+ ExternalAgent::Gemini,
+ cx,
+ ))
+ .child(self.render_agent_server(
+ IconName::AiClaude,
+ "Claude Code",
+ ExternalAgent::ClaudeCode,
+ cx,
+ ))
+ .children(user_defined_agents),
+ )
+ }
+
+ fn render_agent_server(
+ &self,
+ icon: IconName,
+ name: impl Into<SharedString>,
+ agent: ExternalAgent,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
+ let name = name.into();
+ h_flex()
+ .p_1()
+ .pl_2()
+ .gap_1p5()
+ .justify_between()
+ .border_1()
+ .rounded_md()
+ .border_color(self.card_item_border_color(cx))
+ .bg(self.card_item_bg_color(cx))
+ .overflow_hidden()
+ .child(
+ h_flex()
+ .gap_1p5()
+ .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
+ .child(Label::new(name.clone())),
+ )
+ .child(
+ Button::new(
+ SharedString::from(format!("start_acp_thread-{name}")),
+ "Start New Thread",
+ )
+ .label_size(LabelSize::Small)
+ .icon(IconName::Thread)
+ .icon_position(IconPosition::Start)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(
+ NewExternalAgentThread {
+ agent: Some(agent.clone()),
+ }
+ .boxed_clone(),
+ cx,
+ );
+ }),
+ )
+ }
}
impl Render for AgentConfiguration {
@@ -995,6 +1164,7 @@ impl Render for AgentConfiguration {
.size_full()
.overflow_y_scroll()
.child(self.render_general_settings_section(cx))
+ .child(self.render_agent_servers_section(cx))
.child(self.render_context_servers_section(window, cx))
.child(self.render_provider_configuration_section(cx)),
)
@@ -1035,7 +1205,6 @@ fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool
&& manifest.grammars.is_empty()
&& manifest.language_servers.is_empty()
&& manifest.slash_commands.is_empty()
- && manifest.indexed_docs_providers.is_empty()
&& manifest.snippets.is_none()
&& manifest.debug_locators.is_empty()
}
@@ -1071,7 +1240,6 @@ fn show_unable_to_uninstall_extension_with_context_server(
cx,
move |this, _cx| {
let workspace_handle = workspace_handle.clone();
- let context_server_id = context_server_id.clone();
this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
.dismiss_button(true)
@@ -1115,3 +1283,110 @@ fn show_unable_to_uninstall_extension_with_context_server(
workspace.toggle_status_toast(status_toast, cx);
}
+
+async fn open_new_agent_servers_entry_in_settings_editor(
+ workspace: WeakEntity<Workspace>,
+ cx: &mut AsyncWindowContext,
+) -> Result<()> {
+ let settings_editor = workspace
+ .update_in(cx, |_, window, cx| {
+ create_and_open_local_file(paths::settings_file(), window, cx, || {
+ settings::initial_user_settings_content().as_ref().into()
+ })
+ })?
+ .await?
+ .downcast::<Editor>()
+ .unwrap();
+
+ settings_editor
+ .downgrade()
+ .update_in(cx, |item, window, cx| {
+ let text = item.buffer().read(cx).snapshot(cx).text();
+
+ let settings = cx.global::<SettingsStore>();
+
+ let mut unique_server_name = None;
+ let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
+ let server_name: Option<SharedString> = (0..u8::MAX)
+ .map(|i| {
+ if i == 0 {
+ "your_agent".into()
+ } else {
+ format!("your_agent_{}", i).into()
+ }
+ })
+ .find(|name| !file.custom.contains_key(name));
+ if let Some(server_name) = server_name {
+ unique_server_name = Some(server_name.clone());
+ file.custom.insert(
+ server_name,
+ CustomAgentServerSettings {
+ command: AgentServerCommand {
+ path: "path_to_executable".into(),
+ args: vec![],
+ env: Some(HashMap::default()),
+ },
+ default_mode: None,
+ },
+ );
+ }
+ });
+
+ if edits.is_empty() {
+ return;
+ }
+
+ let ranges = edits
+ .iter()
+ .map(|(range, _)| range.clone())
+ .collect::<Vec<_>>();
+
+ item.edit(edits, cx);
+ if let Some((unique_server_name, buffer)) =
+ unique_server_name.zip(item.buffer().read(cx).as_singleton())
+ {
+ let snapshot = buffer.read(cx).snapshot();
+ if let Some(range) =
+ find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
+ {
+ item.change_selections(
+ SelectionEffects::scroll(Autoscroll::newest()),
+ window,
+ cx,
+ |selections| {
+ selections.select_ranges(vec![range]);
+ },
+ );
+ }
+ }
+ })
+}
+
+fn find_text_in_buffer(
+ text: &str,
+ start: usize,
+ snapshot: &language::BufferSnapshot,
+) -> Option<Range<usize>> {
+ let chars = text.chars().collect::<Vec<char>>();
+
+ let mut offset = start;
+ let mut char_offset = 0;
+ for c in snapshot.chars_at(start) {
+ if char_offset >= chars.len() {
+ break;
+ }
+ offset += 1;
+
+ if c == chars[char_offset] {
+ char_offset += 1;
+ } else {
+ char_offset = 0;
+ }
+ }
+
+ if char_offset == chars.len() {
+ Some(offset.saturating_sub(chars.len())..offset)
+ } else {
+ None
+ }
+}
@@ -7,10 +7,12 @@ use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, T
use language_model::LanguageModelRegistry;
use language_models::{
AllLanguageModelSettings, OpenAiCompatibleSettingsContent,
- provider::open_ai_compatible::AvailableModel,
+ provider::open_ai_compatible::{AvailableModel, ModelCapabilities},
};
use settings::update_settings_file;
-use ui::{Banner, KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*};
+use ui::{
+ Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
+};
use ui_input::SingleLineInput;
use workspace::{ModalView, Workspace};
@@ -69,11 +71,19 @@ impl AddLlmProviderInput {
}
}
+struct ModelCapabilityToggles {
+ pub supports_tools: ToggleState,
+ pub supports_images: ToggleState,
+ pub supports_parallel_tool_calls: ToggleState,
+ pub supports_prompt_cache_key: ToggleState,
+}
+
struct ModelInput {
name: Entity<SingleLineInput>,
max_completion_tokens: Entity<SingleLineInput>,
max_output_tokens: Entity<SingleLineInput>,
max_tokens: Entity<SingleLineInput>,
+ capabilities: ModelCapabilityToggles,
}
impl ModelInput {
@@ -100,11 +110,23 @@ impl ModelInput {
cx,
);
let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
+ let ModelCapabilities {
+ tools,
+ images,
+ parallel_tool_calls,
+ prompt_cache_key,
+ } = ModelCapabilities::default();
Self {
name: model_name,
max_completion_tokens,
max_output_tokens,
max_tokens,
+ capabilities: ModelCapabilityToggles {
+ supports_tools: tools.into(),
+ supports_images: images.into(),
+ supports_parallel_tool_calls: parallel_tool_calls.into(),
+ supports_prompt_cache_key: prompt_cache_key.into(),
+ },
}
}
@@ -136,6 +158,12 @@ impl ModelInput {
.text(cx)
.parse::<u64>()
.map_err(|_| SharedString::from("Max Tokens must be a number"))?,
+ capabilities: ModelCapabilities {
+ tools: self.capabilities.supports_tools.selected(),
+ images: self.capabilities.supports_images.selected(),
+ parallel_tool_calls: self.capabilities.supports_parallel_tool_calls.selected(),
+ prompt_cache_key: self.capabilities.supports_prompt_cache_key.selected(),
+ },
})
}
}
@@ -322,6 +350,55 @@ impl AddLlmProviderModal {
.child(model.max_output_tokens.clone()),
)
.child(model.max_tokens.clone())
+ .child(
+ v_flex()
+ .gap_1()
+ .child(
+ Checkbox::new(("supports-tools", ix), model.capabilities.supports_tools)
+ .label("Supports tools")
+ .on_click(cx.listener(move |this, checked, _window, cx| {
+ this.input.models[ix].capabilities.supports_tools = *checked;
+ cx.notify();
+ })),
+ )
+ .child(
+ Checkbox::new(("supports-images", ix), model.capabilities.supports_images)
+ .label("Supports images")
+ .on_click(cx.listener(move |this, checked, _window, cx| {
+ this.input.models[ix].capabilities.supports_images = *checked;
+ cx.notify();
+ })),
+ )
+ .child(
+ Checkbox::new(
+ ("supports-parallel-tool-calls", ix),
+ model.capabilities.supports_parallel_tool_calls,
+ )
+ .label("Supports parallel_tool_calls")
+ .on_click(cx.listener(
+ move |this, checked, _window, cx| {
+ this.input.models[ix]
+ .capabilities
+ .supports_parallel_tool_calls = *checked;
+ cx.notify();
+ },
+ )),
+ )
+ .child(
+ Checkbox::new(
+ ("supports-prompt-cache-key", ix),
+ model.capabilities.supports_prompt_cache_key,
+ )
+ .label("Supports prompt_cache_key")
+ .on_click(cx.listener(
+ move |this, checked, _window, cx| {
+ this.input.models[ix].capabilities.supports_prompt_cache_key =
+ *checked;
+ cx.notify();
+ },
+ )),
+ ),
+ )
.when(has_more_than_one_model, |this| {
this.child(
Button::new(("remove-model", ix), "Remove Model")
@@ -377,7 +454,7 @@ impl Render for AddLlmProviderModal {
this.section(
Section::new().child(
Banner::new()
- .severity(ui::Severity::Warning)
+ .severity(Severity::Warning)
.child(div().text_xs().child(error)),
),
)
@@ -562,6 +639,93 @@ mod tests {
);
}
+ #[gpui::test]
+ async fn test_model_input_default_capabilities(cx: &mut TestAppContext) {
+ let cx = setup_test(cx).await;
+
+ cx.update(|window, cx| {
+ let model_input = ModelInput::new(window, cx);
+ model_input.name.update(cx, |input, cx| {
+ input.editor().update(cx, |editor, cx| {
+ editor.set_text("somemodel", window, cx);
+ });
+ });
+ assert_eq!(
+ model_input.capabilities.supports_tools,
+ ToggleState::Selected
+ );
+ assert_eq!(
+ model_input.capabilities.supports_images,
+ ToggleState::Unselected
+ );
+ assert_eq!(
+ model_input.capabilities.supports_parallel_tool_calls,
+ ToggleState::Unselected
+ );
+ assert_eq!(
+ model_input.capabilities.supports_prompt_cache_key,
+ ToggleState::Unselected
+ );
+
+ let parsed_model = model_input.parse(cx).unwrap();
+ assert!(parsed_model.capabilities.tools);
+ assert!(!parsed_model.capabilities.images);
+ assert!(!parsed_model.capabilities.parallel_tool_calls);
+ assert!(!parsed_model.capabilities.prompt_cache_key);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_model_input_deselected_capabilities(cx: &mut TestAppContext) {
+ let cx = setup_test(cx).await;
+
+ cx.update(|window, cx| {
+ let mut model_input = ModelInput::new(window, cx);
+ model_input.name.update(cx, |input, cx| {
+ input.editor().update(cx, |editor, cx| {
+ editor.set_text("somemodel", window, cx);
+ });
+ });
+
+ model_input.capabilities.supports_tools = ToggleState::Unselected;
+ model_input.capabilities.supports_images = ToggleState::Unselected;
+ model_input.capabilities.supports_parallel_tool_calls = ToggleState::Unselected;
+ model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected;
+
+ let parsed_model = model_input.parse(cx).unwrap();
+ assert!(!parsed_model.capabilities.tools);
+ assert!(!parsed_model.capabilities.images);
+ assert!(!parsed_model.capabilities.parallel_tool_calls);
+ assert!(!parsed_model.capabilities.prompt_cache_key);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_model_input_with_name_and_capabilities(cx: &mut TestAppContext) {
+ let cx = setup_test(cx).await;
+
+ cx.update(|window, cx| {
+ let mut model_input = ModelInput::new(window, cx);
+ model_input.name.update(cx, |input, cx| {
+ input.editor().update(cx, |editor, cx| {
+ editor.set_text("somemodel", window, cx);
+ });
+ });
+
+ model_input.capabilities.supports_tools = ToggleState::Selected;
+ model_input.capabilities.supports_images = ToggleState::Unselected;
+ model_input.capabilities.supports_parallel_tool_calls = ToggleState::Selected;
+ model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected;
+
+ let parsed_model = model_input.parse(cx).unwrap();
+ assert_eq!(parsed_model.name, "somemodel");
+ assert!(parsed_model.capabilities.tools);
+ assert!(!parsed_model.capabilities.images);
+ assert!(parsed_model.capabilities.parallel_tool_calls);
+ assert!(!parsed_model.capabilities.prompt_cache_key);
+ });
+ }
+
async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext {
cx.update(|cx| {
let store = SettingsStore::test(cx);
@@ -1,16 +1,14 @@
use std::{
path::PathBuf,
sync::{Arc, Mutex},
- time::Duration,
};
use anyhow::{Context as _, Result};
use context_server::{ContextServerCommand, ContextServerId};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
- Animation, AnimationExt as _, AsyncWindowContext, DismissEvent, Entity, EventEmitter,
- FocusHandle, Focusable, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle,
- WeakEntity, percentage, prelude::*,
+ AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
+ TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
};
use language::{Language, LanguageRegistry};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
@@ -24,7 +22,9 @@ use project::{
};
use settings::{Settings as _, update_settings_file};
use theme::ThemeSettings;
-use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
+use ui::{
+ CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*,
+};
use util::ResultExt as _;
use workspace::{ModalView, Workspace};
@@ -163,10 +163,10 @@ impl ConfigurationSource {
.read(cx)
.text(cx);
let settings = serde_json_lenient::from_str::<serde_json::Value>(&text)?;
- if let Some(settings_validator) = settings_validator {
- if let Err(error) = settings_validator.validate(&settings) {
- return Err(anyhow::anyhow!(error.to_string()));
- }
+ if let Some(settings_validator) = settings_validator
+ && let Err(error) = settings_validator.validate(&settings)
+ {
+ return Err(anyhow::anyhow!(error.to_string()));
}
Ok((
id.clone(),
@@ -251,6 +251,7 @@ pub struct ConfigureContextServerModal {
workspace: WeakEntity<Workspace>,
source: ConfigurationSource,
state: State,
+ original_server_id: Option<ContextServerId>,
}
impl ConfigureContextServerModal {
@@ -261,7 +262,6 @@ impl ConfigureContextServerModal {
_cx: &mut Context<Workspace>,
) {
workspace.register_action({
- let language_registry = language_registry.clone();
move |_workspace, _: &AddContextServer, window, cx| {
let workspace_handle = cx.weak_entity();
let language_registry = language_registry.clone();
@@ -349,6 +349,11 @@ impl ConfigureContextServerModal {
context_server_store,
workspace: workspace_handle,
state: State::Idle,
+ original_server_id: match &target {
+ ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
+ ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
+ ConfigurationTarget::New => None,
+ },
source: ConfigurationSource::from_target(
target,
language_registry,
@@ -416,9 +421,19 @@ impl ConfigureContextServerModal {
// When we write the settings to the file, the context server will be restarted.
workspace.update(cx, |workspace, cx| {
let fs = workspace.app_state().fs.clone();
- update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
- project_settings.context_servers.insert(id.0, settings);
- });
+ let original_server_id = self.original_server_id.clone();
+ update_settings_file::<ProjectSettings>(
+ fs.clone(),
+ cx,
+ move |project_settings, _| {
+ if let Some(original_id) = original_server_id {
+ if original_id != id {
+ project_settings.context_servers.remove(&original_id.0);
+ }
+ }
+ project_settings.context_servers.insert(id.0, settings);
+ },
+ );
});
} else if let Some(existing_server) = existing_server {
self.context_server_store
@@ -487,7 +502,7 @@ impl ConfigureContextServerModal {
}
fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
- const MODAL_DESCRIPTION: &'static str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables.";
+ const MODAL_DESCRIPTION: &str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables.";
if let ConfigurationSource::Extension {
installation_instructions: Some(installation_instructions),
@@ -639,11 +654,7 @@ impl ConfigureContextServerModal {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- )
+ .with_rotate_animation(2)
.into_any_element(),
)
.child(
@@ -716,24 +727,24 @@ fn wait_for_context_server(
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
match status {
ContextServerStatus::Running => {
- if server_id == &context_server_id {
- if let Some(tx) = tx.lock().unwrap().take() {
- let _ = tx.send(Ok(()));
- }
+ if server_id == &context_server_id
+ && let Some(tx) = tx.lock().unwrap().take()
+ {
+ let _ = tx.send(Ok(()));
}
}
ContextServerStatus::Stopped => {
- if server_id == &context_server_id {
- if let Some(tx) = tx.lock().unwrap().take() {
- let _ = tx.send(Err("Context server stopped running".into()));
- }
+ if server_id == &context_server_id
+ && let Some(tx) = tx.lock().unwrap().take()
+ {
+ let _ = tx.send(Err("Context server stopped running".into()));
}
}
ContextServerStatus::Error(error) => {
- if server_id == &context_server_id {
- if let Some(tx) = tx.lock().unwrap().take() {
- let _ = tx.send(Err(error.clone()));
- }
+ if server_id == &context_server_id
+ && let Some(tx) = tx.lock().unwrap().take()
+ {
+ let _ = tx.send(Err(error.clone()));
}
}
_ => {}
@@ -156,7 +156,7 @@ impl ManageProfilesModal {
) {
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
name_editor.update(cx, |editor, cx| {
- editor.set_placeholder_text("Profile name", cx);
+ editor.set_placeholder_text("Profile name", window, cx);
});
self.mode = Mode::NewProfile(NewProfileMode {
@@ -464,7 +464,7 @@ impl ManageProfilesModal {
},
))
.child(ListSeparator)
- .child(h_flex().p_2().child(mode.name_editor.clone()))
+ .child(h_flex().p_2().child(mode.name_editor))
}
fn render_view_profile(
@@ -191,10 +191,10 @@ impl PickerDelegate for ToolPickerDelegate {
BTreeMap::default();
for item in all_items.iter() {
- if let PickerItem::Tool { server_id, name } = item.clone() {
- if name.contains(&query) {
- tools_by_provider.entry(server_id).or_default().push(name);
- }
+ if let PickerItem::Tool { server_id, name } = item.clone()
+ && name.contains(&query)
+ {
+ tools_by_provider.entry(server_id).or_default().push(name);
}
}
@@ -318,7 +318,7 @@ impl PickerDelegate for ToolPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let item = &self.filtered_items[ix];
+ let item = &self.filtered_items.get(ix)?;
match item {
PickerItem::ContextServer { server_id, .. } => Some(
div()
@@ -10,12 +10,12 @@ use editor::{
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
SelectionEffects, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
+ multibuffer_context_lines,
scroll::Autoscroll,
};
use gpui::{
- Action, Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Empty, Entity,
- EventEmitter, FocusHandle, Focusable, Global, SharedString, Subscription, Task, Transformation,
- WeakEntity, Window, percentage, prelude::*,
+ Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
+ Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
};
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
@@ -28,9 +28,8 @@ use std::{
collections::hash_map::Entry,
ops::Range,
sync::Arc,
- time::Duration,
};
-use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
+use ui::{CommonAnimationExt, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::ResultExt;
use workspace::{
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
@@ -185,7 +184,7 @@ impl AgentDiffPane {
let focus_handle = cx.focus_handle();
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
- let project = thread.project(cx).clone();
+ let project = thread.project(cx);
let editor = cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
@@ -196,27 +195,24 @@ impl AgentDiffPane {
editor
});
- let action_log = thread.action_log(cx).clone();
+ let action_log = thread.action_log(cx);
let mut this = Self {
- _subscriptions: [
- Some(
- cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
- this.update_excerpts(window, cx)
- }),
- ),
+ _subscriptions: vec![
+ cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
+ this.update_excerpts(window, cx)
+ }),
match &thread {
- AgentDiffThread::Native(thread) => {
- Some(cx.subscribe(&thread, |this, _thread, event, cx| {
- this.handle_thread_event(event, cx)
- }))
- }
- AgentDiffThread::AcpThread(_) => None,
+ AgentDiffThread::Native(thread) => cx
+ .subscribe(thread, |this, _thread, event, cx| {
+ this.handle_native_thread_event(event, cx)
+ }),
+ AgentDiffThread::AcpThread(thread) => cx
+ .subscribe(thread, |this, _thread, event, cx| {
+ this.handle_acp_thread_event(event, cx)
+ }),
},
- ]
- .into_iter()
- .flatten()
- .collect(),
+ ],
title: SharedString::default(),
multibuffer,
editor,
@@ -260,7 +256,7 @@ impl AgentDiffPane {
path_key.clone(),
buffer.clone(),
diff_hunk_ranges,
- editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(diff_handle, cx);
@@ -288,7 +284,7 @@ impl AgentDiffPane {
&& buffer
.read(cx)
.file()
- .map_or(false, |file| file.disk_state() == DiskState::Deleted)
+ .is_some_and(|file| file.disk_state() == DiskState::Deleted)
{
editor.fold_buffer(snapshot.text.remote_id(), cx)
}
@@ -324,10 +320,15 @@ impl AgentDiffPane {
}
}
- fn handle_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
- match event {
- ThreadEvent::SummaryGenerated => self.update_title(cx),
- _ => {}
+ fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
+ if let ThreadEvent::SummaryGenerated = event {
+ self.update_title(cx)
+ }
+ }
+
+ fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) {
+ if let AcpThreadEvent::TitleUpdated = event {
+ self.update_title(cx)
}
}
@@ -398,7 +399,7 @@ fn keep_edits_in_selection(
.disjoint_anchor_ranges()
.collect::<Vec<_>>();
- keep_edits_in_ranges(editor, buffer_snapshot, &thread, ranges, window, cx)
+ keep_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx)
}
fn reject_edits_in_selection(
@@ -412,7 +413,7 @@ fn reject_edits_in_selection(
.selections
.disjoint_anchor_ranges()
.collect::<Vec<_>>();
- reject_edits_in_ranges(editor, buffer_snapshot, &thread, ranges, window, cx)
+ reject_edits_in_ranges(editor, buffer_snapshot, thread, ranges, window, cx)
}
fn keep_edits_in_ranges(
@@ -503,8 +504,7 @@ fn update_editor_selection(
&[last_kept_hunk_end..editor::Anchor::max()],
buffer_snapshot,
)
- .skip(1)
- .next()
+ .nth(1)
})
.or_else(|| {
let first_kept_hunk = diff_hunks.first()?;
@@ -1001,7 +1001,7 @@ impl AgentDiffToolbar {
return;
};
- *state = agent_diff.read(cx).editor_state(&editor);
+ *state = agent_diff.read(cx).editor_state(editor);
self.update_location(cx);
cx.notify();
}
@@ -1044,23 +1044,23 @@ impl ToolbarItemView for AgentDiffToolbar {
return self.location(cx);
}
- if let Some(editor) = item.act_as::<Editor>(cx) {
- if editor.read(cx).mode().is_full() {
- let agent_diff = AgentDiff::global(cx);
+ if let Some(editor) = item.act_as::<Editor>(cx)
+ && editor.read(cx).mode().is_full()
+ {
+ let agent_diff = AgentDiff::global(cx);
- self.active_item = Some(AgentDiffToolbarItem::Editor {
- editor: editor.downgrade(),
- state: agent_diff.read(cx).editor_state(&editor.downgrade()),
- _diff_subscription: cx.observe(&agent_diff, Self::handle_diff_notify),
- });
+ self.active_item = Some(AgentDiffToolbarItem::Editor {
+ editor: editor.downgrade(),
+ state: agent_diff.read(cx).editor_state(&editor.downgrade()),
+ _diff_subscription: cx.observe(&agent_diff, Self::handle_diff_notify),
+ });
- return self.location(cx);
- }
+ return self.location(cx);
}
}
self.active_item = None;
- return self.location(cx);
+ self.location(cx)
}
fn pane_focus_update(
@@ -1082,11 +1082,7 @@ impl Render for AgentDiffToolbar {
Icon::new(IconName::LoadCircle)
.size(IconSize::Small)
.color(Color::Accent)
- .with_animation(
- "load_circle",
- Animation::new(Duration::from_secs(3)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- ),
+ .with_rotate_animation(3),
)
.into_any();
@@ -1311,7 +1307,7 @@ impl AgentDiff {
let entity = cx.new(|_cx| Self::default());
let global = AgentDiffGlobal(entity.clone());
cx.set_global(global);
- entity.clone()
+ entity
})
}
@@ -1333,7 +1329,7 @@ impl AgentDiff {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let action_log = thread.action_log(cx).clone();
+ let action_log = thread.action_log(cx);
let action_log_subscription = cx.observe_in(&action_log, window, {
let workspace = workspace.clone();
@@ -1343,13 +1339,13 @@ impl AgentDiff {
});
let thread_subscription = match &thread {
- AgentDiffThread::Native(thread) => cx.subscribe_in(&thread, window, {
+ AgentDiffThread::Native(thread) => cx.subscribe_in(thread, window, {
let workspace = workspace.clone();
move |this, _thread, event, window, cx| {
this.handle_native_thread_event(&workspace, event, window, cx)
}
}),
- AgentDiffThread::AcpThread(thread) => cx.subscribe_in(&thread, window, {
+ AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, {
let workspace = workspace.clone();
move |this, thread, event, window, cx| {
this.handle_acp_thread_event(&workspace, thread, event, window, cx)
@@ -1357,11 +1353,11 @@ impl AgentDiff {
}),
};
- if let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) {
+ if let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) {
// replace thread and action log subscription, but keep editors
workspace_thread.thread = thread.downgrade();
workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription);
- self.update_reviewing_editors(&workspace, window, cx);
+ self.update_reviewing_editors(workspace, window, cx);
return;
}
@@ -1506,7 +1502,7 @@ impl AgentDiff {
.read(cx)
.entries()
.last()
- .map_or(false, |entry| entry.diffs().next().is_some())
+ .is_some_and(|entry| entry.diffs().next().is_some())
{
self.update_reviewing_editors(workspace, window, cx);
}
@@ -1516,16 +1512,25 @@ impl AgentDiff {
.read(cx)
.entries()
.get(*ix)
- .map_or(false, |entry| entry.diffs().next().is_some())
+ .is_some_and(|entry| entry.diffs().next().is_some())
{
self.update_reviewing_editors(workspace, window, cx);
}
}
- AcpThreadEvent::EntriesRemoved(_)
- | AcpThreadEvent::Stopped
- | AcpThreadEvent::ToolAuthorizationRequired
+ AcpThreadEvent::Stopped
| AcpThreadEvent::Error
- | AcpThreadEvent::ServerExited(_) => {}
+ | AcpThreadEvent::LoadError(_)
+ | AcpThreadEvent::Refusal => {
+ self.update_reviewing_editors(workspace, window, cx);
+ }
+ AcpThreadEvent::TitleUpdated
+ | AcpThreadEvent::TokenUsageUpdated
+ | AcpThreadEvent::EntriesRemoved(_)
+ | AcpThreadEvent::ToolAuthorizationRequired
+ | AcpThreadEvent::PromptCapabilitiesUpdated
+ | AcpThreadEvent::AvailableCommandsUpdated(_)
+ | AcpThreadEvent::Retry(_)
+ | AcpThreadEvent::ModeUpdated(_) => {}
}
}
@@ -1536,21 +1541,11 @@ impl AgentDiff {
window: &mut Window,
cx: &mut Context<Self>,
) {
- match event {
- workspace::Event::ItemAdded { item } => {
- if let Some(editor) = item.downcast::<Editor>() {
- if let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx) {
- self.register_editor(
- workspace.downgrade(),
- buffer.clone(),
- editor,
- window,
- cx,
- );
- }
- }
- }
- _ => {}
+ if let workspace::Event::ItemAdded { item } = event
+ && let Some(editor) = item.downcast::<Editor>()
+ && let Some(buffer) = Self::full_editor_buffer(editor.read(cx), cx)
+ {
+ self.register_editor(workspace.downgrade(), buffer, editor, window, cx);
}
}
@@ -1649,7 +1644,7 @@ impl AgentDiff {
continue;
};
- for (weak_editor, _) in buffer_editors {
+ for weak_editor in buffer_editors.keys() {
let Some(editor) = weak_editor.upgrade() else {
continue;
};
@@ -1677,7 +1672,7 @@ impl AgentDiff {
editor.register_addon(EditorAgentDiffAddon);
});
} else {
- unaffected.remove(&weak_editor);
+ unaffected.remove(weak_editor);
}
if new_state == EditorState::Reviewing && previous_state != Some(new_state) {
@@ -1710,7 +1705,7 @@ impl AgentDiff {
.read_with(cx, |editor, _cx| editor.workspace())
.ok()
.flatten()
- .map_or(false, |editor_workspace| {
+ .is_some_and(|editor_workspace| {
editor_workspace.entity_id() == workspace.entity_id()
});
@@ -1730,7 +1725,7 @@ impl AgentDiff {
fn editor_state(&self, editor: &WeakEntity<Editor>) -> EditorState {
self.reviewing_editors
- .get(&editor)
+ .get(editor)
.cloned()
.unwrap_or(EditorState::Idle)
}
@@ -1850,26 +1845,26 @@ impl AgentDiff {
let thread = thread.upgrade()?;
- if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) {
- if let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
- let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
-
- let mut keys = changed_buffers.keys().cycle();
- keys.find(|k| *k == &curr_buffer);
- let next_project_path = keys
- .next()
- .filter(|k| *k != &curr_buffer)
- .and_then(|after| after.read(cx).project_path(cx));
-
- if let Some(path) = next_project_path {
- let task = workspace.open_path(path, None, true, window, cx);
- let task = cx.spawn(async move |_, _cx| task.await.map(|_| ()));
- return Some(task);
- }
+ if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx)
+ && let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton()
+ {
+ let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
+
+ let mut keys = changed_buffers.keys().cycle();
+ keys.find(|k| *k == &curr_buffer);
+ let next_project_path = keys
+ .next()
+ .filter(|k| *k != &curr_buffer)
+ .and_then(|after| after.read(cx).project_path(cx));
+
+ if let Some(path) = next_project_path {
+ let task = workspace.open_path(path, None, true, window, cx);
+ let task = cx.spawn(async move |_, _cx| task.await.map(|_| ()));
+ return Some(task);
}
}
- return Some(Task::ready(Ok(())));
+ Some(Task::ready(Ok(())))
}
}
@@ -66,10 +66,8 @@ impl AgentModelSelector {
fs.clone(),
cx,
move |settings, _cx| {
- settings.set_inline_assistant_model(
- provider.clone(),
- model_id.clone(),
- );
+ settings
+ .set_inline_assistant_model(provider.clone(), model_id);
},
);
}
@@ -1,17 +1,22 @@
-use std::cell::RefCell;
use std::ops::{Not, Range};
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
-use agent_servers::AgentServer;
+use acp_thread::AcpThread;
+use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
+use project::agent_server_store::{
+ AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME,
+};
use serde::{Deserialize, Serialize};
+use zed_actions::OpenBrowser;
+use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
-use crate::NewExternalAgentThread;
+use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::agent_diff::AgentDiffThread;
-use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
+use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@@ -26,11 +31,13 @@ use crate::{
slash_command::SlashCommandCompletionProvider,
text_thread_editor::{
AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate,
- render_remaining_tokens,
},
thread_history::{HistoryEntryElement, ThreadHistory},
ui::{AgentOnboardingModal, EndTrialUpsell},
};
+use crate::{
+ ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
+};
use agent::{
Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
context_store::ContextStore,
@@ -44,25 +51,22 @@ use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::{UserStore, zed_urls};
-use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
+use cloud_llm_client::{CompletionIntent, Plan, PlanV1, PlanV2, UsageLimit};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
-use feature_flags::{self, FeatureFlagAppExt};
+use feature_flags::{self, ClaudeCodeFeatureFlag, FeatureFlagAppExt, GeminiAndNativeFeatureFlag};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
- Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
- KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*,
- pulsating_between,
+ Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext,
+ Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
};
use language::LanguageRegistry;
-use language_model::{
- ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
-};
+use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry};
use project::{DisableAiSettings, Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search};
-use settings::{Settings, update_settings_file};
+use settings::{Settings, SettingsStore, update_settings_file};
use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
@@ -77,13 +81,16 @@ use workspace::{
};
use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
- agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
+ agent::{
+ OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
+ ToggleModelSelector,
+ },
assistant::{OpenRulesLibrary, ToggleFocus},
};
const AGENT_PANEL_KEY: &str = "agent_panel";
-#[derive(Serialize, Deserialize)]
+#[derive(Serialize, Deserialize, Debug)]
struct SerializedAgentPanel {
width: Option<Pixels>,
selected_agent: Option<AgentType>,
@@ -99,6 +106,16 @@ pub fn init(cx: &mut App) {
workspace.focus_panel::<AgentPanel>(window, cx);
}
})
+ .register_action(
+ |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| {
+ if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+ panel.update(cx, |panel, cx| {
+ panel.new_native_agent_thread_from_summary(action, window, cx)
+ });
+ workspace.focus_panel::<AgentPanel>(window, cx);
+ }
+ },
+ )
.register_action(|workspace, _: &OpenHistory, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
@@ -121,7 +138,7 @@ pub fn init(cx: &mut App) {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| {
- panel.new_external_thread(action.agent, window, cx)
+ panel.external_thread(action.agent.clone(), None, None, window, cx)
});
}
})
@@ -191,6 +208,12 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
AgentOnboardingModal::toggle(workspace, window, cx)
})
+ .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
+ AcpOnboardingModal::toggle(workspace, window, cx)
+ })
+ .register_action(|workspace, _: &OpenClaudeCodeOnboardingModal, window, cx| {
+ ClaudeCodeOnboardingModal::toggle(workspace, window, cx)
+ })
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
window.refresh();
@@ -232,7 +255,8 @@ enum WhichFontSize {
None,
}
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+// TODO unify this with ExternalAgent
+#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub enum AgentType {
#[default]
Zed,
@@ -240,24 +264,40 @@ pub enum AgentType {
Gemini,
ClaudeCode,
NativeAgent,
+ Custom {
+ name: SharedString,
+ command: AgentServerCommand,
+ },
}
impl AgentType {
- fn label(self) -> impl Into<SharedString> {
+ fn label(&self) -> SharedString {
match self {
- Self::Zed | Self::TextThread => "Zed",
- Self::NativeAgent => "Agent 2",
- Self::Gemini => "Gemini",
- Self::ClaudeCode => "Claude Code",
+ Self::Zed | Self::TextThread => "Zed Agent".into(),
+ Self::NativeAgent => "Agent 2".into(),
+ Self::Gemini => "Gemini CLI".into(),
+ Self::ClaudeCode => "Claude Code".into(),
+ Self::Custom { name, .. } => name.into(),
}
}
- fn icon(self) -> IconName {
+ fn icon(&self) -> Option<IconName> {
match self {
- Self::Zed | Self::TextThread => IconName::AiZed,
- Self::NativeAgent => IconName::ZedAssistant,
- Self::Gemini => IconName::AiGemini,
- Self::ClaudeCode => IconName::AiClaude,
+ Self::Zed | Self::NativeAgent | Self::TextThread => None,
+ Self::Gemini => Some(IconName::AiGemini),
+ Self::ClaudeCode => Some(IconName::AiClaude),
+ Self::Custom { .. } => Some(IconName::Terminal),
+ }
+ }
+}
+
+impl From<ExternalAgent> for AgentType {
+ fn from(value: ExternalAgent) -> Self {
+ match value {
+ ExternalAgent::Gemini => Self::Gemini,
+ ExternalAgent::ClaudeCode => Self::ClaudeCode,
+ ExternalAgent::Custom { name, command } => Self::Custom { name, command },
+ ExternalAgent::NativeAgent => Self::NativeAgent,
}
}
}
@@ -356,7 +396,7 @@ impl ActiveView {
Self::Thread {
change_title_editor: editor,
thread: active_thread,
- message_editor: message_editor,
+ message_editor,
_subscriptions: subscriptions,
}
}
@@ -364,6 +404,7 @@ impl ActiveView {
pub fn prompt_editor(
context_editor: Entity<TextThreadEditor>,
history_store: Entity<HistoryStore>,
+ acp_history_store: Entity<agent2::HistoryStore>,
language_registry: Arc<LanguageRegistry>,
window: &mut Window,
cx: &mut App,
@@ -441,6 +482,18 @@ impl ActiveView {
);
}
});
+
+ acp_history_store.update(cx, |history_store, cx| {
+ if let Some(old_path) = old_path {
+ history_store
+ .replace_recently_opened_text_thread(old_path, new_path, cx);
+ } else {
+ history_store.push_recently_opened_entry(
+ agent2::HistoryEntryId::TextThread(new_path.clone()),
+ cx,
+ );
+ }
+ });
}
_ => {}
}
@@ -469,6 +522,8 @@ pub struct AgentPanel {
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
thread_store: Entity<ThreadStore>,
+ acp_history: Entity<AcpThreadHistory>,
+ acp_history_store: Entity<agent2::HistoryStore>,
_default_model_subscription: Subscription,
context_store: Entity<TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
@@ -477,8 +532,6 @@ pub struct AgentPanel {
configuration_subscription: Option<Subscription>,
local_timezone: UtcOffset,
active_view: ActiveView,
- acp_message_history:
- Rc<RefCell<crate::acp::MessageHistory<Vec<agent_client_protocol::ContentBlock>>>>,
previous_view: Option<ActiveView>,
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
@@ -498,7 +551,7 @@ pub struct AgentPanel {
impl AgentPanel {
fn serialize(&mut self, cx: &mut Context<Self>) {
let width = self.width;
- let selected_agent = self.selected_agent;
+ let selected_agent = self.selected_agent.clone();
self.pending_serialization = Some(cx.background_spawn(async move {
KEY_VALUE_STORE
.write_kvp(
@@ -512,6 +565,7 @@ impl AgentPanel {
anyhow::Ok(())
}));
}
+
pub fn load(
workspace: WeakEntity<Workspace>,
prompt_builder: Arc<PromptBuilder>,
@@ -556,7 +610,7 @@ impl AgentPanel {
.log_err()
.flatten()
{
- Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
+ serde_json::from_str::<SerializedAgentPanel>(&panel).log_err()
} else {
None
};
@@ -576,10 +630,15 @@ impl AgentPanel {
panel.update(cx, |panel, cx| {
panel.width = serialized_panel.width.map(|w| w.round());
if let Some(selected_agent) = serialized_panel.selected_agent {
- panel.selected_agent = selected_agent;
+ panel.selected_agent = selected_agent.clone();
+ panel.new_agent_thread(selected_agent, window, cx);
}
cx.notify();
});
+ } else {
+ panel.update(cx, |panel, cx| {
+ panel.new_agent_thread(AgentType::NativeAgent, window, cx);
+ });
}
panel
})?;
@@ -636,6 +695,29 @@ impl AgentPanel {
)
});
+ let acp_history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx));
+ let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx));
+ cx.subscribe_in(
+ &acp_history,
+ window,
+ |this, _, event, window, cx| match event {
+ ThreadHistoryEvent::Open(HistoryEntry::AcpThread(thread)) => {
+ this.external_thread(
+ Some(crate::ExternalAgent::NativeAgent),
+ Some(thread.clone()),
+ None,
+ window,
+ cx,
+ );
+ }
+ ThreadHistoryEvent::Open(HistoryEntry::TextThread(thread)) => {
+ this.open_saved_prompt_editor(thread.path.clone(), window, cx)
+ .detach_and_log_err(cx);
+ }
+ },
+ )
+ .detach();
+
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
let active_thread = cx.new(|cx| {
@@ -674,6 +756,7 @@ impl AgentPanel {
ActiveView::prompt_editor(
context_editor,
history_store.clone(),
+ acp_history_store.clone(),
language_registry.clone(),
window,
cx,
@@ -690,7 +773,11 @@ impl AgentPanel {
let assistant_navigation_menu =
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
if let Some(panel) = panel.upgrade() {
- menu = Self::populate_recently_opened_menu_section(menu, panel, cx);
+ if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
+ menu = Self::populate_recently_opened_menu_section_new(menu, panel, cx);
+ } else {
+ menu = Self::populate_recently_opened_menu_section_old(menu, panel, cx);
+ }
}
menu.action("View All", Box::new(OpenHistory))
.end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
@@ -716,25 +803,25 @@ impl AgentPanel {
.ok();
});
- let _default_model_subscription = cx.subscribe(
- &LanguageModelRegistry::global(cx),
- |this, _, event: &language_model::Event, cx| match event {
- language_model::Event::DefaultModelChanged => match &this.active_view {
- ActiveView::Thread { thread, .. } => {
- thread
- .read(cx)
- .thread()
- .clone()
- .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
+ let _default_model_subscription =
+ cx.subscribe(
+ &LanguageModelRegistry::global(cx),
+ |this, _, event: &language_model::Event, cx| {
+ if let language_model::Event::DefaultModelChanged = event {
+ match &this.active_view {
+ ActiveView::Thread { thread, .. } => {
+ thread.read(cx).thread().clone().update(cx, |thread, cx| {
+ thread.get_or_init_configured_model(cx)
+ });
+ }
+ ActiveView::ExternalAgentThread { .. }
+ | ActiveView::TextThread { .. }
+ | ActiveView::History
+ | ActiveView::Configuration => {}
+ }
}
- ActiveView::ExternalAgentThread { .. }
- | ActiveView::TextThread { .. }
- | ActiveView::History
- | ActiveView::Configuration => {}
},
- _ => {}
- },
- );
+ );
let onboarding = cx.new(|cx| {
AgentPanelOnboarding::new(
@@ -766,7 +853,6 @@ impl AgentPanel {
.unwrap(),
inline_assist_context_store,
previous_view: None,
- acp_message_history: Default::default(),
history_store: history_store.clone(),
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
hovered_recent_history_item: None,
@@ -779,6 +865,8 @@ impl AgentPanel {
zoomed: false,
pending_serialization: None,
onboarding,
+ acp_history,
+ acp_history_store,
selected_agent: AgentType::default(),
}
}
@@ -823,10 +911,10 @@ impl AgentPanel {
ActiveView::Thread { thread, .. } => {
thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
}
- ActiveView::ExternalAgentThread { thread_view, .. } => {
- thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx));
- }
- ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
+ ActiveView::ExternalAgentThread { .. }
+ | ActiveView::TextThread { .. }
+ | ActiveView::History
+ | ActiveView::Configuration => {}
}
}
@@ -840,7 +928,20 @@ impl AgentPanel {
}
}
+ fn active_thread_view(&self) -> Option<&Entity<AcpThreadView>> {
+ match &self.active_view {
+ ActiveView::ExternalAgentThread { thread_view, .. } => Some(thread_view),
+ ActiveView::Thread { .. }
+ | ActiveView::TextThread { .. }
+ | ActiveView::History
+ | ActiveView::Configuration => None,
+ }
+ }
+
fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
+ if cx.has_flag::<GeminiAndNativeFeatureFlag>() {
+ return self.new_agent_thread(AgentType::NativeAgent, window, cx);
+ }
// Preserve chat box text when using creating new thread
let preserved_text = self
.active_message_editor()
@@ -913,13 +1014,38 @@ impl AgentPanel {
message_editor.focus_handle(cx).focus(window);
- let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
+ let thread_view = ActiveView::thread(active_thread, message_editor, window, cx);
self.set_active_view(thread_view, window, cx);
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
}
+ fn new_native_agent_thread_from_summary(
+ &mut self,
+ action: &NewNativeAgentThreadFromSummary,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(thread) = self
+ .acp_history_store
+ .read(cx)
+ .thread_from_session_id(&action.from_session_id)
+ else {
+ return;
+ };
+
+ self.external_thread(
+ Some(ExternalAgent::NativeAgent),
+ None,
+ Some(thread.clone()),
+ window,
+ cx,
+ );
+ }
+
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ telemetry::event!("Agent Thread Started", agent = "zed-text");
+
let context = self
.context_store
.update(cx, |context_store, cx| context_store.create(cx));
@@ -941,10 +1067,16 @@ impl AgentPanel {
editor
});
+ if self.selected_agent != AgentType::TextThread {
+ self.selected_agent = AgentType::TextThread;
+ self.serialize(cx);
+ }
+
self.set_active_view(
ActiveView::prompt_editor(
context_editor.clone(),
self.history_store.clone(),
+ self.acp_history_store.clone(),
self.language_registry.clone(),
window,
cx,
@@ -955,16 +1087,18 @@ impl AgentPanel {
context_editor.focus_handle(cx).focus(window);
}
- fn new_external_thread(
+ fn external_thread(
&mut self,
agent_choice: Option<crate::ExternalAgent>,
+ resume_thread: Option<DbThreadMetadata>,
+ summarize_thread: Option<DbThreadMetadata>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let workspace = self.workspace.clone();
let project = self.project.clone();
- let message_history = self.acp_message_history.clone();
let fs = self.fs.clone();
+ let is_via_collab = self.project.read(cx).is_via_collab();
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
@@ -973,52 +1107,82 @@ impl AgentPanel {
agent: crate::ExternalAgent,
}
- let thread_store = self.thread_store.clone();
- let text_thread_store = self.context_store.clone();
+ let history = self.acp_history_store.clone();
cx.spawn_in(window, async move |this, cx| {
- let server: Rc<dyn AgentServer> = match agent_choice {
+ let ext_agent = match agent_choice {
Some(agent) => {
- cx.background_spawn(async move {
- if let Some(serialized) =
- serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
- {
- KEY_VALUE_STORE
- .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
- .await
- .log_err();
+ cx.background_spawn({
+ let agent = agent.clone();
+ async move {
+ if let Some(serialized) =
+ serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
+ {
+ KEY_VALUE_STORE
+ .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
+ .await
+ .log_err();
+ }
}
})
.detach();
- agent.server(fs)
+ agent
+ }
+ None => {
+ if is_via_collab {
+ ExternalAgent::NativeAgent
+ } else {
+ cx.background_spawn(async move {
+ KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
+ })
+ .await
+ .log_err()
+ .flatten()
+ .and_then(|value| {
+ serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
+ })
+ .unwrap_or_default()
+ .agent
+ }
}
- None => cx
- .background_spawn(async move {
- KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
- })
- .await
- .log_err()
- .flatten()
- .and_then(|value| {
- serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
- })
- .unwrap_or_default()
- .agent
- .server(fs),
};
+ telemetry::event!("Agent Thread Started", agent = ext_agent.name());
+
+ let server = ext_agent.server(fs, history);
+
this.update_in(cx, |this, window, cx| {
+ match ext_agent {
+ crate::ExternalAgent::Gemini
+ | crate::ExternalAgent::NativeAgent
+ | crate::ExternalAgent::Custom { .. } => {
+ if !cx.has_flag::<GeminiAndNativeFeatureFlag>() {
+ return;
+ }
+ }
+ crate::ExternalAgent::ClaudeCode => {
+ if !cx.has_flag::<ClaudeCodeFeatureFlag>() {
+ return;
+ }
+ }
+ }
+
+ let selected_agent = ext_agent.into();
+ if this.selected_agent != selected_agent {
+ this.selected_agent = selected_agent;
+ this.serialize(cx);
+ }
+
let thread_view = cx.new(|cx| {
crate::acp::AcpThreadView::new(
server,
+ resume_thread,
+ summarize_thread,
workspace.clone(),
project,
- thread_store.clone(),
- text_thread_store.clone(),
- message_history,
- MIN_EDITOR_LINES,
- Some(MAX_EDITOR_LINES),
+ this.acp_history_store.clone(),
+ this.prompt_store.clone(),
window,
cx,
)
@@ -1105,10 +1269,17 @@ impl AgentPanel {
cx,
)
});
+
+ if self.selected_agent != AgentType::TextThread {
+ self.selected_agent = AgentType::TextThread;
+ self.serialize(cx);
+ }
+
self.set_active_view(
ActiveView::prompt_editor(
- editor.clone(),
+ editor,
self.history_store.clone(),
+ self.acp_history_store.clone(),
self.language_registry.clone(),
window,
cx,
@@ -1179,7 +1350,7 @@ impl AgentPanel {
});
message_editor.focus_handle(cx).focus(window);
- let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
+ let thread_view = ActiveView::thread(active_thread, message_editor, window, cx);
self.set_active_view(thread_view, window, cx);
AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
}
@@ -1266,13 +1437,11 @@ impl AgentPanel {
ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
let _ = settings
.agent_font_size
- .insert(theme::clamp_font_size(agent_font_size).0);
+ .insert(Some(theme::clamp_font_size(agent_font_size).into()));
},
);
} else {
- theme::adjust_agent_font_size(cx, |size| {
- *size += delta;
- });
+ theme::adjust_agent_font_size(cx, |size| size + delta);
}
}
WhichFontSize::BufferFont => {
@@ -1338,6 +1507,7 @@ impl AgentPanel {
}
pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let agent_server_store = self.project.read(cx).agent_server_store().clone();
let context_server_store = self.project.read(cx).context_server_store();
let tools = self.thread_store.read(cx).tools();
let fs = self.fs.clone();
@@ -1346,6 +1516,7 @@ impl AgentPanel {
self.configuration = Some(cx.new(|cx| {
AgentConfiguration::new(
fs,
+ agent_server_store,
context_server_store,
tools,
self.language_registry.clone(),
@@ -1408,15 +1579,14 @@ impl AgentPanel {
AssistantConfigurationEvent::NewThread(provider) => {
if LanguageModelRegistry::read_global(cx)
.default_model()
- .map_or(true, |model| model.provider.id() != provider.id())
+ .is_none_or(|model| model.provider.id() != provider.id())
+ && let Some(model) = provider.default_model(cx)
{
- if let Some(model) = provider.default_model(cx) {
- update_settings_file::<AgentSettings>(
- self.fs.clone(),
- cx,
- move |settings, _| settings.set_model(model),
- );
- }
+ update_settings_file::<AgentSettings>(
+ self.fs.clone(),
+ cx,
+ move |settings, _| settings.set_model(model),
+ );
}
self.new_thread(&NewThread::default(), window, cx);
@@ -1443,6 +1613,14 @@ impl AgentPanel {
_ => None,
}
}
+ pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
+ match &self.active_view {
+ ActiveView::ExternalAgentThread { thread_view, .. } => {
+ thread_view.read(cx).thread().cloned()
+ }
+ _ => None,
+ }
+ }
pub(crate) fn delete_thread(
&mut self,
@@ -1463,7 +1641,7 @@ impl AgentPanel {
return;
}
- let model = thread_state.configured_model().map(|cm| cm.model.clone());
+ let model = thread_state.configured_model().map(|cm| cm.model);
if let Some(model) = model {
thread.update(cx, |active_thread, cx| {
active_thread.thread().update(cx, |thread, cx| {
@@ -1535,17 +1713,14 @@ impl AgentPanel {
let current_is_special = current_is_history || current_is_config;
let new_is_special = new_is_history || new_is_config;
- match &self.active_view {
- ActiveView::Thread { thread, .. } => {
- let thread = thread.read(cx);
- if thread.is_empty() {
- let id = thread.thread().read(cx).id().clone();
- self.history_store.update(cx, |store, cx| {
- store.remove_recently_opened_thread(id, cx);
- });
- }
+ if let ActiveView::Thread { thread, .. } = &self.active_view {
+ let thread = thread.read(cx);
+ if thread.is_empty() {
+ let id = thread.thread().read(cx).id().clone();
+ self.history_store.update(cx, |store, cx| {
+ store.remove_recently_opened_thread(id, cx);
+ });
}
- _ => {}
}
match &new_view {
@@ -1558,6 +1733,14 @@ impl AgentPanel {
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
}
+ });
+ self.acp_history_store.update(cx, |store, cx| {
+ if let Some(path) = context_editor.read(cx).context().read(cx).path() {
+ store.push_recently_opened_entry(
+ agent2::HistoryEntryId::TextThread(path.clone()),
+ cx,
+ )
+ }
})
}
ActiveView::ExternalAgentThread { .. } => {}
@@ -1575,12 +1758,10 @@ impl AgentPanel {
self.active_view = new_view;
}
- self.acp_message_history.borrow_mut().reset_position();
-
self.focus_handle(cx).focus(window);
}
- fn populate_recently_opened_menu_section(
+ fn populate_recently_opened_menu_section_old(
mut menu: ContextMenu,
panel: Entity<Self>,
cx: &mut Context<ContextMenu>,
@@ -1615,7 +1796,7 @@ impl AgentPanel {
.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx),
HistoryEntryId::Context(path) => this
- .open_saved_prompt_editor(path.clone(), window, cx)
+ .open_saved_prompt_editor(path, window, cx)
.detach_and_log_err(cx),
})
.ok();
@@ -1644,15 +1825,140 @@ impl AgentPanel {
menu
}
- pub fn set_selected_agent(&mut self, agent: AgentType, cx: &mut Context<Self>) {
- if self.selected_agent != agent {
- self.selected_agent = agent;
- self.serialize(cx);
+ fn populate_recently_opened_menu_section_new(
+ mut menu: ContextMenu,
+ panel: Entity<Self>,
+ cx: &mut Context<ContextMenu>,
+ ) -> ContextMenu {
+ let entries = panel
+ .read(cx)
+ .acp_history_store
+ .read(cx)
+ .recently_opened_entries(cx);
+
+ if entries.is_empty() {
+ return menu;
+ }
+
+ menu = menu.header("Recently Opened");
+
+ for entry in entries {
+ let title = entry.title().clone();
+
+ menu = menu.entry_with_end_slot_on_hover(
+ title,
+ None,
+ {
+ let panel = panel.downgrade();
+ let entry = entry.clone();
+ move |window, cx| {
+ let entry = entry.clone();
+ panel
+ .update(cx, move |this, cx| match &entry {
+ agent2::HistoryEntry::AcpThread(entry) => this.external_thread(
+ Some(ExternalAgent::NativeAgent),
+ Some(entry.clone()),
+ None,
+ window,
+ cx,
+ ),
+ agent2::HistoryEntry::TextThread(entry) => this
+ .open_saved_prompt_editor(entry.path.clone(), window, cx)
+ .detach_and_log_err(cx),
+ })
+ .ok();
+ }
+ },
+ IconName::Close,
+ "Close Entry".into(),
+ {
+ let panel = panel.downgrade();
+ let id = entry.id();
+ move |_window, cx| {
+ panel
+ .update(cx, |this, cx| {
+ this.acp_history_store.update(cx, |history_store, cx| {
+ history_store.remove_recently_opened_entry(&id, cx);
+ });
+ })
+ .ok();
+ }
+ },
+ );
}
+
+ menu = menu.separator();
+
+ menu
}
pub fn selected_agent(&self) -> AgentType {
- self.selected_agent
+ self.selected_agent.clone()
+ }
+
+ pub fn new_agent_thread(
+ &mut self,
+ agent: AgentType,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ match agent {
+ AgentType::Zed => {
+ window.dispatch_action(
+ NewThread {
+ from_thread_id: None,
+ }
+ .boxed_clone(),
+ cx,
+ );
+ }
+ AgentType::TextThread => {
+ window.dispatch_action(NewTextThread.boxed_clone(), cx);
+ }
+ AgentType::NativeAgent => self.external_thread(
+ Some(crate::ExternalAgent::NativeAgent),
+ None,
+ None,
+ window,
+ cx,
+ ),
+ AgentType::Gemini => {
+ self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
+ }
+ AgentType::ClaudeCode => {
+ self.selected_agent = AgentType::ClaudeCode;
+ self.serialize(cx);
+ self.external_thread(
+ Some(crate::ExternalAgent::ClaudeCode),
+ None,
+ None,
+ window,
+ cx,
+ )
+ }
+ AgentType::Custom { name, command } => self.external_thread(
+ Some(crate::ExternalAgent::Custom { name, command }),
+ None,
+ None,
+ window,
+ cx,
+ ),
+ }
+ }
+
+ pub fn load_agent_thread(
+ &mut self,
+ thread: DbThreadMetadata,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.external_thread(
+ Some(ExternalAgent::NativeAgent),
+ Some(thread),
+ None,
+ window,
+ cx,
+ );
}
}
@@ -1661,7 +1967,13 @@ impl Focusable for AgentPanel {
match &self.active_view {
ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
- ActiveView::History => self.history.focus_handle(cx),
+ ActiveView::History => {
+ if cx.has_flag::<feature_flags::GeminiAndNativeFeatureFlag>() {
+ self.acp_history.focus_handle(cx)
+ } else {
+ self.history.focus_handle(cx)
+ }
+ }
ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
ActiveView::Configuration => {
if let Some(configuration) = self.configuration.as_ref() {
@@ -1783,11 +2095,13 @@ impl AgentPanel {
};
match state {
- ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
+ ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT)
.truncate()
+ .color(Color::Muted)
.into_any_element(),
ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
.truncate()
+ .color(Color::Muted)
.into_any_element(),
ThreadSummary::Ready(_) => div()
.w_full()
@@ -1797,7 +2111,8 @@ impl AgentPanel {
.w_full()
.child(change_title_editor.clone())
.child(
- ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
+ IconButton::new("retry-summary-generation", IconName::RotateCcw)
+ .icon_size(IconSize::Small)
.on_click({
let active_thread = active_thread.clone();
move |_, _window, cx| {
@@ -1818,9 +2133,33 @@ impl AgentPanel {
}
}
ActiveView::ExternalAgentThread { thread_view } => {
- Label::new(thread_view.read(cx).title(cx))
- .truncate()
- .into_any_element()
+ if let Some(title_editor) = thread_view.read(cx).title_editor() {
+ div()
+ .w_full()
+ .on_action({
+ let thread_view = thread_view.downgrade();
+ move |_: &menu::Confirm, window, cx| {
+ if let Some(thread_view) = thread_view.upgrade() {
+ thread_view.focus_handle(cx).focus(window);
+ }
+ }
+ })
+ .on_action({
+ let thread_view = thread_view.downgrade();
+ move |_: &editor::actions::Cancel, window, cx| {
+ if let Some(thread_view) = thread_view.upgrade() {
+ thread_view.focus_handle(cx).focus(window);
+ }
+ }
+ })
+ .child(title_editor)
+ .into_any_element()
+ } else {
+ Label::new(thread_view.read(cx).title(cx))
+ .color(Color::Muted)
+ .truncate()
+ .into_any_element()
+ }
}
ActiveView::TextThread {
title_editor,
@@ -5,7 +5,6 @@ mod agent_diff;
mod agent_model_selector;
mod agent_panel;
mod buffer_codegen;
-mod burn_mode_tooltip;
mod context_picker;
mod context_server_configuration;
mod context_strip;
@@ -35,12 +34,13 @@ use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _;
use fs::Fs;
-use gpui::{Action, App, Entity, actions};
+use gpui::{Action, App, Entity, SharedString, actions};
use language::LanguageRegistry;
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
};
use project::DisableAiSettings;
+use project::agent_server_store::AgentServerCommand;
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -72,8 +72,10 @@ actions!(
ToggleOptionsMenu,
/// Deletes the recently opened thread from history.
DeleteRecentlyOpenThread,
- /// Toggles the profile selector for switching between agent profiles.
+ /// Toggles the profile or mode selector for switching between agent profiles.
ToggleProfileSelector,
+ /// Cycles through available session modes.
+ CycleModeSelector,
/// Removes all added context from the current conversation.
RemoveAllContext,
/// Expands the message editor to full size.
@@ -114,6 +116,12 @@ actions!(
RejectAll,
/// Keeps all suggestions or changes.
KeepAll,
+ /// Allow this operation only this time.
+ AllowOnce,
+ /// Allow this operation and remember the choice.
+ AllowAlways,
+ /// Reject this operation only this time.
+ RejectOnce,
/// Follows the agent's suggestions.
Follow,
/// Resets the trial upsell notification.
@@ -129,6 +137,12 @@ actions!(
]
);
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Action)]
+#[action(namespace = agent)]
+#[action(deprecated_aliases = ["assistant::QuoteSelection"])]
+/// Quotes the current selection in the agent panel's message editor.
+pub struct QuoteSelection;
+
/// Creates a new conversation thread, optionally based on an existing thread.
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)]
@@ -147,21 +161,57 @@ pub struct NewExternalAgentThread {
agent: Option<ExternalAgent>,
}
-#[derive(Default, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
+#[action(namespace = agent)]
+#[serde(deny_unknown_fields)]
+pub struct NewNativeAgentThreadFromSummary {
+ from_session_id: agent_client_protocol::SessionId,
+}
+
+// TODO unify this with AgentType
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
enum ExternalAgent {
#[default]
Gemini,
ClaudeCode,
NativeAgent,
+ Custom {
+ name: SharedString,
+ command: AgentServerCommand,
+ },
+}
+
+fn placeholder_command() -> AgentServerCommand {
+ AgentServerCommand {
+ path: "/placeholder".into(),
+ args: vec![],
+ env: None,
+ }
}
impl ExternalAgent {
- pub fn server(&self, fs: Arc<dyn fs::Fs>) -> Rc<dyn agent_servers::AgentServer> {
+ fn name(&self) -> &'static str {
+ match self {
+ Self::NativeAgent => "zed",
+ Self::Gemini => "gemini-cli",
+ Self::ClaudeCode => "claude-code",
+ Self::Custom { .. } => "custom",
+ }
+ }
+
+ pub fn server(
+ &self,
+ fs: Arc<dyn fs::Fs>,
+ history: Entity<agent2::HistoryStore>,
+ ) -> Rc<dyn agent_servers::AgentServer> {
match self {
- ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
- ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
- ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs)),
+ Self::Gemini => Rc::new(agent_servers::Gemini),
+ Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
+ Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
+ Self::Custom { name, command: _ } => {
+ Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
+ }
}
}
}
@@ -237,13 +287,7 @@ pub fn init(
client.telemetry().clone(),
cx,
);
- terminal_inline_assistant::init(
- fs.clone(),
- prompt_builder.clone(),
- client.telemetry().clone(),
- cx,
- );
- indexed_docs::init(cx);
+ terminal_inline_assistant::init(fs.clone(), prompt_builder, client.telemetry().clone(), cx);
cx.observe_new(move |workspace, window, cx| {
ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
})
@@ -308,8 +352,7 @@ fn update_command_palette_filter(cx: &mut App) {
];
filter.show_action_types(edit_prediction_actions.iter());
- filter
- .show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
+ filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
}
});
}
@@ -322,7 +365,7 @@ fn init_language_model_settings(cx: &mut App) {
cx.subscribe(
&LanguageModelRegistry::global(cx),
|_, event: &language_model::Event, cx| match event {
- language_model::Event::ProviderStateChanged
+ language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
update_active_language_model_from_settings(cx);
@@ -389,7 +432,6 @@ fn register_slash_commands(cx: &mut App) {
slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({
- let slash_command_registry = slash_command_registry.clone();
move |is_enabled, _cx| {
if is_enabled {
slash_command_registry.register_command(
@@ -410,12 +452,6 @@ fn update_slash_commands_from_settings(cx: &mut App) {
let slash_command_registry = SlashCommandRegistry::global(cx);
let settings = SlashCommandSettings::get_global(cx);
- if settings.docs.enabled {
- slash_command_registry.register_command(assistant_slash_commands::DocsSlashCommand, true);
- } else {
- slash_command_registry.unregister_command(assistant_slash_commands::DocsSlashCommand);
- }
-
if settings.cargo_workspace.enabled {
slash_command_registry
.register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);
@@ -352,12 +352,12 @@ impl CodegenAlternative {
event: &multi_buffer::Event,
cx: &mut Context<Self>,
) {
- if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
- if self.transformation_transaction_id == Some(*transaction_id) {
- self.transformation_transaction_id = None;
- self.generation = Task::ready(());
- cx.emit(CodegenEvent::Undone);
- }
+ if let multi_buffer::Event::TransactionUndone { transaction_id } = event
+ && self.transformation_transaction_id == Some(*transaction_id)
+ {
+ self.transformation_transaction_id = None;
+ self.generation = Task::ready(());
+ cx.emit(CodegenEvent::Undone);
}
}
@@ -388,7 +388,7 @@ impl CodegenAlternative {
} else {
let request = self.build_request(&model, user_prompt, cx)?;
cx.spawn(async move |_, cx| {
- Ok(model.stream_completion_text(request.await, &cx).await?)
+ Ok(model.stream_completion_text(request.await, cx).await?)
})
.boxed_local()
};
@@ -447,7 +447,7 @@ impl CodegenAlternative {
}
});
- let temperature = AgentSettings::temperature_for_model(&model, cx);
+ let temperature = AgentSettings::temperature_for_model(model, cx);
Ok(cx.spawn(async move |_cx| {
let mut request_message = LanguageModelRequestMessage {
@@ -576,38 +576,34 @@ impl CodegenAlternative {
let mut lines = chunk.split('\n').peekable();
while let Some(line) = lines.next() {
new_text.push_str(line);
- if line_indent.is_none() {
- if let Some(non_whitespace_ch_ix) =
+ if line_indent.is_none()
+ && let Some(non_whitespace_ch_ix) =
new_text.find(|ch: char| !ch.is_whitespace())
- {
- line_indent = Some(non_whitespace_ch_ix);
- base_indent = base_indent.or(line_indent);
-
- let line_indent = line_indent.unwrap();
- let base_indent = base_indent.unwrap();
- let indent_delta =
- line_indent as i32 - base_indent as i32;
- let mut corrected_indent_len = cmp::max(
- 0,
- suggested_line_indent.len as i32 + indent_delta,
- )
- as usize;
- if first_line {
- corrected_indent_len = corrected_indent_len
- .saturating_sub(
- selection_start.column as usize,
- );
- }
-
- let indent_char = suggested_line_indent.char();
- let mut indent_buffer = [0; 4];
- let indent_str =
- indent_char.encode_utf8(&mut indent_buffer);
- new_text.replace_range(
- ..line_indent,
- &indent_str.repeat(corrected_indent_len),
- );
+ {
+ line_indent = Some(non_whitespace_ch_ix);
+ base_indent = base_indent.or(line_indent);
+
+ let line_indent = line_indent.unwrap();
+ let base_indent = base_indent.unwrap();
+ let indent_delta = line_indent as i32 - base_indent as i32;
+ let mut corrected_indent_len = cmp::max(
+ 0,
+ suggested_line_indent.len as i32 + indent_delta,
+ )
+ as usize;
+ if first_line {
+ corrected_indent_len = corrected_indent_len
+ .saturating_sub(selection_start.column as usize);
}
+
+ let indent_char = suggested_line_indent.char();
+ let mut indent_buffer = [0; 4];
+ let indent_str =
+ indent_char.encode_utf8(&mut indent_buffer);
+ new_text.replace_range(
+ ..line_indent,
+ &indent_str.repeat(corrected_indent_len),
+ );
}
if line_indent.is_some() {
@@ -1028,7 +1024,7 @@ where
chunk.push('\n');
}
- chunk.push_str(&line);
+ chunk.push_str(line);
}
consumed += line.len();
@@ -1133,7 +1129,7 @@ mod tests {
)
});
- let chunks_tx = simulate_response_stream(codegen.clone(), cx);
+ let chunks_tx = simulate_response_stream(&codegen, cx);
let mut new_text = concat!(
" let mut x = 0;\n",
@@ -1143,7 +1139,7 @@ mod tests {
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
- let len = rng.gen_range(1..=max_len);
+ let len = rng.random_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;
@@ -1200,7 +1196,7 @@ mod tests {
)
});
- let chunks_tx = simulate_response_stream(codegen.clone(), cx);
+ let chunks_tx = simulate_response_stream(&codegen, cx);
cx.background_executor.run_until_parked();
@@ -1212,7 +1208,7 @@ mod tests {
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
- let len = rng.gen_range(1..=max_len);
+ let len = rng.random_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;
@@ -1269,7 +1265,7 @@ mod tests {
)
});
- let chunks_tx = simulate_response_stream(codegen.clone(), cx);
+ let chunks_tx = simulate_response_stream(&codegen, cx);
cx.background_executor.run_until_parked();
@@ -1281,7 +1277,7 @@ mod tests {
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
- let len = rng.gen_range(1..=max_len);
+ let len = rng.random_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;
@@ -1338,7 +1334,7 @@ mod tests {
)
});
- let chunks_tx = simulate_response_stream(codegen.clone(), cx);
+ let chunks_tx = simulate_response_stream(&codegen, cx);
let new_text = concat!(
"func main() {\n",
"\tx := 0\n",
@@ -1395,7 +1391,7 @@ mod tests {
)
});
- let chunks_tx = simulate_response_stream(codegen.clone(), cx);
+ let chunks_tx = simulate_response_stream(&codegen, cx);
chunks_tx
.unbounded_send("let mut x = 0;\nx += 1;".to_string())
.unwrap();
@@ -1477,7 +1473,7 @@ mod tests {
}
fn simulate_response_stream(
- codegen: Entity<CodegenAlternative>,
+ codegen: &Entity<CodegenAlternative>,
cx: &mut TestAppContext,
) -> mpsc::UnboundedSender<String> {
let (chunks_tx, chunks_rx) = mpsc::unbounded();
@@ -1,61 +0,0 @@
-use gpui::{Context, FontWeight, IntoElement, Render, Window};
-use ui::{prelude::*, tooltip_container};
-
-pub struct BurnModeTooltip {
- selected: bool,
-}
-
-impl BurnModeTooltip {
- pub fn new() -> Self {
- Self { selected: false }
- }
-
- pub fn selected(mut self, selected: bool) -> Self {
- self.selected = selected;
- self
- }
-}
-
-impl Render for BurnModeTooltip {
- fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let (icon, color) = if self.selected {
- (IconName::ZedBurnModeOn, Color::Error)
- } else {
- (IconName::ZedBurnMode, Color::Default)
- };
-
- let turned_on = h_flex()
- .h_4()
- .px_1()
- .border_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().text_accent.opacity(0.1))
- .rounded_sm()
- .child(
- Label::new("ON")
- .size(LabelSize::XSmall)
- .weight(FontWeight::SEMIBOLD)
- .color(Color::Accent),
- );
-
- let title = h_flex()
- .gap_1p5()
- .child(Icon::new(icon).size(IconSize::Small).color(color))
- .child(Label::new("Burn Mode"))
- .when(self.selected, |title| title.child(turned_on));
-
- tooltip_container(window, cx, |this, _, _| {
- this
- .child(title)
- .child(
- div()
- .max_w_64()
- .child(
- Label::new("Enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning.")
- .size(LabelSize::Small)
- .color(Color::Muted)
- )
- )
- })
- }
-}
@@ -13,7 +13,7 @@ use anyhow::{Result, anyhow};
use collections::HashSet;
pub use completion_provider::ContextPickerCompletionProvider;
use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
-use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
+use editor::{Anchor, Editor, ExcerptId, FoldPlaceholder, ToOffset};
use fetch_context_picker::FetchContextPicker;
use file_context_picker::FileContextPicker;
use file_context_picker::render_file_context_entry;
@@ -228,7 +228,7 @@ impl ContextPicker {
}
fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
- let context_picker = cx.entity().clone();
+ let context_picker = cx.entity();
let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
let recent = self.recent_entries(cx);
@@ -385,12 +385,11 @@ impl ContextPicker {
}
pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- match &self.mode {
- ContextPickerState::Default(entity) => entity.update(cx, |entity, cx| {
+ // Other variants already select their first entry on open automatically
+ if let ContextPickerState::Default(entity) = &self.mode {
+ entity.update(cx, |entity, cx| {
entity.select_first(&Default::default(), window, cx)
- }),
- // Other variants already select their first entry on open automatically
- _ => {}
+ })
}
}
@@ -610,9 +609,7 @@ pub(crate) fn available_context_picker_entries(
.read(cx)
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
- .map_or(false, |editor| {
- editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))
- });
+ .is_some_and(|editor| editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx)));
if has_selection {
entries.push(ContextPickerEntry::Action(
ContextPickerAction::AddSelections,
@@ -680,7 +677,7 @@ pub(crate) fn recent_context_picker_entries(
.filter(|(_, abs_path)| {
abs_path
.as_ref()
- .map_or(true, |path| !exclude_paths.contains(path.as_path()))
+ .is_none_or(|path| !exclude_paths.contains(path.as_path()))
})
.take(4)
.filter_map(|(project_path, _)| {
@@ -821,13 +818,8 @@ pub fn crease_for_mention(
let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
- Crease::inline(
- range,
- placeholder.clone(),
- fold_toggle("mention"),
- render_trailer,
- )
- .with_metadata(CreaseMetadata { icon_path, label })
+ Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
+ .with_metadata(CreaseMetadata { icon_path, label })
}
fn render_fold_icon_button(
@@ -837,42 +829,9 @@ fn render_fold_icon_button(
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
Arc::new({
move |fold_id, fold_range, cx| {
- let is_in_text_selection = editor.upgrade().is_some_and(|editor| {
- editor.update(cx, |editor, cx| {
- let snapshot = editor
- .buffer()
- .update(cx, |multi_buffer, cx| multi_buffer.snapshot(cx));
-
- let is_in_pending_selection = || {
- editor
- .selections
- .pending
- .as_ref()
- .is_some_and(|pending_selection| {
- pending_selection
- .selection
- .range()
- .includes(&fold_range, &snapshot)
- })
- };
-
- let mut is_in_complete_selection = || {
- editor
- .selections
- .disjoint_in_range::<usize>(fold_range.clone(), cx)
- .into_iter()
- .any(|selection| {
- // This is needed to cover a corner case, if we just check for an existing
- // selection in the fold range, having a cursor at the start of the fold
- // marks it as selected. Non-empty selections don't cause this.
- let length = selection.end - selection.start;
- length > 0
- })
- };
-
- is_in_pending_selection() || is_in_complete_selection()
- })
- });
+ let is_in_text_selection = editor
+ .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
+ .unwrap_or_default();
ButtonLike::new(fold_id)
.style(ButtonStyle::Filled)
@@ -1028,7 +987,8 @@ impl MentionLink {
.read(cx)
.project()
.read(cx)
- .entry_for_path(&project_path, cx)?;
+ .entry_for_path(&project_path, cx)?
+ .clone();
Some(MentionLink::File(project_path, entry))
}
Self::SYMBOL => {
@@ -13,7 +13,10 @@ use http_client::HttpClientWithUrl;
use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
-use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, Symbol, WorktreeId};
+use project::{
+ Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
+ Symbol, WorktreeId,
+};
use prompt_store::PromptStore;
use rope::Point;
use text::{Anchor, OffsetRangeExt, ToPoint};
@@ -79,8 +82,7 @@ fn search(
) -> Task<Vec<Match>> {
match mode {
Some(ContextPickerMode::File) => {
- let search_files_task =
- search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
+ let search_files_task = search_files(query, cancellation_flag, &workspace, cx);
cx.background_spawn(async move {
search_files_task
.await
@@ -91,8 +93,7 @@ fn search(
}
Some(ContextPickerMode::Symbol) => {
- let search_symbols_task =
- search_symbols(query.clone(), cancellation_flag.clone(), &workspace, cx);
+ let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx);
cx.background_spawn(async move {
search_symbols_task
.await
@@ -108,13 +109,8 @@ fn search(
.and_then(|t| t.upgrade())
.zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade()))
{
- let search_threads_task = search_threads(
- query.clone(),
- cancellation_flag.clone(),
- thread_store,
- context_store,
- cx,
- );
+ let search_threads_task =
+ search_threads(query, cancellation_flag, thread_store, context_store, cx);
cx.background_spawn(async move {
search_threads_task
.await
@@ -137,8 +133,7 @@ fn search(
Some(ContextPickerMode::Rules) => {
if let Some(prompt_store) = prompt_store.as_ref() {
- let search_rules_task =
- search_rules(query.clone(), cancellation_flag.clone(), prompt_store, cx);
+ let search_rules_task = search_rules(query, cancellation_flag, prompt_store, cx);
cx.background_spawn(async move {
search_rules_task
.await
@@ -196,7 +191,7 @@ fn search(
let executor = cx.background_executor().clone();
let search_files_task =
- search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
+ search_files(query.clone(), cancellation_flag, &workspace, cx);
let entries =
available_context_picker_entries(&prompt_store, &thread_store, &workspace, cx);
@@ -283,7 +278,7 @@ impl ContextPickerCompletionProvider {
) -> Option<Completion> {
match entry {
ContextPickerEntry::Mode(mode) => Some(Completion {
- replace_range: source_range.clone(),
+ replace_range: source_range,
new_text: format!("@{} ", mode.keyword()),
label: CodeLabel::plain(mode.label().to_string(), None),
icon_path: Some(mode.icon().path().into()),
@@ -330,9 +325,6 @@ impl ContextPickerCompletionProvider {
);
let callback = Arc::new({
- let context_store = context_store.clone();
- let selections = selections.clone();
- let selection_infos = selection_infos.clone();
move |_, window: &mut Window, cx: &mut App| {
context_store.update(cx, |context_store, cx| {
for (buffer, range) in &selections {
@@ -441,7 +433,7 @@ impl ContextPickerCompletionProvider {
excerpt_id,
source_range.start,
new_text_len - 1,
- editor.clone(),
+ editor,
context_store.clone(),
move |window, cx| match &thread_entry {
ThreadContextEntry::Thread { id, .. } => {
@@ -510,7 +502,7 @@ impl ContextPickerCompletionProvider {
excerpt_id,
source_range.start,
new_text_len - 1,
- editor.clone(),
+ editor,
context_store.clone(),
move |_, cx| {
let user_prompt_id = rules.prompt_id;
@@ -547,7 +539,7 @@ impl ContextPickerCompletionProvider {
excerpt_id,
source_range.start,
new_text_len - 1,
- editor.clone(),
+ editor,
context_store.clone(),
move |_, cx| {
let context_store = context_store.clone();
@@ -704,16 +696,16 @@ impl ContextPickerCompletionProvider {
excerpt_id,
source_range.start,
new_text_len - 1,
- editor.clone(),
+ editor,
context_store.clone(),
move |_, cx| {
let symbol = symbol.clone();
let context_store = context_store.clone();
let workspace = workspace.clone();
let result = super::symbol_context_picker::add_symbol(
- symbol.clone(),
+ symbol,
false,
- workspace.clone(),
+ workspace,
context_store.downgrade(),
cx,
);
@@ -728,11 +720,11 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx:
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::default();
- label.push_str(&file_name, None);
+ label.push_str(file_name, None);
label.push_str(" ", None);
if let Some(directory) = directory {
- label.push_str(&directory, comment_id);
+ label.push_str(directory, comment_id);
}
label.filter_range = 0..label.text().len();
@@ -908,6 +900,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
Ok(vec![CompletionResponse {
completions,
+ display_options: CompletionDisplayOptions::default(),
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,
@@ -1020,7 +1013,7 @@ impl MentionCompletion {
&& line
.chars()
.nth(last_mention_start - 1)
- .map_or(false, |c| !c.is_whitespace())
+ .is_some_and(|c| !c.is_whitespace())
{
return None;
}
@@ -1162,7 +1155,7 @@ mod tests {
impl Focusable for AtMentionEditor {
fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.0.read(cx).focus_handle(cx).clone()
+ self.0.read(cx).focus_handle(cx)
}
}
@@ -1480,7 +1473,7 @@ mod tests {
let completions = editor.current_completions().expect("Missing completions");
completions
.into_iter()
- .map(|completion| completion.label.text.to_string())
+ .map(|completion| completion.label.text)
.collect::<Vec<_>>()
}
@@ -226,9 +226,10 @@ impl PickerDelegate for FetchContextPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let added = self.context_store.upgrade().map_or(false, |context_store| {
- context_store.read(cx).includes_url(&self.url)
- });
+ let added = self
+ .context_store
+ .upgrade()
+ .is_some_and(|context_store| context_store.read(cx).includes_url(&self.url));
Some(
ListItem::new(ix)
@@ -160,7 +160,7 @@ impl PickerDelegate for FileContextPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let FileMatch { mat, .. } = &self.matches[ix];
+ let FileMatch { mat, .. } = &self.matches.get(ix)?;
Some(
ListItem::new(ix)
@@ -239,9 +239,7 @@ pub(crate) fn search_files(
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
- include_ignored: worktree
- .root_entry()
- .map_or(false, |entry| entry.is_ignored),
+ include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Entries,
}
@@ -315,7 +313,7 @@ pub fn render_file_context_entry(
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
- let (file_name, directory) = extract_file_name_and_directory(&path, path_prefix);
+ let (file_name, directory) = extract_file_name_and_directory(path, path_prefix);
let added = context_store.upgrade().and_then(|context_store| {
let project_path = ProjectPath {
@@ -334,7 +332,7 @@ pub fn render_file_context_entry(
let file_icon = if is_directory {
FileIcons::get_folder_icon(false, cx)
} else {
- FileIcons::get_icon(&path, cx)
+ FileIcons::get_icon(path, cx)
}
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));
@@ -146,7 +146,7 @@ impl PickerDelegate for RulesContextPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let thread = &self.matches[ix];
+ let thread = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),
@@ -159,7 +159,7 @@ pub fn render_thread_context_entry(
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Div {
- let added = context_store.upgrade().map_or(false, |context_store| {
+ let added = context_store.upgrade().is_some_and(|context_store| {
context_store
.read(cx)
.includes_user_rules(user_rules.prompt_id)
@@ -169,7 +169,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let mat = &self.matches[ix];
+ let mat = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
@@ -289,12 +289,12 @@ pub(crate) fn search_symbols(
.iter()
.enumerate()
.map(|(id, symbol)| {
- StringMatchCandidate::new(id, &symbol.label.filter_text())
+ StringMatchCandidate::new(id, symbol.label.filter_text())
})
.partition(|candidate| {
project
.entry_for_path(&symbols[candidate.id].path, cx)
- .map_or(false, |e| !e.is_ignored)
+ .is_some_and(|e| !e.is_ignored)
})
})
.log_err()
@@ -167,7 +167,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
return;
};
let open_thread_task =
- thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx));
+ thread_store.update(cx, |this, cx| this.open_thread(id, window, cx));
cx.spawn(async move |this, cx| {
let thread = open_thread_task.await?;
@@ -220,7 +220,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let thread = &self.matches[ix];
+ let thread = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),
@@ -236,12 +236,10 @@ pub fn render_thread_context_entry(
let is_added = match entry {
ThreadContextEntry::Thread { id, .. } => context_store
.upgrade()
- .map_or(false, |ctx_store| ctx_store.read(cx).includes_thread(&id)),
- ThreadContextEntry::Context { path, .. } => {
- context_store.upgrade().map_or(false, |ctx_store| {
- ctx_store.read(cx).includes_text_thread(path)
- })
- }
+ .is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(id)),
+ ThreadContextEntry::Context { path, .. } => context_store
+ .upgrade()
+ .is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(path)),
};
h_flex()
@@ -338,7 +336,7 @@ pub(crate) fn search_threads(
let candidates = threads
.iter()
.enumerate()
- .map(|(id, (_, thread))| StringMatchCandidate::new(id, &thread.title()))
+ .map(|(id, (_, thread))| StringMatchCandidate::new(id, thread.title()))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
@@ -145,7 +145,7 @@ impl ContextStrip {
}
let file_name = active_buffer.file()?.file_name(cx);
- let icon_path = FileIcons::get_icon(&Path::new(&file_name), cx);
+ let icon_path = FileIcons::get_icon(Path::new(&file_name), cx);
Some(SuggestedContext::File {
name: file_name.to_string_lossy().into_owned().into(),
buffer: active_buffer_entity.downgrade(),
@@ -368,16 +368,16 @@ impl ContextStrip {
_window: &mut Window,
cx: &mut Context<Self>,
) {
- if let Some(suggested) = self.suggested_context(cx) {
- if self.is_suggested_focused(&self.added_contexts(cx)) {
- self.add_suggested_context(&suggested, cx);
- }
+ if let Some(suggested) = self.suggested_context(cx)
+ && self.is_suggested_focused(&self.added_contexts(cx))
+ {
+ self.add_suggested_context(&suggested, cx);
}
}
fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context<Self>) {
self.context_store.update(cx, |context_store, cx| {
- context_store.add_suggested_context(&suggested, cx)
+ context_store.add_suggested_context(suggested, cx)
});
cx.notify();
}
@@ -1,7 +1,7 @@
#![allow(unused, dead_code)]
use client::{ModelRequestUsage, RequestUsage};
-use cloud_llm_client::{Plan, UsageLimit};
+use cloud_llm_client::{Plan, PlanV1, UsageLimit};
use gpui::Global;
use std::ops::{Deref, DerefMut};
use ui::prelude::*;
@@ -75,7 +75,7 @@ impl Default for DebugAccountState {
Self {
enabled: false,
trial_expired: false,
- plan: Plan::ZedFree,
+ plan: Plan::V1(PlanV1::ZedFree),
custom_prompt_usage: ModelRequestUsage(RequestUsage {
limit: UsageLimit::Unlimited,
amount: 0,
@@ -72,7 +72,7 @@ pub fn init(
let Some(window) = window else {
return;
};
- let workspace = cx.entity().clone();
+ let workspace = cx.entity();
InlineAssistant::update_global(cx, |inline_assistant, cx| {
inline_assistant.register_workspace(&workspace, window, cx)
});
@@ -144,7 +144,8 @@ impl InlineAssistant {
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return;
};
- let enabled = AgentSettings::get_global(cx).enabled;
+ let enabled = !DisableAiSettings::get_global(cx).disable_ai
+ && AgentSettings::get_global(cx).enabled;
terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.set_assistant_enabled(enabled, cx)
});
@@ -182,13 +183,13 @@ impl InlineAssistant {
match event {
workspace::Event::UserSavedItem { item, .. } => {
// When the user manually saves an editor, automatically accepts all finished transformations.
- if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx)) {
- if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
- for assist_id in editor_assists.assist_ids.clone() {
- let assist = &self.assists[&assist_id];
- if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) {
- self.finish_assist(assist_id, false, window, cx)
- }
+ if let Some(editor) = item.upgrade().and_then(|item| item.act_as::<Editor>(cx))
+ && let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade())
+ {
+ for assist_id in editor_assists.assist_ids.clone() {
+ let assist = &self.assists[&assist_id];
+ if let CodegenStatus::Done = assist.codegen.read(cx).status(cx) {
+ self.finish_assist(assist_id, false, window, cx)
}
}
}
@@ -342,13 +343,11 @@ impl InlineAssistant {
)
.await
.ok();
- if let Some(answer) = answer {
- if answer == 0 {
- cx.update(|window, cx| {
- window.dispatch_action(Box::new(OpenSettings), cx)
- })
+ if let Some(answer) = answer
+ && answer == 0
+ {
+ cx.update(|window, cx| window.dispatch_action(Box::new(OpenSettings), cx))
.ok();
- }
}
anyhow::Ok(())
})
@@ -435,11 +434,11 @@ impl InlineAssistant {
}
}
- if let Some(prev_selection) = selections.last_mut() {
- if selection.start <= prev_selection.end {
- prev_selection.end = selection.end;
- continue;
- }
+ if let Some(prev_selection) = selections.last_mut()
+ && selection.start <= prev_selection.end
+ {
+ prev_selection.end = selection.end;
+ continue;
}
let latest_selection = newest_selection.get_or_insert_with(|| selection.clone());
@@ -526,9 +525,9 @@ impl InlineAssistant {
if assist_to_focus.is_none() {
let focus_assist = if newest_selection.reversed {
- range.start.to_point(&snapshot) == newest_selection.start
+ range.start.to_point(snapshot) == newest_selection.start
} else {
- range.end.to_point(&snapshot) == newest_selection.end
+ range.end.to_point(snapshot) == newest_selection.end
};
if focus_assist {
assist_to_focus = Some(assist_id);
@@ -550,7 +549,7 @@ impl InlineAssistant {
let editor_assists = self
.assists_by_editor
.entry(editor.downgrade())
- .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx));
+ .or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
let mut assist_group = InlineAssistGroup::new();
for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists {
let codegen = prompt_editor.read(cx).codegen().clone();
@@ -649,7 +648,7 @@ impl InlineAssistant {
let editor_assists = self
.assists_by_editor
.entry(editor.downgrade())
- .or_insert_with(|| EditorInlineAssists::new(&editor, window, cx));
+ .or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
let mut assist_group = InlineAssistGroup::new();
self.assists.insert(
@@ -985,14 +984,13 @@ impl InlineAssistant {
EditorEvent::SelectionsChanged { .. } => {
for assist_id in editor_assists.assist_ids.clone() {
let assist = &self.assists[&assist_id];
- if let Some(decorations) = assist.decorations.as_ref() {
- if decorations
+ if let Some(decorations) = assist.decorations.as_ref()
+ && decorations
.prompt_editor
.focus_handle(cx)
.is_focused(window)
- {
- return;
- }
+ {
+ return;
}
}
@@ -1123,7 +1121,7 @@ impl InlineAssistant {
if editor_assists
.scroll_lock
.as_ref()
- .map_or(false, |lock| lock.assist_id == assist_id)
+ .is_some_and(|lock| lock.assist_id == assist_id)
{
editor_assists.scroll_lock = None;
}
@@ -1503,20 +1501,18 @@ impl InlineAssistant {
window: &mut Window,
cx: &mut App,
) -> Option<InlineAssistTarget> {
- if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
- if terminal_panel
+ if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx)
+ && terminal_panel
.read(cx)
.focus_handle(cx)
.contains_focused(window, cx)
- {
- if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
- pane.read(cx)
- .active_item()
- .and_then(|t| t.downcast::<TerminalView>())
- }) {
- return Some(InlineAssistTarget::Terminal(terminal_view));
- }
- }
+ && let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
+ pane.read(cx)
+ .active_item()
+ .and_then(|t| t.downcast::<TerminalView>())
+ })
+ {
+ return Some(InlineAssistTarget::Terminal(terminal_view));
}
let context_editor = agent_panel
@@ -1537,13 +1533,11 @@ impl InlineAssistant {
.and_then(|item| item.act_as::<Editor>(cx))
{
Some(InlineAssistTarget::Editor(workspace_editor))
- } else if let Some(terminal_view) = workspace
- .active_item(cx)
- .and_then(|item| item.act_as::<TerminalView>(cx))
- {
- Some(InlineAssistTarget::Terminal(terminal_view))
} else {
- None
+ workspace
+ .active_item(cx)
+ .and_then(|item| item.act_as::<TerminalView>(cx))
+ .map(InlineAssistTarget::Terminal)
}
}
}
@@ -1698,7 +1692,7 @@ impl InlineAssist {
}),
range,
codegen: codegen.clone(),
- workspace: workspace.clone(),
+ workspace,
_subscriptions: vec![
window.on_focus_in(&prompt_editor_focus_handle, cx, move |_, cx| {
InlineAssistant::update_global(cx, |this, cx| {
@@ -1741,22 +1735,20 @@ impl InlineAssist {
return;
};
- if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) {
- if assist.decorations.is_none() {
- if let Some(workspace) = assist.workspace.upgrade() {
- let error = format!("Inline assistant error: {}", error);
- workspace.update(cx, |workspace, cx| {
- struct InlineAssistantError;
-
- let id =
- NotificationId::composite::<InlineAssistantError>(
- assist_id.0,
- );
-
- workspace.show_toast(Toast::new(id, error), cx);
- })
- }
- }
+ if let CodegenStatus::Error(error) = codegen.read(cx).status(cx)
+ && assist.decorations.is_none()
+ && let Some(workspace) = assist.workspace.upgrade()
+ {
+ let error = format!("Inline assistant error: {}", error);
+ workspace.update(cx, |workspace, cx| {
+ struct InlineAssistantError;
+
+ let id = NotificationId::composite::<InlineAssistantError>(
+ assist_id.0,
+ );
+
+ workspace.show_toast(Toast::new(id, error), cx);
+ })
}
if assist.decorations.is_none() {
@@ -1821,18 +1813,15 @@ impl CodeActionProvider for AssistantCodeActionProvider {
has_diagnostics = true;
}
if has_diagnostics {
- if let Some(symbols_containing_start) = snapshot.symbols_containing(range.start, None) {
- if let Some(symbol) = symbols_containing_start.last() {
- range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
- range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
- }
+ let symbols_containing_start = snapshot.symbols_containing(range.start, None);
+ if let Some(symbol) = symbols_containing_start.last() {
+ range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
+ range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
}
-
- if let Some(symbols_containing_end) = snapshot.symbols_containing(range.end, None) {
- if let Some(symbol) = symbols_containing_end.last() {
- range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
- range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
- }
+ let symbols_containing_end = snapshot.symbols_containing(range.end, None);
+ if let Some(symbol) = symbols_containing_end.last() {
+ range.start = cmp::min(range.start, symbol.range.start.to_point(&snapshot));
+ range.end = cmp::max(range.end, symbol.range.end.to_point(&snapshot));
}
Task::ready(Ok(vec![CodeAction {
@@ -1,29 +1,18 @@
-use crate::agent_model_selector::AgentModelSelector;
-use crate::buffer_codegen::BufferCodegen;
-use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
-use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
-use crate::message_editor::{ContextCreasesAddon, extract_message_creases, insert_message_creases};
-use crate::terminal_codegen::TerminalCodegen;
-use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
-use crate::{RemoveAllContext, ToggleContextPicker};
use agent::{
context_store::ContextStore,
thread_store::{TextThreadStore, ThreadStore},
};
-use client::ErrorExt;
use collections::VecDeque;
-use db::kvp::Dismissable;
use editor::actions::Paste;
use editor::display_map::EditorMargins;
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
actions::{MoveDown, MoveUp},
};
-use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag};
use fs::Fs;
use gpui::{
- AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
- Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
+ AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable,
+ Subscription, TextStyle, WeakEntity, Window,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use parking_lot::Mutex;
@@ -33,12 +22,19 @@ use std::rc::Rc;
use std::sync::Arc;
use theme::ThemeSettings;
use ui::utils::WithRemSize;
-use ui::{
- CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
-};
+use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
use workspace::Workspace;
use zed_actions::agent::ToggleModelSelector;
+use crate::agent_model_selector::AgentModelSelector;
+use crate::buffer_codegen::BufferCodegen;
+use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
+use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
+use crate::message_editor::{ContextCreasesAddon, extract_message_creases, insert_message_creases};
+use crate::terminal_codegen::TerminalCodegen;
+use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
+use crate::{RemoveAllContext, ToggleContextPicker};
+
pub struct PromptEditor<T> {
pub editor: Entity<Editor>,
mode: PromptEditorMode,
@@ -75,7 +71,7 @@ impl<T: 'static> Render for PromptEditor<T> {
let codegen = codegen.read(cx);
if codegen.alternative_count(cx) > 1 {
- buttons.push(self.render_cycle_controls(&codegen, cx));
+ buttons.push(self.render_cycle_controls(codegen, cx));
}
let editor_margins = editor_margins.lock();
@@ -93,8 +89,8 @@ impl<T: 'static> Render for PromptEditor<T> {
};
let bottom_padding = match &self.mode {
- PromptEditorMode::Buffer { .. } => Pixels::from(0.),
- PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
+ PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
+ PromptEditorMode::Terminal { .. } => rems_from_px(8.0),
};
buttons.extend(self.render_buttons(window, cx));
@@ -144,47 +140,16 @@ impl<T: 'static> Render for PromptEditor<T> {
};
let error_message = SharedString::from(error.to_string());
- if error.error_code() == proto::ErrorCode::RateLimitExceeded
- && cx.has_flag::<ZedProFeatureFlag>()
- {
- el.child(
- v_flex()
- .child(
- IconButton::new(
- "rate-limit-error",
- IconName::XCircle,
- )
- .toggle_state(self.show_rate_limit_notice)
- .shape(IconButtonShape::Square)
- .icon_size(IconSize::Small)
- .on_click(
- cx.listener(Self::toggle_rate_limit_notice),
- ),
- )
- .children(self.show_rate_limit_notice.then(|| {
- deferred(
- anchored()
- .position_mode(
- gpui::AnchoredPositionMode::Local,
- )
- .position(point(px(0.), px(24.)))
- .anchor(gpui::Corner::TopLeft)
- .child(self.render_rate_limit_notice(cx)),
- )
- })),
- )
- } else {
- el.child(
- div()
- .id("error")
- .tooltip(Tooltip::text(error_message))
- .child(
- Icon::new(IconName::XCircle)
- .size(IconSize::Small)
- .color(Color::Error),
- ),
- )
- }
+ el.child(
+ div()
+ .id("error")
+ .tooltip(Tooltip::text(error_message))
+ .child(
+ Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error),
+ ),
+ )
}),
)
.child(
@@ -264,7 +229,7 @@ impl<T: 'static> PromptEditor<T> {
self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
- editor.set_placeholder_text("Add a prompt…", cx);
+ editor.set_placeholder_text("Add a prompt…", window, cx);
editor.set_text(prompt, window, cx);
insert_message_creases(
&mut editor,
@@ -310,19 +275,6 @@ impl<T: 'static> PromptEditor<T> {
crate::active_thread::attach_pasted_images_as_context(&self.context_store, cx);
}
- fn toggle_rate_limit_notice(
- &mut self,
- _: &ClickEvent,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- self.show_rate_limit_notice = !self.show_rate_limit_notice;
- if self.show_rate_limit_notice {
- window.focus(&self.editor.focus_handle(cx));
- }
- cx.notify();
- }
-
fn handle_prompt_editor_events(
&mut self,
_: &Entity<Editor>,
@@ -334,7 +286,7 @@ impl<T: 'static> PromptEditor<T> {
EditorEvent::Edited { .. } => {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
- let is_via_ssh = workspace.project().read(cx).is_via_ssh();
+ let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
workspace
.client()
@@ -345,7 +297,7 @@ impl<T: 'static> PromptEditor<T> {
let prompt = self.editor.read(cx).text(cx);
if self
.prompt_history_ix
- .map_or(true, |ix| self.prompt_history[ix] != prompt)
+ .is_none_or(|ix| self.prompt_history[ix] != prompt)
{
self.prompt_history_ix.take();
self.pending_prompt = prompt;
@@ -707,75 +659,22 @@ impl<T: 'static> PromptEditor<T> {
.into_any_element()
}
- fn render_rate_limit_notice(&self, cx: &mut Context<Self>) -> impl IntoElement {
- Popover::new().child(
- v_flex()
- .occlude()
- .p_2()
- .child(
- Label::new("Out of Tokens")
- .size(LabelSize::Small)
- .weight(FontWeight::BOLD),
- )
- .child(Label::new(
- "Try Zed Pro for higher limits, a wider range of models, and more.",
- ))
- .child(
- h_flex()
- .justify_between()
- .child(CheckboxWithLabel::new(
- "dont-show-again",
- Label::new("Don't show again"),
- if RateLimitNotice::dismissed() {
- ui::ToggleState::Selected
- } else {
- ui::ToggleState::Unselected
- },
- |selection, _, cx| {
- let is_dismissed = match selection {
- ui::ToggleState::Unselected => false,
- ui::ToggleState::Indeterminate => return,
- ui::ToggleState::Selected => true,
- };
-
- RateLimitNotice::set_dismissed(is_dismissed, cx);
- },
- ))
- .child(
- h_flex()
- .gap_2()
- .child(
- Button::new("dismiss", "Dismiss")
- .style(ButtonStyle::Transparent)
- .on_click(cx.listener(Self::toggle_rate_limit_notice)),
- )
- .child(Button::new("more-info", "More Info").on_click(
- |_event, window, cx| {
- window.dispatch_action(
- Box::new(zed_actions::OpenAccountSettings),
- cx,
- )
- },
- )),
- ),
- ),
- )
- }
-
- fn render_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
- let font_size = TextSize::Default.rems(cx);
- let line_height = font_size.to_pixels(window.rem_size()) * 1.3;
+ fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
+ let colors = cx.theme().colors();
div()
.key_context("InlineAssistEditor")
.size_full()
.p_2()
.pl_1()
- .bg(cx.theme().colors().editor_background)
+ .bg(colors.editor_background)
.child({
let settings = ThemeSettings::get_global(cx);
+ let font_size = settings.buffer_font_size(cx);
+ let line_height = font_size * 1.2;
+
let text_style = TextStyle {
- color: cx.theme().colors().editor_foreground,
+ color: colors.editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
@@ -786,7 +685,7 @@ impl<T: 'static> PromptEditor<T> {
EditorElement::new(
&self.editor,
EditorStyle {
- background: cx.theme().colors().editor_background,
+ background: colors.editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
@@ -883,7 +782,7 @@ impl PromptEditor<BufferCodegen> {
// always show the cursor (even when it isn't focused) because
// typing in one will make what you typed appear in all of them.
editor.set_show_cursor_when_unfocused(true, cx);
- editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
+ editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
editor.register_addon(ContextCreasesAddon::new());
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
@@ -976,15 +875,7 @@ impl PromptEditor<BufferCodegen> {
self.editor
.update(cx, |editor, _| editor.set_read_only(false));
}
- CodegenStatus::Error(error) => {
- if cx.has_flag::<ZedProFeatureFlag>()
- && error.error_code() == proto::ErrorCode::RateLimitExceeded
- && !RateLimitNotice::dismissed()
- {
- self.show_rate_limit_notice = true;
- cx.notify();
- }
-
+ CodegenStatus::Error(_error) => {
self.edited_since_done = false;
self.editor
.update(cx, |editor, _| editor.set_read_only(false));
@@ -1058,7 +949,7 @@ impl PromptEditor<TerminalCodegen> {
cx,
);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
- editor.set_placeholder_text(Self::placeholder_text(&mode, window, cx), cx);
+ editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
@@ -1187,12 +1078,6 @@ impl PromptEditor<TerminalCodegen> {
}
}
-struct RateLimitNotice;
-
-impl Dismissable for RateLimitNotice {
- const KEY: &'static str = "dismissed-rate-limit-notice";
-}
-
pub enum CodegenStatus {
Idle,
Pending,
@@ -1229,27 +1114,27 @@ pub enum GenerationMode {
impl GenerationMode {
fn start_label(self) -> &'static str {
match self {
- GenerationMode::Generate { .. } => "Generate",
+ GenerationMode::Generate => "Generate",
GenerationMode::Transform => "Transform",
}
}
fn tooltip_interrupt(self) -> &'static str {
match self {
- GenerationMode::Generate { .. } => "Interrupt Generation",
+ GenerationMode::Generate => "Interrupt Generation",
GenerationMode::Transform => "Interrupt Transform",
}
}
fn tooltip_restart(self) -> &'static str {
match self {
- GenerationMode::Generate { .. } => "Restart Generation",
+ GenerationMode::Generate => "Restart Generation",
GenerationMode::Transform => "Restart Transform",
}
}
fn tooltip_accept(self) -> &'static str {
match self {
- GenerationMode::Generate { .. } => "Accept Generation",
+ GenerationMode::Generate => "Accept Generation",
GenerationMode::Transform => "Accept Transform",
}
}
@@ -1,7 +1,6 @@
use std::{cmp::Reverse, sync::Arc};
use collections::{HashSet, IndexMap};
-use feature_flags::ZedProFeatureFlag;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
use language_model::{
@@ -10,11 +9,8 @@ use language_model::{
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
-use proto::Plan;
use ui::{ListItem, ListItemSpacing, prelude::*};
-const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
-
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
@@ -93,7 +89,7 @@ impl LanguageModelPickerDelegate {
let entries = models.entries();
Self {
- on_model_changed: on_model_changed.clone(),
+ on_model_changed,
all_models: Arc::new(models),
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
filtered_entries: entries,
@@ -104,7 +100,7 @@ impl LanguageModelPickerDelegate {
window,
|picker, _, event, window, cx| {
match event {
- language_model::Event::ProviderStateChanged
+ language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
let query = picker.query(cx);
@@ -296,7 +292,7 @@ impl ModelMatcher {
pub fn fuzzy_search(&self, query: &str) -> Vec<ModelInfo> {
let mut matches = self.bg_executor.block(match_strings(
&self.candidates,
- &query,
+ query,
false,
true,
100,
@@ -514,7 +510,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.pl_0p5()
.gap_1p5()
.w(px(240.))
- .child(Label::new(model_info.model.name().0.clone()).truncate()),
+ .child(Label::new(model_info.model.name().0).truncate()),
)
.end_slot(div().pr_3().when(is_selected, |this| {
this.child(
@@ -531,13 +527,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
fn render_footer(
&self,
- _: &mut Window,
+ _window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<gpui::AnyElement> {
- use feature_flags::FeatureFlagAppExt;
-
- let plan = proto::Plan::ZedPro;
-
Some(
h_flex()
.w_full()
@@ -546,28 +538,6 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.p_1()
.gap_4()
.justify_between()
- .when(cx.has_flag::<ZedProFeatureFlag>(), |this| {
- this.child(match plan {
- Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
- .icon(IconName::ZedAssistant)
- .icon_size(IconSize::Small)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .on_click(|_, window, cx| {
- window
- .dispatch_action(Box::new(zed_actions::OpenAccountSettings), cx)
- }),
- Plan::Free | Plan::ZedProTrial => Button::new(
- "try-pro",
- if plan == Plan::ZedProTrial {
- "Upgrade to Pro"
- } else {
- "Try Pro"
- },
- )
- .on_click(|_, _, cx| cx.open_url(TRY_ZED_PRO_URL)),
- })
- })
.child(
Button::new("configure", "Configure")
.icon(IconName::Settings)
@@ -6,7 +6,7 @@ use crate::agent_diff::AgentDiffThread;
use crate::agent_model_selector::AgentModelSelector;
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
use crate::ui::{
- MaxModeTooltip,
+ BurnModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use agent::history_store::HistoryStore;
@@ -14,10 +14,10 @@ use agent::{
context::{AgentContextKey, ContextLoadResult, load_context},
context_store::ContextStoreEvent,
};
-use agent_settings::{AgentSettings, CompletionMode};
+use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
use ai_onboarding::ApiKeysWithProviders;
use buffer_diff::BufferDiff;
-use cloud_llm_client::CompletionIntent;
+use cloud_llm_client::{CompletionIntent, PlanV1};
use collections::{HashMap, HashSet};
use editor::actions::{MoveUp, Paste};
use editor::display_map::CreaseId;
@@ -55,7 +55,7 @@ use zed_actions::agent::ToggleModelSelector;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
-use crate::profile_selector::ProfileSelector;
+use crate::profile_selector::{ProfileProvider, ProfileSelector};
use crate::{
ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
@@ -117,14 +117,15 @@ pub(crate) fn create_editor(
let mut editor = Editor::new(
editor::EditorMode::AutoHeight {
min_lines,
- max_lines: max_lines,
+ max_lines,
},
buffer,
None,
window,
cx,
);
- editor.set_placeholder_text("Message the agent – @ to include context", cx);
+ editor.set_placeholder_text("Message the agent – @ to include context", window, cx);
+ editor.disable_word_completions();
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap();
editor.set_use_modal_editing(true);
@@ -152,6 +153,24 @@ pub(crate) fn create_editor(
editor
}
+impl ProfileProvider for Entity<Thread> {
+ fn profiles_supported(&self, cx: &App) -> bool {
+ self.read(cx)
+ .configured_model()
+ .is_some_and(|model| model.model.supports_tools())
+ }
+
+ fn profile_id(&self, cx: &App) -> AgentProfileId {
+ self.read(cx).profile().id().clone()
+ }
+
+ fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
+ self.update(cx, |this, cx| {
+ this.set_profile(profile_id, cx);
+ });
+ }
+}
+
impl MessageEditor {
pub fn new(
fs: Arc<dyn Fs>,
@@ -197,9 +216,10 @@ impl MessageEditor {
let subscriptions = vec![
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
- cx.subscribe(&editor, |this, _, event, cx| match event {
- EditorEvent::BufferEdited => this.handle_message_changed(cx),
- _ => {}
+ cx.subscribe(&editor, |this, _, event: &EditorEvent, cx| {
+ if event == &EditorEvent::BufferEdited {
+ this.handle_message_changed(cx)
+ }
}),
cx.observe(&context_store, |this, _, cx| {
// When context changes, reload it for token counting.
@@ -221,14 +241,15 @@ impl MessageEditor {
)
});
- let profile_selector =
- cx.new(|cx| ProfileSelector::new(fs, thread.clone(), editor.focus_handle(cx), cx));
+ let profile_selector = cx.new(|cx| {
+ ProfileSelector::new(fs, Arc::new(thread.clone()), editor.focus_handle(cx), cx)
+ });
Self {
editor: editor.clone(),
project: thread.read(cx).project().clone(),
thread,
- incompatible_tools_state: incompatible_tools.clone(),
+ incompatible_tools_state: incompatible_tools,
workspace,
context_store,
prompt_store,
@@ -358,18 +379,13 @@ impl MessageEditor {
}
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let Some(ConfiguredModel { model, provider }) = self
+ let Some(ConfiguredModel { model, .. }) = self
.thread
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
else {
return;
};
- if provider.must_accept_terms(cx) {
- cx.notify();
- return;
- }
-
let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| {
let creases = extract_message_creases(editor, cx);
let text = editor.text(cx);
@@ -422,11 +438,11 @@ impl MessageEditor {
thread.cancel_editing(cx);
});
- let cancelled = self.thread.update(cx, |thread, cx| {
+ let canceled = self.thread.update(cx, |thread, cx| {
thread.cancel_last_completion(Some(window.window_handle()), cx)
});
- if cancelled {
+ if canceled {
self.set_editor_is_expanded(false, cx);
self.send_to_model(window, cx);
}
@@ -605,7 +621,7 @@ impl MessageEditor {
this.toggle_burn_mode(&ToggleBurnMode, window, cx);
}))
.tooltip(move |_window, cx| {
- cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled))
+ cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
.into()
})
.into_any_element(),
@@ -671,11 +687,7 @@ impl MessageEditor {
.as_ref()
.map(|model| {
self.incompatible_tools_state.update(cx, |state, cx| {
- state
- .incompatible_tools(&model.model, cx)
- .iter()
- .cloned()
- .collect::<Vec<_>>()
+ state.incompatible_tools(&model.model, cx).to_vec()
})
})
.unwrap_or_default();
@@ -823,7 +835,6 @@ impl MessageEditor {
.child(self.profile_selector.clone())
.child(self.model_selector.clone())
.map({
- let focus_handle = focus_handle.clone();
move |parent| {
if is_generating {
parent
@@ -1117,7 +1128,7 @@ impl MessageEditor {
)
.when(is_edit_changes_expanded, |parent| {
parent.child(
- v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
+ v_flex().children(changed_buffers.iter().enumerate().flat_map(
|(index, (buffer, _diff))| {
let file = buffer.read(cx).file()?;
let path = file.path();
@@ -1147,7 +1158,7 @@ impl MessageEditor {
.buffer_font(cx)
});
- let file_icon = FileIcons::get_icon(&path, cx)
+ let file_icon = FileIcons::get_icon(path, cx)
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
.unwrap_or_else(|| {
@@ -1274,7 +1285,7 @@ impl MessageEditor {
self.thread
.read(cx)
.configured_model()
- .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
+ .is_some_and(|model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
}
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
@@ -1287,7 +1298,9 @@ impl MessageEditor {
return None;
}
- let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree);
+ let plan = user_store
+ .plan()
+ .unwrap_or(cloud_llm_client::Plan::V1(PlanV1::ZedFree));
let usage = user_store.model_request_usage()?;
@@ -1304,14 +1317,10 @@ impl MessageEditor {
token_usage_ratio: TokenUsageRatio,
cx: &mut Context<Self>,
) -> Option<Div> {
- let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
- Icon::new(IconName::Close)
- .color(Color::Error)
- .size(IconSize::XSmall)
+ let (icon, severity) = if token_usage_ratio == TokenUsageRatio::Exceeded {
+ (IconName::Close, Severity::Error)
} else {
- Icon::new(IconName::Warning)
- .color(Color::Warning)
- .size(IconSize::XSmall)
+ (IconName::Warning, Severity::Warning)
};
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
@@ -1326,29 +1335,33 @@ impl MessageEditor {
"To continue, start a new thread from a summary."
};
- let mut callout = Callout::new()
+ let callout = Callout::new()
.line_height(line_height)
+ .severity(severity)
.icon(icon)
.title(title)
.description(description)
- .primary_action(
- Button::new("start-new-thread", "Start New Thread")
- .label_size(LabelSize::Small)
- .on_click(cx.listener(|this, _, window, cx| {
- let from_thread_id = Some(this.thread.read(cx).id().clone());
- window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
- })),
- );
-
- if self.is_using_zed_provider(cx) {
- callout = callout.secondary_action(
- IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(|this, _event, window, cx| {
- this.toggle_burn_mode(&ToggleBurnMode, window, cx);
- })),
+ .actions_slot(
+ h_flex()
+ .gap_0p5()
+ .when(self.is_using_zed_provider(cx), |this| {
+ this.child(
+ IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
+ .icon_size(IconSize::XSmall)
+ .on_click(cx.listener(|this, _event, window, cx| {
+ this.toggle_burn_mode(&ToggleBurnMode, window, cx);
+ })),
+ )
+ })
+ .child(
+ Button::new("start-new-thread", "Start New Thread")
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener(|this, _, window, cx| {
+ let from_thread_id = Some(this.thread.read(cx).id().clone());
+ window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
+ })),
+ ),
);
- }
Some(
div()
@@ -1385,7 +1398,7 @@ impl MessageEditor {
})
.ok();
});
- // Replace existing load task, if any, causing it to be cancelled.
+ // Replace existing load task, if any, causing it to be canceled.
let load_task = load_task.shared();
self.load_context_task = Some(load_task.clone());
cx.spawn(async move |this, cx| {
@@ -1427,7 +1440,7 @@ impl MessageEditor {
let message_text = editor.read(cx).text(cx);
if message_text.is_empty()
- && loaded_context.map_or(true, |loaded_context| loaded_context.is_empty())
+ && loaded_context.is_none_or(|loaded_context| loaded_context.is_empty())
{
return None;
}
@@ -1540,9 +1553,8 @@ impl ContextCreasesAddon {
cx: &mut Context<Editor>,
) {
self.creases.entry(key).or_default().extend(creases);
- self._subscription = Some(cx.subscribe(
- &context_store,
- |editor, _, event, cx| match event {
+ self._subscription = Some(
+ cx.subscribe(context_store, |editor, _, event, cx| match event {
ContextStoreEvent::ContextRemoved(key) => {
let Some(this) = editor.addon_mut::<Self>() else {
return;
@@ -1562,8 +1574,8 @@ impl ContextCreasesAddon {
editor.edit(ranges.into_iter().zip(replacement_texts), cx);
cx.notify();
}
- },
- ))
+ }),
+ )
}
pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
@@ -1591,7 +1603,8 @@ pub fn extract_message_creases(
.collect::<HashMap<_, _>>();
// Filter the addon's list of creases based on what the editor reports,
// since the addon might have removed creases in it.
- let creases = editor.display_map.update(cx, |display_map, cx| {
+
+ editor.display_map.update(cx, |display_map, cx| {
display_map
.snapshot(cx)
.crease_snapshot
@@ -1615,8 +1628,7 @@ pub fn extract_message_creases(
}
})
.collect()
- });
- creases
+ })
}
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
@@ -1668,7 +1680,7 @@ impl Render for MessageEditor {
let has_history = self
.history_store
.as_ref()
- .and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok())
+ .and_then(|hs| hs.update(cx, |hs, cx| !hs.entries(cx).is_empty()).ok())
.unwrap_or(false)
|| self
.thread
@@ -1681,7 +1693,7 @@ impl Render for MessageEditor {
!has_history && is_signed_out && has_configured_providers,
|this| this.child(cx.new(ApiKeysWithProviders::new)),
)
- .when(changed_buffers.len() > 0, |parent| {
+ .when(!changed_buffers.is_empty(), |parent| {
parent.child(self.render_edits_bar(&changed_buffers, window, cx))
})
.child(self.render_editor(window, cx))
@@ -1786,7 +1798,7 @@ impl AgentPreview for MessageEditor {
.bg(cx.theme().colors().panel_background)
.border_1()
.border_color(cx.theme().colors().border)
- .child(default_message_editor.clone())
+ .child(default_message_editor)
.into_any_element(),
)])
.into_any_element(),
@@ -1,23 +1,31 @@
use crate::{ManageProfiles, ToggleProfileSelector};
-use agent::{
- Thread,
- agent_profile::{AgentProfile, AvailableProfiles},
-};
+use agent::agent_profile::{AgentProfile, AvailableProfiles};
use agent_settings::{AgentDockPosition, AgentProfileId, AgentSettings, builtin_profiles};
use fs::Fs;
-use gpui::{Action, Empty, Entity, FocusHandle, Subscription, prelude::*};
-use language_model::LanguageModelRegistry;
+use gpui::{Action, Entity, FocusHandle, Subscription, prelude::*};
use settings::{Settings as _, SettingsStore, update_settings_file};
use std::sync::Arc;
use ui::{
- ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, Tooltip,
- prelude::*,
+ ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, TintColor,
+ Tooltip, prelude::*,
};
+/// Trait for types that can provide and manage agent profiles
+pub trait ProfileProvider {
+ /// Get the current profile ID
+ fn profile_id(&self, cx: &App) -> AgentProfileId;
+
+ /// Set the profile ID
+ fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App);
+
+ /// Check if profiles are supported in the current context (e.g. if the model that is selected has tool support)
+ fn profiles_supported(&self, cx: &App) -> bool;
+}
+
pub struct ProfileSelector {
profiles: AvailableProfiles,
fs: Arc<dyn Fs>,
- thread: Entity<Thread>,
+ provider: Arc<dyn ProfileProvider>,
menu_handle: PopoverMenuHandle<ContextMenu>,
focus_handle: FocusHandle,
_subscriptions: Vec<Subscription>,
@@ -26,7 +34,7 @@ pub struct ProfileSelector {
impl ProfileSelector {
pub fn new(
fs: Arc<dyn Fs>,
- thread: Entity<Thread>,
+ provider: Arc<dyn ProfileProvider>,
focus_handle: FocusHandle,
cx: &mut Context<Self>,
) -> Self {
@@ -37,7 +45,7 @@ impl ProfileSelector {
Self {
profiles: AgentProfile::available_profiles(cx),
fs,
- thread,
+ provider,
menu_handle: PopoverMenuHandle::default(),
focus_handle,
_subscriptions: vec![settings_subscription],
@@ -113,10 +121,10 @@ impl ProfileSelector {
builtin_profiles::MINIMAL => Some("Chat about anything with no tools."),
_ => None,
};
- let thread_profile_id = self.thread.read(cx).profile().id();
+ let thread_profile_id = self.provider.profile_id(cx);
let entry = ContextMenuEntry::new(profile_name.clone())
- .toggleable(IconPosition::End, &profile_id == thread_profile_id);
+ .toggleable(IconPosition::End, profile_id == thread_profile_id);
let entry = if let Some(doc_text) = documentation {
entry.documentation_aside(documentation_side(settings.dock), move |_| {
@@ -128,19 +136,16 @@ impl ProfileSelector {
entry.handler({
let fs = self.fs.clone();
- let thread = self.thread.clone();
- let profile_id = profile_id.clone();
+ let provider = self.provider.clone();
move |_window, cx| {
update_settings_file::<AgentSettings>(fs.clone(), cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
- settings.set_profile(profile_id.clone());
+ settings.set_profile(profile_id);
}
});
- thread.update(cx, |this, cx| {
- this.set_profile(profile_id.clone(), cx);
- });
+ provider.set_profile(profile_id.clone(), cx);
}
})
}
@@ -149,23 +154,15 @@ impl ProfileSelector {
impl Render for ProfileSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AgentSettings::get_global(cx);
- let profile_id = self.thread.read(cx).profile().id();
- let profile = settings.profiles.get(profile_id);
+ let profile_id = self.provider.profile_id(cx);
+ let profile = settings.profiles.get(&profile_id);
let selected_profile = profile
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
- let configured_model = self.thread.read(cx).configured_model().or_else(|| {
- let model_registry = LanguageModelRegistry::read_global(cx);
- model_registry.default_model()
- });
- let Some(configured_model) = configured_model else {
- return Empty.into_any_element();
- };
-
- if configured_model.model.supports_tools() {
- let this = cx.entity().clone();
+ if self.provider.profiles_supported(cx) {
+ let this = cx.entity();
let focus_handle = self.focus_handle.clone();
let trigger_button = Button::new("profile-selector-model", selected_profile)
.label_size(LabelSize::Small)
@@ -173,11 +170,11 @@ impl Render for ProfileSelector {
.icon(IconName::ChevronDown)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
- .icon_color(Color::Muted);
+ .icon_color(Color::Muted)
+ .selected_style(ButtonStyle::Tinted(TintColor::Accent));
PopoverMenu::new("profile-selector")
.trigger_with_tooltip(trigger_button, {
- let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Toggle Profile Menu",
@@ -199,6 +196,10 @@ impl Render for ProfileSelector {
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
+ .offset(gpui::Point {
+ x: px(0.0),
+ y: px(-2.0),
+ })
.into_any_element()
} else {
Button::new("tools-not-supported-button", "Tools Unsupported")
@@ -7,7 +7,10 @@ use fuzzy::{StringMatchCandidate, match_strings};
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
use language::{Anchor, Buffer, ToPoint};
use parking_lot::Mutex;
-use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
+use project::{
+ CompletionDisplayOptions, CompletionIntent, CompletionSource,
+ lsp_store::CompletionDocumentation,
+};
use rope::Point;
use std::{
ops::Range,
@@ -88,8 +91,6 @@ impl SlashCommandCompletionProvider {
.map(|(editor, workspace)| {
let command_name = mat.string.clone();
let command_range = command_range.clone();
- let editor = editor.clone();
- let workspace = workspace.clone();
Arc::new(
move |intent: CompletionIntent,
window: &mut Window,
@@ -135,6 +136,7 @@ impl SlashCommandCompletionProvider {
vec![project::CompletionResponse {
completions,
+ display_options: CompletionDisplayOptions::default(),
is_incomplete: false,
}]
})
@@ -158,7 +160,7 @@ impl SlashCommandCompletionProvider {
if let Some(command) = self.slash_commands.command(command_name, cx) {
let completions = command.complete_argument(
arguments,
- new_cancel_flag.clone(),
+ new_cancel_flag,
self.workspace.clone(),
window,
cx,
@@ -239,6 +241,7 @@ impl SlashCommandCompletionProvider {
Ok(vec![project::CompletionResponse {
completions,
+ display_options: CompletionDisplayOptions::default(),
// TODO: Could have slash commands indicate whether their completions are incomplete.
is_incomplete: true,
}])
@@ -246,6 +249,7 @@ impl SlashCommandCompletionProvider {
} else {
Task::ready(Ok(vec![project::CompletionResponse {
completions: Vec::new(),
+ display_options: CompletionDisplayOptions::default(),
is_incomplete: true,
}]))
}
@@ -307,6 +311,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
else {
return Task::ready(Ok(vec![project::CompletionResponse {
completions: Vec::new(),
+ display_options: CompletionDisplayOptions::default(),
is_incomplete: false,
}]));
};
@@ -140,12 +140,10 @@ impl PickerDelegate for SlashCommandDelegate {
);
ret.push(index - 1);
}
- } else {
- if let SlashCommandEntry::Advert { .. } = command {
- previous_is_advert = true;
- if index != 0 {
- ret.push(index - 1);
- }
+ } else if let SlashCommandEntry::Advert { .. } = command {
+ previous_is_advert = true;
+ if index != 0 {
+ ret.push(index - 1);
}
}
}
@@ -214,7 +212,7 @@ impl PickerDelegate for SlashCommandDelegate {
let mut label = format!("{}", info.name);
if let Some(args) = info.args.as_ref().filter(|_| selected)
{
- label.push_str(&args);
+ label.push_str(args);
}
Label::new(label)
.single_line()
@@ -329,9 +327,7 @@ where
};
let picker_view = cx.new(|cx| {
- let picker =
- Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into()));
- picker
+ Picker::uniform_list(delegate, window, cx).max_height(Some(rems(20.).into()))
});
let handle = self
@@ -2,27 +2,17 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
/// Settings for slash commands.
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(key = "slash_commands")]
pub struct SlashCommandSettings {
- /// Settings for the `/docs` slash command.
- #[serde(default)]
- pub docs: DocsCommandSettings,
/// Settings for the `/cargo-workspace` slash command.
#[serde(default)]
pub cargo_workspace: CargoWorkspaceCommandSettings,
}
-/// Settings for the `/docs` slash command.
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
-pub struct DocsCommandSettings {
- /// Whether `/docs` is enabled.
- #[serde(default)]
- pub enabled: bool,
-}
-
/// Settings for the `/cargo-workspace` slash command.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
pub struct CargoWorkspaceCommandSettings {
@@ -32,8 +22,6 @@ pub struct CargoWorkspaceCommandSettings {
}
impl Settings for SlashCommandSettings {
- const KEY: Option<&'static str> = Some("slash_commands");
-
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
@@ -48,7 +48,7 @@ impl TerminalCodegen {
let prompt = prompt_task.await;
let model_telemetry_id = model.telemetry_id();
let model_provider_id = model.provider_id();
- let response = model.stream_completion_text(prompt, &cx).await;
+ let response = model.stream_completion_text(prompt, cx).await;
let generate = async {
let message_id = response
.as_ref()
@@ -388,20 +388,20 @@ impl TerminalInlineAssistant {
window: &mut Window,
cx: &mut App,
) {
- if let Some(assist) = self.assists.get_mut(&assist_id) {
- if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
- assist
- .terminal
- .update(cx, |terminal, cx| {
- terminal.clear_block_below_cursor(cx);
- let block = terminal_view::BlockProperties {
- height,
- render: Box::new(move |_| prompt_editor.clone().into_any_element()),
- };
- terminal.set_block_below_cursor(block, window, cx);
- })
- .log_err();
- }
+ if let Some(assist) = self.assists.get_mut(&assist_id)
+ && let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned()
+ {
+ assist
+ .terminal
+ .update(cx, |terminal, cx| {
+ terminal.clear_block_below_cursor(cx);
+ let block = terminal_view::BlockProperties {
+ height,
+ render: Box::new(move |_| prompt_editor.clone().into_any_element()),
+ };
+ terminal.set_block_below_cursor(block, window, cx);
+ })
+ .log_err();
}
}
}
@@ -432,7 +432,7 @@ impl TerminalInlineAssist {
terminal: terminal.downgrade(),
prompt_editor: Some(prompt_editor.clone()),
codegen: codegen.clone(),
- workspace: workspace.clone(),
+ workspace,
context_store,
prompt_store,
_subscriptions: vec![
@@ -450,23 +450,20 @@ impl TerminalInlineAssist {
return;
};
- if let CodegenStatus::Error(error) = &codegen.read(cx).status {
- if assist.prompt_editor.is_none() {
- if let Some(workspace) = assist.workspace.upgrade() {
- let error =
- format!("Terminal inline assistant error: {}", error);
- workspace.update(cx, |workspace, cx| {
- struct InlineAssistantError;
-
- let id =
- NotificationId::composite::<InlineAssistantError>(
- assist_id.0,
- );
-
- workspace.show_toast(Toast::new(id, error), cx);
- })
- }
- }
+ if let CodegenStatus::Error(error) = &codegen.read(cx).status
+ && assist.prompt_editor.is_none()
+ && let Some(workspace) = assist.workspace.upgrade()
+ {
+ let error = format!("Terminal inline assistant error: {}", error);
+ workspace.update(cx, |workspace, cx| {
+ struct InlineAssistantError;
+
+ let id = NotificationId::composite::<InlineAssistantError>(
+ assist_id.0,
+ );
+
+ workspace.show_toast(Toast::new(id, error), cx);
+ })
}
if assist.prompt_editor.is_none() {
@@ -1,14 +1,12 @@
use crate::{
- burn_mode_tooltip::BurnModeTooltip,
+ QuoteSelection,
language_model_selector::{LanguageModelSelector, language_model_selector},
+ ui::BurnModeTooltip,
};
use agent_settings::{AgentSettings, CompletionMode};
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
-use assistant_slash_commands::{
- DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs, FileSlashCommand,
- selections_creases,
-};
+use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
use client::{proto, zed_urls};
use collections::{BTreeSet, HashMap, HashSet, hash_map};
use editor::{
@@ -27,10 +25,9 @@ use gpui::{
Action, Animation, AnimationExt, AnyElement, AnyView, App, ClipboardEntry, ClipboardItem,
Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement,
IntoElement, ParentElement, Pixels, Render, RenderImage, SharedString, Size,
- StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions,
- div, img, percentage, point, prelude::*, pulsating_between, size,
+ StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, actions, div, img, point,
+ prelude::*, pulsating_between, size,
};
-use indexed_docs::IndexedDocsStore;
use language::{
BufferSnapshot, LspAdapterDelegate, ToOffset,
language_settings::{SoftWrap, all_language_settings},
@@ -56,8 +53,8 @@ use std::{
};
use text::SelectionGoal;
use ui::{
- ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
- prelude::*,
+ ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle,
+ TintColor, Tooltip, prelude::*,
};
use util::{ResultExt, maybe};
use workspace::{
@@ -77,7 +74,7 @@ use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker}
use assistant_context::{
AssistantContext, CacheStatus, Content, ContextEvent, ContextId, InvokedSlashCommandId,
InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, MessageStatus,
- ParsedSlashCommand, PendingSlashCommandStatus, ThoughtProcessOutputSection,
+ PendingSlashCommandStatus, ThoughtProcessOutputSection,
};
actions!(
@@ -93,8 +90,6 @@ actions!(
CycleMessageRole,
/// Inserts the selected text into the active editor.
InsertIntoEditor,
- /// Quotes the current selection in the assistant conversation.
- QuoteSelection,
/// Splits the conversation at the current cursor position.
Split,
]
@@ -195,7 +190,6 @@ pub struct TextThreadEditor {
invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
_subscriptions: Vec<Subscription>,
last_error: Option<AssistError>,
- show_accept_terms: bool,
pub(crate) slash_menu_handle:
PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
// dragged_file_worktrees is used to keep references to worktrees that were added
@@ -294,7 +288,6 @@ impl TextThreadEditor {
invoked_slash_command_creases: HashMap::default(),
_subscriptions,
last_error: None,
- show_accept_terms: false,
slash_menu_handle: Default::default(),
dragged_file_worktrees: Vec::new(),
language_model_selector: cx.new(|cx| {
@@ -368,24 +361,12 @@ impl TextThreadEditor {
if self.sending_disabled(cx) {
return;
}
+ telemetry::event!("Agent Message Sent", agent = "zed-text");
self.send_to_model(window, cx);
}
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let provider = LanguageModelRegistry::read_global(cx)
- .default_model()
- .map(|default| default.provider);
- if provider
- .as_ref()
- .map_or(false, |provider| provider.must_accept_terms(cx))
- {
- self.show_accept_terms = true;
- cx.notify();
- return;
- }
-
self.last_error = None;
-
if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
let new_selection = {
let cursor = user_message
@@ -461,7 +442,7 @@ impl TextThreadEditor {
|| snapshot
.chars_at(newest_cursor)
.next()
- .map_or(false, |ch| ch != '\n')
+ .is_some_and(|ch| ch != '\n')
{
editor.move_to_end_of_line(
&MoveToEndOfLine {
@@ -544,7 +525,7 @@ impl TextThreadEditor {
let context = self.context.read(cx);
let sections = context
.slash_command_output_sections()
- .into_iter()
+ .iter()
.filter(|section| section.is_valid(context.buffer().read(cx)))
.cloned()
.collect::<Vec<_>>();
@@ -701,19 +682,7 @@ impl TextThreadEditor {
}
};
let render_trailer = {
- let command = command.clone();
- move |row, _unfold, _window: &mut Window, cx: &mut App| {
- // TODO: In the future we should investigate how we can expose
- // this as a hook on the `SlashCommand` trait so that we don't
- // need to special-case it here.
- if command.name == DocsSlashCommand::NAME {
- return render_docs_slash_command_trailer(
- row,
- command.clone(),
- cx,
- );
- }
-
+ move |_row, _unfold, _window: &mut Window, _cx: &mut App| {
Empty.into_any()
}
};
@@ -761,32 +730,27 @@ impl TextThreadEditor {
) {
if let Some(invoked_slash_command) =
self.context.read(cx).invoked_slash_command(&command_id)
+ && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status
{
- if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
- let run_commands_in_ranges = invoked_slash_command
- .run_commands_in_ranges
- .iter()
- .cloned()
- .collect::<Vec<_>>();
- for range in run_commands_in_ranges {
- let commands = self.context.update(cx, |context, cx| {
- context.reparse(cx);
- context
- .pending_commands_for_range(range.clone(), cx)
- .to_vec()
- });
+ let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone();
+ for range in run_commands_in_ranges {
+ let commands = self.context.update(cx, |context, cx| {
+ context.reparse(cx);
+ context
+ .pending_commands_for_range(range.clone(), cx)
+ .to_vec()
+ });
- for command in commands {
- self.run_command(
- command.source_range,
- &command.name,
- &command.arguments,
- false,
- self.workspace.clone(),
- window,
- cx,
- );
- }
+ for command in commands {
+ self.run_command(
+ command.source_range,
+ &command.name,
+ &command.arguments,
+ false,
+ self.workspace.clone(),
+ window,
+ cx,
+ );
}
}
}
@@ -1097,15 +1061,7 @@ impl TextThreadEditor {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(
- percentage(delta),
- ))
- },
- )
+ .with_rotate_animation(2)
.into_any_element(),
);
note = Some(Self::esc_kbd(cx).into_any_element());
@@ -1258,7 +1214,7 @@ impl TextThreadEditor {
let mut new_blocks = vec![];
let mut block_index_to_message = vec![];
for message in self.context.read(cx).messages(cx) {
- if let Some(_) = blocks_to_remove.remove(&message.id) {
+ if blocks_to_remove.remove(&message.id).is_some() {
// This is an old message that we might modify.
let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
debug_assert!(
@@ -1296,7 +1252,7 @@ impl TextThreadEditor {
context_editor_view: &Entity<TextThreadEditor>,
cx: &mut Context<Workspace>,
) -> Option<(String, bool)> {
- const CODE_FENCE_DELIMITER: &'static str = "```";
+ const CODE_FENCE_DELIMITER: &str = "```";
let context_editor = context_editor_view.read(cx).editor.clone();
context_editor.update(cx, |context_editor, cx| {
@@ -1760,7 +1716,7 @@ impl TextThreadEditor {
render_slash_command_output_toggle,
|_, _, _, _| Empty.into_any(),
)
- .with_metadata(metadata.crease.clone())
+ .with_metadata(metadata.crease)
}),
cx,
);
@@ -1831,7 +1787,7 @@ impl TextThreadEditor {
.filter_map(|(anchor, render_image)| {
const MAX_HEIGHT_IN_LINES: u32 = 8;
let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
- let image = render_image.clone();
+ let image = render_image;
anchor.is_valid(&buffer).then(|| BlockProperties {
placement: BlockPlacement::Above(anchor),
height: Some(MAX_HEIGHT_IN_LINES),
@@ -1893,8 +1849,55 @@ impl TextThreadEditor {
.update(cx, |context, cx| context.summarize(true, cx));
}
+ fn render_remaining_tokens(&self, cx: &App) -> Option<impl IntoElement + use<>> {
+ let (token_count_color, token_count, max_token_count, tooltip) =
+ match token_state(&self.context, cx)? {
+ TokenState::NoTokensLeft {
+ max_token_count,
+ token_count,
+ } => (
+ Color::Error,
+ token_count,
+ max_token_count,
+ Some("Token Limit Reached"),
+ ),
+ TokenState::HasMoreTokens {
+ max_token_count,
+ token_count,
+ over_warn_threshold,
+ } => {
+ let (color, tooltip) = if over_warn_threshold {
+ (Color::Warning, Some("Token Limit is Close to Exhaustion"))
+ } else {
+ (Color::Muted, None)
+ };
+ (color, token_count, max_token_count, tooltip)
+ }
+ };
+
+ Some(
+ h_flex()
+ .id("token-count")
+ .gap_0p5()
+ .child(
+ Label::new(humanize_token_count(token_count))
+ .size(LabelSize::Small)
+ .color(token_count_color),
+ )
+ .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
+ .child(
+ Label::new(humanize_token_count(max_token_count))
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .when_some(tooltip, |element, tooltip| {
+ element.tooltip(Tooltip::text(tooltip))
+ }),
+ )
+ }
+
fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let focus_handle = self.focus_handle(cx).clone();
+ let focus_handle = self.focus_handle(cx);
let (style, tooltip) = match token_state(&self.context, cx) {
Some(TokenState::NoTokensLeft { .. }) => (
@@ -1952,7 +1955,6 @@ impl TextThreadEditor {
ConfigurationError::NoProvider
| ConfigurationError::ModelNotFound
| ConfigurationError::ProviderNotAuthenticated(_) => true,
- ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms,
}
}
@@ -2036,7 +2038,7 @@ impl TextThreadEditor {
None => IconName::Ai,
};
- let focus_handle = self.editor().focus_handle(cx).clone();
+ let focus_handle = self.editor().focus_handle(cx);
PickerPopoverMenu::new(
self.language_model_selector.clone(),
@@ -2182,8 +2184,8 @@ impl TextThreadEditor {
/// Returns the contents of the *outermost* fenced code block that contains the given offset.
fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
- const CODE_BLOCK_NODE: &'static str = "fenced_code_block";
- const CODE_BLOCK_CONTENT: &'static str = "code_fence_content";
+ const CODE_BLOCK_NODE: &str = "fenced_code_block";
+ const CODE_BLOCK_CONTENT: &str = "code_fence_content";
let layer = snapshot.syntax_layers().next()?;
@@ -2398,70 +2400,6 @@ fn render_pending_slash_command_gutter_decoration(
icon.into_any_element()
}
-fn render_docs_slash_command_trailer(
- row: MultiBufferRow,
- command: ParsedSlashCommand,
- cx: &mut App,
-) -> AnyElement {
- if command.arguments.is_empty() {
- return Empty.into_any();
- }
- let args = DocsSlashCommandArgs::parse(&command.arguments);
-
- let Some(store) = args
- .provider()
- .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
- else {
- return Empty.into_any();
- };
-
- let Some(package) = args.package() else {
- return Empty.into_any();
- };
-
- let mut children = Vec::new();
-
- if store.is_indexing(&package) {
- children.push(
- div()
- .id(("crates-being-indexed", row.0))
- .child(Icon::new(IconName::ArrowCircle).with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(4)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- ))
- .tooltip({
- let package = package.clone();
- Tooltip::text(format!("Indexing {package}…"))
- })
- .into_any_element(),
- );
- }
-
- if let Some(latest_error) = store.latest_error_for_package(&package) {
- children.push(
- div()
- .id(("latest-error", row.0))
- .child(
- Icon::new(IconName::Warning)
- .size(IconSize::Small)
- .color(Color::Warning),
- )
- .tooltip(Tooltip::text(format!("Failed to index: {latest_error}")))
- .into_any_element(),
- )
- }
-
- let is_indexing = store.is_indexing(&package);
- let latest_error = store.latest_error_for_package(&package);
-
- if !is_indexing && latest_error.is_none() {
- return Empty.into_any();
- }
-
- h_flex().gap_2().children(children).into_any_element()
-}
-
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CopyMetadata {
creases: Vec<SelectedCreaseMetadata>,
@@ -2521,9 +2459,14 @@ impl Render for TextThreadEditor {
)
.child(
h_flex()
- .gap_1()
- .child(self.render_language_model_selector(window, cx))
- .child(self.render_send_button(window, cx)),
+ .gap_2p5()
+ .children(self.render_remaining_tokens(cx))
+ .child(
+ h_flex()
+ .gap_1()
+ .child(self.render_language_model_selector(window, cx))
+ .child(self.render_send_button(window, cx)),
+ ),
),
)
}
@@ -2811,58 +2754,6 @@ impl FollowableItem for TextThreadEditor {
}
}
-pub fn render_remaining_tokens(
- context_editor: &Entity<TextThreadEditor>,
- cx: &App,
-) -> Option<impl IntoElement + use<>> {
- let context = &context_editor.read(cx).context;
-
- let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)?
- {
- TokenState::NoTokensLeft {
- max_token_count,
- token_count,
- } => (
- Color::Error,
- token_count,
- max_token_count,
- Some("Token Limit Reached"),
- ),
- TokenState::HasMoreTokens {
- max_token_count,
- token_count,
- over_warn_threshold,
- } => {
- let (color, tooltip) = if over_warn_threshold {
- (Color::Warning, Some("Token Limit is Close to Exhaustion"))
- } else {
- (Color::Muted, None)
- };
- (color, token_count, max_token_count, tooltip)
- }
- };
-
- Some(
- h_flex()
- .id("token-count")
- .gap_0p5()
- .child(
- Label::new(humanize_token_count(token_count))
- .size(LabelSize::Small)
- .color(token_count_color),
- )
- .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
- .child(
- Label::new(humanize_token_count(max_token_count))
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- .when_some(tooltip, |element, tooltip| {
- element.tooltip(Tooltip::text(tooltip))
- }),
- )
-}
-
enum PendingSlashCommand {}
fn invoked_slash_command_fold_placeholder(
@@ -2891,11 +2782,7 @@ fn invoked_slash_command_fold_placeholder(
.child(Label::new(format!("/{}", command.name)))
.map(|parent| match &command.status {
InvokedSlashCommandStatus::Running(_) => {
- parent.child(Icon::new(IconName::ArrowCircle).with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(4)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- ))
+ parent.child(Icon::new(IconName::ArrowCircle).with_rotate_animation(4))
}
InvokedSlashCommandStatus::Error(message) => parent.child(
Label::new(format!("error: {message}"))
@@ -3214,7 +3101,7 @@ mod tests {
let context_editor = window
.update(&mut cx, |_, window, cx| {
cx.new(|cx| {
- let editor = TextThreadEditor::for_context(
+ TextThreadEditor::for_context(
context.clone(),
fs,
workspace.downgrade(),
@@ -3222,8 +3109,7 @@ mod tests {
None,
window,
cx,
- );
- editor
+ )
})
})
.unwrap();
@@ -73,7 +73,7 @@ impl ThreadHistory {
) -> Self {
let search_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("Search threads...", cx);
+ editor.set_placeholder_text("Search threads...", window, cx);
editor
});
@@ -166,14 +166,13 @@ impl ThreadHistory {
this.all_entries.len().saturating_sub(1),
cx,
);
- } else if let Some(prev_id) = previously_selected_entry {
- if let Some(new_ix) = this
+ } else if let Some(prev_id) = previously_selected_entry
+ && let Some(new_ix) = this
.all_entries
.iter()
.position(|probe| probe.id() == prev_id)
- {
- this.set_selected_entry_index(new_ix, cx);
- }
+ {
+ this.set_selected_entry_index(new_ix, cx);
}
}
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
@@ -541,6 +540,7 @@ impl Render for ThreadHistory {
v_flex()
.key_context("ThreadHistory")
.size_full()
+ .bg(cx.theme().colors().panel_background)
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_first))
@@ -14,13 +14,11 @@ pub struct IncompatibleToolsState {
impl IncompatibleToolsState {
pub fn new(thread: Entity<Thread>, cx: &mut Context<Self>) -> Self {
- let _tool_working_set_subscription =
- cx.subscribe(&thread, |this, _, event, _| match event {
- ThreadEvent::ProfileChanged => {
- this.cache.clear();
- }
- _ => {}
- });
+ let _tool_working_set_subscription = cx.subscribe(&thread, |this, _, event, _| {
+ if let ThreadEvent::ProfileChanged = event {
+ this.cache.clear();
+ }
+ });
Self {
cache: HashMap::default(),
@@ -1,14 +1,18 @@
+mod acp_onboarding_modal;
mod agent_notification;
mod burn_mode_tooltip;
+mod claude_code_onboarding_modal;
mod context_pill;
mod end_trial_upsell;
-// mod new_thread_button;
mod onboarding_modal;
pub mod preview;
+mod unavailable_editing_tooltip;
+pub use acp_onboarding_modal::*;
pub use agent_notification::*;
pub use burn_mode_tooltip::*;
+pub use claude_code_onboarding_modal::*;
pub use context_pill::*;
pub use end_trial_upsell::*;
-// pub use new_thread_button::*;
pub use onboarding_modal::*;
+pub use unavailable_editing_tooltip::*;
@@ -0,0 +1,246 @@
+use client::zed_urls;
+use gpui::{
+ ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
+ linear_color_stop, linear_gradient,
+};
+use ui::{TintColor, Vector, VectorName, prelude::*};
+use workspace::{ModalView, Workspace};
+
+use crate::agent_panel::{AgentPanel, AgentType};
+
+macro_rules! acp_onboarding_event {
+ ($name:expr) => {
+ telemetry::event!($name, source = "ACP Onboarding");
+ };
+ ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
+ telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+);
+ };
+}
+
+pub struct AcpOnboardingModal {
+ focus_handle: FocusHandle,
+ workspace: Entity<Workspace>,
+}
+
+impl AcpOnboardingModal {
+ pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
+ let workspace_entity = cx.entity();
+ workspace.toggle_modal(window, cx, |_window, cx| Self {
+ workspace: workspace_entity,
+ focus_handle: cx.focus_handle(),
+ });
+ }
+
+ fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
+ self.workspace.update(cx, |workspace, cx| {
+ workspace.focus_panel::<AgentPanel>(window, cx);
+
+ if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+ panel.update(cx, |panel, cx| {
+ panel.new_agent_thread(AgentType::Gemini, window, cx);
+ });
+ }
+ });
+
+ cx.emit(DismissEvent);
+
+ acp_onboarding_event!("Open Panel Clicked");
+ }
+
+ fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
+ cx.open_url(&zed_urls::external_agents_docs(cx));
+ cx.notify();
+
+ acp_onboarding_event!("Documentation Link Clicked");
+ }
+
+ fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+ cx.emit(DismissEvent);
+ }
+}
+
+impl EventEmitter<DismissEvent> for AcpOnboardingModal {}
+
+impl Focusable for AcpOnboardingModal {
+ fn focus_handle(&self, _cx: &App) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl ModalView for AcpOnboardingModal {}
+
+impl Render for AcpOnboardingModal {
+ fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let illustration_element = |label: bool, opacity: f32| {
+ h_flex()
+ .px_1()
+ .py_0p5()
+ .gap_1()
+ .rounded_sm()
+ .bg(cx.theme().colors().element_active.opacity(0.05))
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .border_dashed()
+ .child(
+ Icon::new(IconName::Stop)
+ .size(IconSize::Small)
+ .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
+ )
+ .map(|this| {
+ if label {
+ this.child(
+ Label::new("Your Agent Here")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ } else {
+ this.child(
+ div().w_16().h_1().rounded_full().bg(cx
+ .theme()
+ .colors()
+ .element_active
+ .opacity(0.6)),
+ )
+ }
+ })
+ .opacity(opacity)
+ };
+
+ let illustration = h_flex()
+ .relative()
+ .h(rems_from_px(126.))
+ .bg(cx.theme().colors().editor_background)
+ .border_b_1()
+ .border_color(cx.theme().colors().border_variant)
+ .justify_center()
+ .gap_8()
+ .rounded_t_md()
+ .overflow_hidden()
+ .child(
+ div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
+ Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
+ .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
+ ),
+ )
+ .child(div().absolute().inset_0().size_full().bg(linear_gradient(
+ 0.,
+ linear_color_stop(
+ cx.theme().colors().elevated_surface_background.opacity(0.1),
+ 0.9,
+ ),
+ linear_color_stop(
+ cx.theme().colors().elevated_surface_background.opacity(0.),
+ 0.,
+ ),
+ )))
+ .child(
+ div()
+ .absolute()
+ .inset_0()
+ .size_full()
+ .bg(gpui::black().opacity(0.15)),
+ )
+ .child(
+ Vector::new(
+ VectorName::AcpLogoSerif,
+ rems_from_px(257.),
+ rems_from_px(47.),
+ )
+ .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
+ )
+ .child(
+ v_flex()
+ .gap_1p5()
+ .child(illustration_element(false, 0.15))
+ .child(illustration_element(true, 0.3))
+ .child(
+ h_flex()
+ .pl_1()
+ .pr_2()
+ .py_0p5()
+ .gap_1()
+ .rounded_sm()
+ .bg(cx.theme().colors().element_active.opacity(0.2))
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Icon::new(IconName::AiGemini)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)),
+ )
+ .child(illustration_element(true, 0.3))
+ .child(illustration_element(false, 0.15)),
+ );
+
+ let heading = v_flex()
+ .w_full()
+ .gap_1()
+ .child(
+ Label::new("Now Available")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large));
+
+ let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
+
+ let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
+ .icon_size(IconSize::Indicator)
+ .style(ButtonStyle::Tinted(TintColor::Accent))
+ .full_width()
+ .on_click(cx.listener(Self::open_panel));
+
+ let docs_button = Button::new("add-other-agents", "Add Other Agents")
+ .icon(IconName::ArrowUpRight)
+ .icon_size(IconSize::Indicator)
+ .icon_color(Color::Muted)
+ .full_width()
+ .on_click(cx.listener(Self::view_docs));
+
+ let close_button = h_flex().absolute().top_2().right_2().child(
+ IconButton::new("cancel", IconName::Close).on_click(cx.listener(
+ |_, _: &ClickEvent, _window, cx| {
+ acp_onboarding_event!("Canceled", trigger = "X click");
+ cx.emit(DismissEvent);
+ },
+ )),
+ );
+
+ v_flex()
+ .id("acp-onboarding")
+ .key_context("AcpOnboardingModal")
+ .relative()
+ .w(rems(34.))
+ .h_full()
+ .elevation_3(cx)
+ .track_focus(&self.focus_handle(cx))
+ .overflow_hidden()
+ .on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
+ acp_onboarding_event!("Canceled", trigger = "Action");
+ cx.emit(DismissEvent);
+ }))
+ .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
+ this.focus_handle.focus(window);
+ }))
+ .child(illustration)
+ .child(
+ v_flex()
+ .p_4()
+ .gap_2()
+ .child(heading)
+ .child(Label::new(copy).color(Color::Muted))
+ .child(
+ v_flex()
+ .w_full()
+ .mt_2()
+ .gap_1()
+ .child(open_panel_button)
+ .child(docs_button),
+ ),
+ )
+ .child(close_button)
+ }
+}
@@ -62,6 +62,8 @@ impl AgentNotification {
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
+ tabbing_identifier: None,
+ ..Default::default()
}
}
}
@@ -2,11 +2,11 @@ use crate::ToggleBurnMode;
use gpui::{Context, FontWeight, IntoElement, Render, Window};
use ui::{KeyBinding, prelude::*, tooltip_container};
-pub struct MaxModeTooltip {
+pub struct BurnModeTooltip {
selected: bool,
}
-impl MaxModeTooltip {
+impl BurnModeTooltip {
pub fn new() -> Self {
Self { selected: false }
}
@@ -17,7 +17,7 @@ impl MaxModeTooltip {
}
}
-impl Render for MaxModeTooltip {
+impl Render for BurnModeTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (icon, color) = if self.selected {
(IconName::ZedBurnModeOn, Color::Error)
@@ -0,0 +1,254 @@
+use client::zed_urls;
+use gpui::{
+ ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
+ linear_color_stop, linear_gradient,
+};
+use ui::{TintColor, Vector, VectorName, prelude::*};
+use workspace::{ModalView, Workspace};
+
+use crate::agent_panel::{AgentPanel, AgentType};
+
+macro_rules! claude_code_onboarding_event {
+ ($name:expr) => {
+ telemetry::event!($name, source = "ACP Claude Code Onboarding");
+ };
+ ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
+ telemetry::event!($name, source = "ACP Claude Code Onboarding", $($key $(= $value)?),+);
+ };
+}
+
+pub struct ClaudeCodeOnboardingModal {
+ focus_handle: FocusHandle,
+ workspace: Entity<Workspace>,
+}
+
+impl ClaudeCodeOnboardingModal {
+ pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
+ let workspace_entity = cx.entity();
+ workspace.toggle_modal(window, cx, |_window, cx| Self {
+ workspace: workspace_entity,
+ focus_handle: cx.focus_handle(),
+ });
+ }
+
+ fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
+ self.workspace.update(cx, |workspace, cx| {
+ workspace.focus_panel::<AgentPanel>(window, cx);
+
+ if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
+ panel.update(cx, |panel, cx| {
+ panel.new_agent_thread(AgentType::ClaudeCode, window, cx);
+ });
+ }
+ });
+
+ cx.emit(DismissEvent);
+
+ claude_code_onboarding_event!("Open Panel Clicked");
+ }
+
+ fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
+ cx.open_url(&zed_urls::external_agents_docs(cx));
+ cx.notify();
+
+ claude_code_onboarding_event!("Documentation Link Clicked");
+ }
+
+ fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+ cx.emit(DismissEvent);
+ }
+}
+
+impl EventEmitter<DismissEvent> for ClaudeCodeOnboardingModal {}
+
+impl Focusable for ClaudeCodeOnboardingModal {
+ fn focus_handle(&self, _cx: &App) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl ModalView for ClaudeCodeOnboardingModal {}
+
+impl Render for ClaudeCodeOnboardingModal {
+ fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let illustration_element = |icon: IconName, label: Option<SharedString>, opacity: f32| {
+ h_flex()
+ .px_1()
+ .py_0p5()
+ .gap_1()
+ .rounded_sm()
+ .bg(cx.theme().colors().element_active.opacity(0.05))
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .border_dashed()
+ .child(
+ Icon::new(icon)
+ .size(IconSize::Small)
+ .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
+ )
+ .map(|this| {
+ if let Some(label_text) = label {
+ this.child(
+ Label::new(label_text)
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ } else {
+ this.child(
+ div().w_16().h_1().rounded_full().bg(cx
+ .theme()
+ .colors()
+ .element_active
+ .opacity(0.6)),
+ )
+ }
+ })
+ .opacity(opacity)
+ };
+
+ let illustration = h_flex()
+ .relative()
+ .h(rems_from_px(126.))
+ .bg(cx.theme().colors().editor_background)
+ .border_b_1()
+ .border_color(cx.theme().colors().border_variant)
+ .justify_center()
+ .gap_8()
+ .rounded_t_md()
+ .overflow_hidden()
+ .child(
+ div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
+ Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
+ .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
+ ),
+ )
+ .child(div().absolute().inset_0().size_full().bg(linear_gradient(
+ 0.,
+ linear_color_stop(
+ cx.theme().colors().elevated_surface_background.opacity(0.1),
+ 0.9,
+ ),
+ linear_color_stop(
+ cx.theme().colors().elevated_surface_background.opacity(0.),
+ 0.,
+ ),
+ )))
+ .child(
+ div()
+ .absolute()
+ .inset_0()
+ .size_full()
+ .bg(gpui::black().opacity(0.15)),
+ )
+ .child(
+ Vector::new(
+ VectorName::AcpLogoSerif,
+ rems_from_px(257.),
+ rems_from_px(47.),
+ )
+ .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
+ )
+ .child(
+ v_flex()
+ .gap_1p5()
+ .child(illustration_element(IconName::Stop, None, 0.15))
+ .child(illustration_element(
+ IconName::AiGemini,
+ Some("New Gemini CLI Thread".into()),
+ 0.3,
+ ))
+ .child(
+ h_flex()
+ .pl_1()
+ .pr_2()
+ .py_0p5()
+ .gap_1()
+ .rounded_sm()
+ .bg(cx.theme().colors().element_active.opacity(0.2))
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Icon::new(IconName::AiClaude)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Label::new("New Claude Code Thread").size(LabelSize::Small)),
+ )
+ .child(illustration_element(
+ IconName::Stop,
+ Some("Your Agent Here".into()),
+ 0.3,
+ ))
+ .child(illustration_element(IconName::Stop, None, 0.15)),
+ );
+
+ let heading = v_flex()
+ .w_full()
+ .gap_1()
+ .child(
+ Label::new("Beta Release")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .child(Headline::new("Claude Code: Natively in Zed").size(HeadlineSize::Large));
+
+ let copy = "Powered by the Agent Client Protocol, you can now run Claude Code as\na first-class citizen in Zed's agent panel.";
+
+ let open_panel_button = Button::new("open-panel", "Start with Claude Code")
+ .icon_size(IconSize::Indicator)
+ .style(ButtonStyle::Tinted(TintColor::Accent))
+ .full_width()
+ .on_click(cx.listener(Self::open_panel));
+
+ let docs_button = Button::new("add-other-agents", "Add Other Agents")
+ .icon(IconName::ArrowUpRight)
+ .icon_size(IconSize::Indicator)
+ .icon_color(Color::Muted)
+ .full_width()
+ .on_click(cx.listener(Self::view_docs));
+
+ let close_button = h_flex().absolute().top_2().right_2().child(
+ IconButton::new("cancel", IconName::Close).on_click(cx.listener(
+ |_, _: &ClickEvent, _window, cx| {
+ claude_code_onboarding_event!("Canceled", trigger = "X click");
+ cx.emit(DismissEvent);
+ },
+ )),
+ );
+
+ v_flex()
+ .id("acp-onboarding")
+ .key_context("AcpOnboardingModal")
+ .relative()
+ .w(rems(34.))
+ .h_full()
+ .elevation_3(cx)
+ .track_focus(&self.focus_handle(cx))
+ .overflow_hidden()
+ .on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
+ claude_code_onboarding_event!("Canceled", trigger = "Action");
+ cx.emit(DismissEvent);
+ }))
+ .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
+ this.focus_handle.focus(window);
+ }))
+ .child(illustration)
+ .child(
+ v_flex()
+ .p_4()
+ .gap_2()
+ .child(heading)
+ .child(Label::new(copy).color(Color::Muted))
+ .child(
+ v_flex()
+ .w_full()
+ .mt_2()
+ .gap_1()
+ .child(open_panel_button)
+ .child(docs_button),
+ ),
+ )
+ .child(close_button)
+ }
+}
@@ -353,7 +353,7 @@ impl AddedContext {
name,
parent,
tooltip: Some(full_path_string),
- icon_path: FileIcons::get_icon(&full_path, cx),
+ icon_path: FileIcons::get_icon(full_path, cx),
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::File(handle),
@@ -499,7 +499,7 @@ impl AddedContext {
let thread = handle.thread.clone();
Some(Rc::new(move |_, cx| {
let text = thread.read(cx).latest_detailed_summary_or_text();
- ContextPillHover::new_text(text.clone(), cx).into()
+ ContextPillHover::new_text(text, cx).into()
}))
},
handle: AgentContextHandle::Thread(handle),
@@ -574,7 +574,7 @@ impl AddedContext {
.unwrap_or_else(|| "Unnamed Rule".into());
Some(AddedContext {
kind: ContextKind::Rules,
- name: title.clone(),
+ name: title,
parent: None,
tooltip: None,
icon_path: None,
@@ -615,7 +615,7 @@ impl AddedContext {
let full_path_string: SharedString = full_path.to_string_lossy().into_owned().into();
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, &full_path_string);
- let icon_path = FileIcons::get_icon(&full_path, cx);
+ let icon_path = FileIcons::get_icon(full_path, cx);
(name, parent, icon_path)
} else {
("Image".into(), None, None)
@@ -706,7 +706,7 @@ impl ContextFileExcerpt {
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().into_owned().into());
- let icon_path = FileIcons::get_icon(&full_path, cx);
+ let icon_path = FileIcons::get_icon(full_path, cx);
ContextFileExcerpt {
file_name_and_range: file_name_and_range.into(),
@@ -2,24 +2,27 @@ use std::sync::Arc;
use ai_onboarding::{AgentPanelOnboardingCard, PlanDefinitions};
use client::zed_urls;
+use cloud_llm_client::{Plan, PlanV1};
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Divider, Tooltip, prelude::*};
#[derive(IntoElement, RegisterComponent)]
pub struct EndTrialUpsell {
+ plan: Plan,
dismiss_upsell: Arc<dyn Fn(&mut Window, &mut App)>,
}
impl EndTrialUpsell {
- pub fn new(dismiss_upsell: Arc<dyn Fn(&mut Window, &mut App)>) -> Self {
- Self { dismiss_upsell }
+ pub fn new(plan: Plan, dismiss_upsell: Arc<dyn Fn(&mut Window, &mut App)>) -> Self {
+ Self {
+ plan,
+ dismiss_upsell,
+ }
}
}
impl RenderOnce for EndTrialUpsell {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
- let plan_definitions = PlanDefinitions;
-
let pro_section = v_flex()
.gap_1()
.child(
@@ -33,7 +36,7 @@ impl RenderOnce for EndTrialUpsell {
)
.child(Divider::horizontal()),
)
- .child(plan_definitions.pro_plan(false))
+ .child(PlanDefinitions.pro_plan(self.plan.is_v2(), false))
.child(
Button::new("cta-button", "Upgrade to Zed Pro")
.full_width()
@@ -64,7 +67,7 @@ impl RenderOnce for EndTrialUpsell {
)
.child(Divider::horizontal()),
)
- .child(plan_definitions.free_plan());
+ .child(PlanDefinitions.free_plan(self.plan.is_v2()));
AgentPanelOnboardingCard::new()
.child(Headline::new("Your Zed Pro Trial has expired"))
@@ -109,6 +112,7 @@ impl Component for EndTrialUpsell {
Some(
v_flex()
.child(EndTrialUpsell {
+ plan: Plan::V1(PlanV1::ZedFree),
dismiss_upsell: Arc::new(|_, _| {}),
})
.into_any_element(),
@@ -1,75 +0,0 @@
-use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled};
-use ui::prelude::*;
-
-#[derive(IntoElement)]
-pub struct NewThreadButton {
- id: ElementId,
- label: SharedString,
- icon: IconName,
- keybinding: Option<ui::KeyBinding>,
- on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
-}
-
-impl NewThreadButton {
- fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self {
- Self {
- id: id.into(),
- label: label.into(),
- icon,
- keybinding: None,
- on_click: None,
- }
- }
-
- fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self {
- self.keybinding = keybinding;
- self
- }
-
- fn on_click<F>(mut self, handler: F) -> Self
- where
- F: Fn(&mut Window, &mut App) + 'static,
- {
- self.on_click = Some(Box::new(
- move |_: &ClickEvent, window: &mut Window, cx: &mut App| handler(window, cx),
- ));
- self
- }
-}
-
-impl RenderOnce for NewThreadButton {
- fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
- h_flex()
- .id(self.id)
- .w_full()
- .py_1p5()
- .px_2()
- .gap_1()
- .justify_between()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border.opacity(0.4))
- .bg(cx.theme().colors().element_active.opacity(0.2))
- .hover(|style| {
- style
- .bg(cx.theme().colors().element_hover)
- .border_color(cx.theme().colors().border)
- })
- .child(
- h_flex()
- .gap_1p5()
- .child(
- Icon::new(self.icon)
- .size(IconSize::XSmall)
- .color(Color::Muted),
- )
- .child(Label::new(self.label).size(LabelSize::Small)),
- )
- .when_some(self.keybinding, |this, keybinding| {
- this.child(keybinding.size(rems_from_px(10.)))
- })
- .when_some(self.on_click, |this, on_click| {
- this.on_click(move |event, window, cx| on_click(event, window, cx))
- })
- }
-}
@@ -1,5 +1,5 @@
use client::{ModelRequestUsage, RequestUsage, zed_urls};
-use cloud_llm_client::{Plan, UsageLimit};
+use cloud_llm_client::{Plan, PlanV1, PlanV2, UsageLimit};
use component::{empty_example, example_group_with_title, single_example};
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Callout, prelude::*};
@@ -38,20 +38,20 @@ impl RenderOnce for UsageCallout {
let (title, message, button_text, url) = if is_limit_reached {
match self.plan {
- Plan::ZedFree => (
+ Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree) => (
"Out of free prompts",
"Upgrade to continue, wait for the next reset, or switch to API key."
.to_string(),
"Upgrade",
zed_urls::account_url(cx),
),
- Plan::ZedProTrial => (
+ Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial) => (
"Out of trial prompts",
"Upgrade to Zed Pro to continue, or switch to API key.".to_string(),
"Upgrade",
zed_urls::account_url(cx),
),
- Plan::ZedPro => (
+ Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro) => (
"Out of included prompts",
"Enable usage-based billing to continue.".to_string(),
"Manage",
@@ -60,7 +60,7 @@ impl RenderOnce for UsageCallout {
}
} else {
match self.plan {
- Plan::ZedFree => (
+ Plan::V1(PlanV1::ZedFree) => (
"Reaching free plan limit soon",
format!(
"{remaining} remaining - Upgrade to increase limit, or switch providers",
@@ -68,7 +68,7 @@ impl RenderOnce for UsageCallout {
"Upgrade",
zed_urls::account_url(cx),
),
- Plan::ZedProTrial => (
+ Plan::V1(PlanV1::ZedProTrial) => (
"Reaching trial limit soon",
format!(
"{remaining} remaining - Upgrade to increase limit, or switch providers",
@@ -76,35 +76,28 @@ impl RenderOnce for UsageCallout {
"Upgrade",
zed_urls::account_url(cx),
),
- _ => return div().into_any_element(),
+ Plan::V1(PlanV1::ZedPro) | Plan::V2(_) => return div().into_any_element(),
}
};
- let icon = if is_limit_reached {
- Icon::new(IconName::Close)
- .color(Color::Error)
- .size(IconSize::XSmall)
+ let (icon, severity) = if is_limit_reached {
+ (IconName::Close, Severity::Error)
} else {
- Icon::new(IconName::Warning)
- .color(Color::Warning)
- .size(IconSize::XSmall)
+ (IconName::Warning, Severity::Warning)
};
- div()
- .border_t_1()
- .border_color(cx.theme().colors().border)
- .child(
- Callout::new()
- .icon(icon)
- .title(title)
- .description(message)
- .primary_action(
- Button::new("upgrade", button_text)
- .label_size(LabelSize::Small)
- .on_click(move |_, _, cx| {
- cx.open_url(&url);
- }),
- ),
+ Callout::new()
+ .icon(icon)
+ .severity(severity)
+ .icon(icon)
+ .title(title)
+ .description(message)
+ .actions_slot(
+ Button::new("upgrade", button_text)
+ .label_size(LabelSize::Small)
+ .on_click(move |_, _, cx| {
+ cx.open_url(&url);
+ }),
)
.into_any_element()
}
@@ -126,7 +119,7 @@ impl Component for UsageCallout {
single_example(
"Approaching limit (90%)",
UsageCallout::new(
- Plan::ZedFree,
+ Plan::V1(PlanV1::ZedFree),
ModelRequestUsage(RequestUsage {
limit: UsageLimit::Limited(50),
amount: 45, // 90% of limit
@@ -137,7 +130,7 @@ impl Component for UsageCallout {
single_example(
"Limit reached (100%)",
UsageCallout::new(
- Plan::ZedFree,
+ Plan::V1(PlanV1::ZedFree),
ModelRequestUsage(RequestUsage {
limit: UsageLimit::Limited(50),
amount: 50, // 100% of limit
@@ -154,7 +147,7 @@ impl Component for UsageCallout {
single_example(
"Approaching limit (90%)",
UsageCallout::new(
- Plan::ZedProTrial,
+ Plan::V1(PlanV1::ZedProTrial),
ModelRequestUsage(RequestUsage {
limit: UsageLimit::Limited(150),
amount: 135, // 90% of limit
@@ -165,7 +158,7 @@ impl Component for UsageCallout {
single_example(
"Limit reached (100%)",
UsageCallout::new(
- Plan::ZedProTrial,
+ Plan::V1(PlanV1::ZedProTrial),
ModelRequestUsage(RequestUsage {
limit: UsageLimit::Limited(150),
amount: 150, // 100% of limit
@@ -182,7 +175,7 @@ impl Component for UsageCallout {
single_example(
"Limit reached (100%)",
UsageCallout::new(
- Plan::ZedPro,
+ Plan::V1(PlanV1::ZedPro),
ModelRequestUsage(RequestUsage {
limit: UsageLimit::Limited(500),
amount: 500, // 100% of limit
@@ -0,0 +1,29 @@
+use gpui::{Context, IntoElement, Render, Window};
+use ui::{prelude::*, tooltip_container};
+
+pub struct UnavailableEditingTooltip {
+ agent_name: SharedString,
+}
+
+impl UnavailableEditingTooltip {
+ pub fn new(agent_name: SharedString) -> Self {
+ Self { agent_name }
+ }
+}
+
+impl Render for UnavailableEditingTooltip {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ tooltip_container(window, cx, |this, _, _| {
+ this.child(Label::new("Unavailable Editing")).child(
+ div().max_w_64().child(
+ Label::new(format!(
+ "Editing previous messages is not available for {} yet.",
+ self.agent_name
+ ))
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ })
+ }
+}
@@ -18,6 +18,7 @@ default = []
client.workspace = true
cloud_llm_client.workspace = true
component.workspace = true
+feature_flags.workspace = true
gpui.workspace = true
language_model.workspace = true
serde.workspace = true
@@ -11,7 +11,7 @@ impl ApiKeysWithProviders {
cx.subscribe(
&LanguageModelRegistry::global(cx),
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
- language_model::Event::ProviderStateChanged
+ language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
this.configured_providers = Self::compute_configured_providers(cx)
@@ -33,7 +33,7 @@ impl ApiKeysWithProviders {
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
- .map(|provider| (provider.icon(), provider.name().0.clone()))
+ .map(|provider| (provider.icon(), provider.name().0))
.collect()
}
}
@@ -1,7 +1,7 @@
use std::sync::Arc;
use client::{Client, UserStore};
-use cloud_llm_client::Plan;
+use cloud_llm_client::{Plan, PlanV1, PlanV2};
use gpui::{Entity, IntoElement, ParentElement};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::prelude::*;
@@ -25,7 +25,7 @@ impl AgentPanelOnboarding {
cx.subscribe(
&LanguageModelRegistry::global(cx),
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
- language_model::Event::ProviderStateChanged
+ language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
this.configured_providers = Self::compute_available_providers(cx)
@@ -50,15 +50,22 @@ impl AgentPanelOnboarding {
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
- .map(|provider| (provider.icon(), provider.name().0.clone()))
+ .map(|provider| (provider.icon(), provider.name().0))
.collect()
}
}
impl Render for AgentPanelOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let enrolled_in_trial = self.user_store.read(cx).plan() == Some(Plan::ZedProTrial);
- let is_pro_user = self.user_store.read(cx).plan() == Some(Plan::ZedPro);
+ let enrolled_in_trial = self.user_store.read(cx).plan().is_some_and(|plan| {
+ matches!(
+ plan,
+ Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial)
+ )
+ });
+ let is_pro_user = self.user_store.read(cx).plan().is_some_and(|plan| {
+ matches!(plan, Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro))
+ });
AgentPanelOnboardingCard::new()
.child(
@@ -74,7 +81,7 @@ impl Render for AgentPanelOnboarding {
}),
)
.map(|this| {
- if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 {
+ if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
this
} else {
this.child(ApiKeysWithoutProviders::new())
@@ -10,7 +10,7 @@ pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProvider
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
pub use ai_upsell_card::AiUpsellCard;
-use cloud_llm_client::Plan;
+use cloud_llm_client::{Plan, PlanV1, PlanV2};
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
pub use plan_definitions::PlanDefinitions;
pub use young_account_banner::YoungAccountBanner;
@@ -18,8 +18,9 @@ pub use young_account_banner::YoungAccountBanner;
use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
+use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt as _};
use gpui::{AnyElement, Entity, IntoElement, ParentElement};
-use ui::{Divider, RegisterComponent, TintColor, Tooltip, prelude::*};
+use ui::{Divider, RegisterComponent, Tooltip, prelude::*};
#[derive(PartialEq)]
pub enum SignInStatus {
@@ -43,12 +44,10 @@ impl From<client::Status> for SignInStatus {
#[derive(RegisterComponent, IntoElement)]
pub struct ZedAiOnboarding {
pub sign_in_status: SignInStatus,
- pub has_accepted_terms_of_service: bool,
pub plan: Option<Plan>,
pub account_too_young: bool,
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
- pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
}
@@ -64,17 +63,9 @@ impl ZedAiOnboarding {
Self {
sign_in_status: status.into(),
- has_accepted_terms_of_service: store.has_accepted_terms_of_service(),
plan: store.plan(),
account_too_young: store.account_too_young(),
continue_with_zed_ai,
- accept_terms_of_service: Arc::new({
- let store = user_store.clone();
- move |_window, cx| {
- let task = store.update(cx, |store, cx| store.accept_terms_of_service(cx));
- task.detach_and_log_err(cx);
- }
- }),
sign_in: Arc::new(move |_window, cx| {
cx.spawn({
let client = client.clone();
@@ -94,45 +85,8 @@ impl ZedAiOnboarding {
self
}
- fn render_accept_terms_of_service(&self) -> AnyElement {
- v_flex()
- .gap_1()
- .w_full()
- .child(Headline::new("Accept Terms of Service"))
- .child(
- Label::new("We don’t sell your data, track you across the web, or compromise your privacy.")
- .color(Color::Muted)
- .mb_2(),
- )
- .child(
- Button::new("terms_of_service", "Review Terms of Service")
- .full_width()
- .style(ButtonStyle::Outlined)
- .icon(IconName::ArrowUpRight)
- .icon_color(Color::Muted)
- .icon_size(IconSize::Small)
- .on_click(move |_, _window, cx| {
- telemetry::event!("Review Terms of Service Clicked");
- cx.open_url(&zed_urls::terms_of_service(cx))
- }),
- )
- .child(
- Button::new("accept_terms", "Accept")
- .full_width()
- .style(ButtonStyle::Tinted(TintColor::Accent))
- .on_click({
- let callback = self.accept_terms_of_service.clone();
- move |_, window, cx| {
- telemetry::event!("Terms of Service Accepted");
- (callback)(window, cx)}
- }),
- )
- .into_any_element()
- }
-
- fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
+ fn render_sign_in_disclaimer(&self, cx: &mut App) -> AnyElement {
let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
- let plan_definitions = PlanDefinitions;
v_flex()
.gap_1()
@@ -142,7 +96,7 @@ impl ZedAiOnboarding {
.color(Color::Muted)
.mb_2(),
)
- .child(plan_definitions.pro_plan(false))
+ .child(PlanDefinitions.pro_plan(cx.has_flag::<BillingV2FeatureFlag>(), false))
.child(
Button::new("sign_in", "Try Zed Pro for Free")
.disabled(signing_in)
@@ -159,17 +113,14 @@ impl ZedAiOnboarding {
.into_any_element()
}
- fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
- let young_account_banner = YoungAccountBanner;
- let plan_definitions = PlanDefinitions;
-
+ fn render_free_plan_state(&self, is_v2: bool, cx: &mut App) -> AnyElement {
if self.account_too_young {
v_flex()
.relative()
.max_w_full()
.gap_1()
.child(Headline::new("Welcome to Zed AI"))
- .child(young_account_banner)
+ .child(YoungAccountBanner)
.child(
v_flex()
.mt_2()
@@ -185,7 +136,7 @@ impl ZedAiOnboarding {
)
.child(Divider::horizontal()),
)
- .child(plan_definitions.pro_plan(true))
+ .child(PlanDefinitions.pro_plan(is_v2, true))
.child(
Button::new("pro", "Get Started")
.full_width()
@@ -228,7 +179,7 @@ impl ZedAiOnboarding {
)
.child(Divider::horizontal()),
)
- .child(plan_definitions.free_plan()),
+ .child(PlanDefinitions.free_plan(is_v2)),
)
.when_some(
self.dismiss_onboarding.as_ref(),
@@ -266,7 +217,7 @@ impl ZedAiOnboarding {
)
.child(Divider::horizontal()),
)
- .child(plan_definitions.pro_trial(true))
+ .child(PlanDefinitions.pro_trial(is_v2, true))
.child(
Button::new("pro", "Start Free Trial")
.full_width()
@@ -284,9 +235,7 @@ impl ZedAiOnboarding {
}
}
- fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
- let plan_definitions = PlanDefinitions;
-
+ fn render_trial_state(&self, is_v2: bool, _cx: &mut App) -> AnyElement {
v_flex()
.relative()
.gap_1()
@@ -296,7 +245,7 @@ impl ZedAiOnboarding {
.color(Color::Muted)
.mb_2(),
)
- .child(plan_definitions.pro_trial(false))
+ .child(PlanDefinitions.pro_trial(is_v2, false))
.when_some(
self.dismiss_onboarding.as_ref(),
|this, dismiss_callback| {
@@ -320,9 +269,7 @@ impl ZedAiOnboarding {
.into_any_element()
}
- fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
- let plan_definitions = PlanDefinitions;
-
+ fn render_pro_plan_state(&self, is_v2: bool, _cx: &mut App) -> AnyElement {
v_flex()
.gap_1()
.child(Headline::new("Welcome to Zed Pro"))
@@ -331,18 +278,26 @@ impl ZedAiOnboarding {
.color(Color::Muted)
.mb_2(),
)
- .child(plan_definitions.pro_plan(false))
- .child(
- Button::new("pro", "Continue with Zed Pro")
- .full_width()
- .style(ButtonStyle::Outlined)
- .on_click({
- let callback = self.continue_with_zed_ai.clone();
- move |_, window, cx| {
- telemetry::event!("Banner Dismissed", source = "AI Onboarding");
- callback(window, cx)
- }
- }),
+ .child(PlanDefinitions.pro_plan(is_v2, false))
+ .when_some(
+ self.dismiss_onboarding.as_ref(),
+ |this, dismiss_callback| {
+ let callback = dismiss_callback.clone();
+ this.child(
+ h_flex().absolute().top_0().right_0().child(
+ IconButton::new("dismiss_onboarding", IconName::Close)
+ .icon_size(IconSize::Small)
+ .tooltip(Tooltip::text("Dismiss"))
+ .on_click(move |_, window, cx| {
+ telemetry::event!(
+ "Banner Dismissed",
+ source = "AI Onboarding",
+ );
+ callback(window, cx)
+ }),
+ ),
+ )
+ },
)
.into_any_element()
}
@@ -351,14 +306,17 @@ impl ZedAiOnboarding {
impl RenderOnce for ZedAiOnboarding {
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
- if self.has_accepted_terms_of_service {
- match self.plan {
- None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
- Some(Plan::ZedProTrial) => self.render_trial_state(cx),
- Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
+ match self.plan {
+ None => self.render_free_plan_state(cx.has_flag::<BillingV2FeatureFlag>(), cx),
+ Some(plan @ (Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))) => {
+ self.render_free_plan_state(plan.is_v2(), cx)
+ }
+ Some(plan @ (Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial))) => {
+ self.render_trial_state(plan.is_v2(), cx)
+ }
+ Some(plan @ (Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro))) => {
+ self.render_pro_plan_state(plan.is_v2(), cx)
}
- } else {
- self.render_accept_terms_of_service()
}
} else {
self.render_sign_in_disclaimer(cx)
@@ -382,18 +340,15 @@ impl Component for ZedAiOnboarding {
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn onboarding(
sign_in_status: SignInStatus,
- has_accepted_terms_of_service: bool,
plan: Option<Plan>,
account_too_young: bool,
) -> AnyElement {
ZedAiOnboarding {
sign_in_status,
- has_accepted_terms_of_service,
plan,
account_too_young,
continue_with_zed_ai: Arc::new(|_, _| {}),
sign_in: Arc::new(|_, _| {}),
- accept_terms_of_service: Arc::new(|_, _| {}),
dismiss_onboarding: None,
}
.into_any_element()
@@ -407,27 +362,35 @@ impl Component for ZedAiOnboarding {
.children(vec![
single_example(
"Not Signed-in",
- onboarding(SignInStatus::SignedOut, false, None, false),
- ),
- single_example(
- "Not Accepted ToS",
- onboarding(SignInStatus::SignedIn, false, None, false),
+ onboarding(SignInStatus::SignedOut, None, false),
),
single_example(
"Young Account",
- onboarding(SignInStatus::SignedIn, true, None, true),
+ onboarding(SignInStatus::SignedIn, None, true),
),
single_example(
"Free Plan",
- onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false),
+ onboarding(
+ SignInStatus::SignedIn,
+ Some(Plan::V1(PlanV1::ZedFree)),
+ false,
+ ),
),
single_example(
"Pro Trial",
- onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false),
+ onboarding(
+ SignInStatus::SignedIn,
+ Some(Plan::V1(PlanV1::ZedProTrial)),
+ false,
+ ),
),
single_example(
"Pro Plan",
- onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false),
+ onboarding(
+ SignInStatus::SignedIn,
+ Some(Plan::V1(PlanV1::ZedPro)),
+ false,
+ ),
),
])
.into_any_element(),
@@ -1,22 +1,20 @@
-use std::{sync::Arc, time::Duration};
+use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
-use cloud_llm_client::Plan;
-use gpui::{
- Animation, AnimationExt, AnyElement, App, Entity, IntoElement, RenderOnce, Transformation,
- Window, percentage,
-};
-use ui::{Divider, Vector, VectorName, prelude::*};
+use cloud_llm_client::{Plan, PlanV1, PlanV2};
+use feature_flags::{BillingV2FeatureFlag, FeatureFlagAppExt};
+use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window};
+use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*};
use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions};
#[derive(IntoElement, RegisterComponent)]
pub struct AiUpsellCard {
- pub sign_in_status: SignInStatus,
- pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
- pub account_too_young: bool,
- pub user_plan: Option<Plan>,
- pub tab_index: Option<isize>,
+ sign_in_status: SignInStatus,
+ sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
+ account_too_young: bool,
+ user_plan: Option<Plan>,
+ tab_index: Option<isize>,
}
impl AiUpsellCard {
@@ -43,12 +41,18 @@ impl AiUpsellCard {
tab_index: None,
}
}
+
+ pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
+ self.tab_index = tab_index;
+ self
+ }
}
impl RenderOnce for AiUpsellCard {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
- let plan_definitions = PlanDefinitions;
- let young_account_banner = YoungAccountBanner;
+ let is_v2_plan = self
+ .user_plan
+ .map_or(cx.has_flag::<BillingV2FeatureFlag>(), |plan| plan.is_v2());
let pro_section = v_flex()
.flex_grow()
@@ -65,7 +69,7 @@ impl RenderOnce for AiUpsellCard {
)
.child(Divider::horizontal()),
)
- .child(plan_definitions.pro_plan(false));
+ .child(PlanDefinitions.pro_plan(is_v2_plan, false));
let free_section = v_flex()
.flex_grow()
@@ -82,12 +86,18 @@ impl RenderOnce for AiUpsellCard {
)
.child(Divider::horizontal()),
)
- .child(plan_definitions.free_plan());
+ .child(PlanDefinitions.free_plan(is_v2_plan));
- let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
- Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
- .color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
- );
+ let grid_bg = h_flex()
+ .absolute()
+ .inset_0()
+ .w_full()
+ .h(px(240.))
+ .bg(gpui::pattern_slash(
+ cx.theme().colors().border.opacity(0.1),
+ 2.,
+ 25.,
+ ));
let gradient_bg = div()
.absolute()
@@ -142,11 +152,7 @@ impl RenderOnce for AiUpsellCard {
rems_from_px(72.),
)
.color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3)))
- .with_animation(
- "loading_stamp",
- Animation::new(Duration::from_secs(10)).repeat(),
- |this, delta| this.transform(Transformation::rotate(percentage(delta))),
- ),
+ .with_rotate_animation(10),
);
let pro_trial_stamp = div()
@@ -165,11 +171,11 @@ impl RenderOnce for AiUpsellCard {
match self.sign_in_status {
SignInStatus::SignedIn => match self.user_plan {
- None | Some(Plan::ZedFree) => card
+ None | Some(Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree)) => card
.child(Label::new("Try Zed AI").size(LabelSize::Large))
.map(|this| {
if self.account_too_young {
- this.child(young_account_banner).child(
+ this.child(YoungAccountBanner).child(
v_flex()
.mt_2()
.gap_1()
@@ -184,7 +190,7 @@ impl RenderOnce for AiUpsellCard {
)
.child(Divider::horizontal()),
)
- .child(plan_definitions.pro_plan(true))
+ .child(PlanDefinitions.pro_plan(is_v2_plan, true))
.child(
Button::new("pro", "Get Started")
.full_width()
@@ -231,16 +237,17 @@ impl RenderOnce for AiUpsellCard {
)
}
}),
- Some(Plan::ZedProTrial) => card
- .child(pro_trial_stamp)
- .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
- .child(
- Label::new("Here's what you get for the next 14 days:")
- .color(Color::Muted)
- .mb_2(),
- )
- .child(plan_definitions.pro_trial(false)),
- Some(Plan::ZedPro) => card
+ Some(plan @ (Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial))) => {
+ card.child(pro_trial_stamp)
+ .child(Label::new("You're in the Zed Pro Trial").size(LabelSize::Large))
+ .child(
+ Label::new("Here's what you get for the next 14 days:")
+ .color(Color::Muted)
+ .mb_2(),
+ )
+ .child(PlanDefinitions.pro_trial(plan.is_v2(), false))
+ }
+ Some(plan @ (Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro))) => card
.child(certified_user_stamp)
.child(Label::new("You're in the Zed Pro plan").size(LabelSize::Large))
.child(
@@ -248,7 +255,7 @@ impl RenderOnce for AiUpsellCard {
.color(Color::Muted)
.mb_2(),
)
- .child(plan_definitions.pro_plan(false)),
+ .child(PlanDefinitions.pro_plan(plan.is_v2(), false)),
},
// Signed Out State
_ => card
@@ -320,7 +327,7 @@ impl Component for AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
account_too_young: false,
- user_plan: Some(Plan::ZedFree),
+ user_plan: Some(Plan::V1(PlanV1::ZedFree)),
tab_index: Some(1),
}
.into_any_element(),
@@ -331,7 +338,7 @@ impl Component for AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
account_too_young: true,
- user_plan: Some(Plan::ZedFree),
+ user_plan: Some(Plan::V1(PlanV1::ZedFree)),
tab_index: Some(1),
}
.into_any_element(),
@@ -342,7 +349,7 @@ impl Component for AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
account_too_young: false,
- user_plan: Some(Plan::ZedProTrial),
+ user_plan: Some(Plan::V1(PlanV1::ZedProTrial)),
tab_index: Some(1),
}
.into_any_element(),
@@ -353,7 +360,7 @@ impl Component for AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
account_too_young: false,
- user_plan: Some(Plan::ZedPro),
+ user_plan: Some(Plan::V1(PlanV1::ZedPro)),
tab_index: Some(1),
}
.into_any_element(),
@@ -1,6 +1,7 @@
use std::sync::Arc;
use client::{Client, UserStore};
+use cloud_llm_client::{Plan, PlanV1, PlanV2};
use gpui::{Entity, IntoElement, ParentElement};
use ui::prelude::*;
@@ -35,6 +36,10 @@ impl EditPredictionOnboarding {
impl Render for EditPredictionOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let is_free_plan = self.user_store.read(cx).plan().is_some_and(|plan| {
+ matches!(plan, Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))
+ });
+
let github_copilot = v_flex()
.gap_1()
.child(Label::new(if self.copilot_is_configured {
@@ -67,7 +72,8 @@ impl Render for EditPredictionOnboarding {
self.continue_with_zed_ai.clone(),
cx,
))
- .child(ui::Divider::horizontal())
- .child(github_copilot)
+ .when(is_free_plan, |this| {
+ this.child(ui::Divider::horizontal()).child(github_copilot)
+ })
}
}
@@ -7,13 +7,13 @@ pub struct PlanDefinitions;
impl PlanDefinitions {
pub const AI_DESCRIPTION: &'static str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI.";
- pub fn free_plan(&self) -> impl IntoElement {
+ pub fn free_plan(&self, _is_v2: bool) -> impl IntoElement {
List::new()
.child(ListBulletItem::new("50 prompts with Claude models"))
.child(ListBulletItem::new("2,000 accepted edit predictions"))
}
- pub fn pro_trial(&self, period: bool) -> impl IntoElement {
+ pub fn pro_trial(&self, _is_v2: bool, period: bool) -> impl IntoElement {
List::new()
.child(ListBulletItem::new("150 prompts with Claude models"))
.child(ListBulletItem::new(
@@ -26,7 +26,7 @@ impl PlanDefinitions {
})
}
- pub fn pro_plan(&self, price: bool) -> impl IntoElement {
+ pub fn pro_plan(&self, _is_v2: bool, price: bool) -> impl IntoElement {
List::new()
.child(ListBulletItem::new("500 prompts with Claude models"))
.child(ListBulletItem::new(
@@ -6,7 +6,7 @@ pub struct YoungAccountBanner;
impl RenderOnce for YoungAccountBanner {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
- const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing-support@zed.dev.";
+ const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for free plan usage or Pro plan free trial. To request an exception, reach out to billing-support@zed.dev.";
let label = div()
.w_full()
@@ -17,6 +17,6 @@ impl RenderOnce for YoungAccountBanner {
div()
.max_w_full()
.my_1()
- .child(Banner::new().severity(ui::Severity::Warning).child(label))
+ .child(Banner::new().severity(Severity::Warning).child(label))
}
}
@@ -363,17 +363,15 @@ pub async fn complete(
api_url: &str,
api_key: &str,
request: Request,
+ beta_headers: String,
) -> Result<Response, AnthropicError> {
let uri = format!("{api_url}/v1/messages");
- let beta_headers = Model::from_id(&request.model)
- .map(|model| model.beta_headers())
- .unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
- .header("X-Api-Key", api_key)
+ .header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json");
let serialized_request =
@@ -409,8 +407,9 @@ pub async fn stream_completion(
api_url: &str,
api_key: &str,
request: Request,
+ beta_headers: String,
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
- stream_completion_with_rate_limit_info(client, api_url, api_key, request)
+ stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
.await
.map(|output| output.0)
}
@@ -506,6 +505,7 @@ pub async fn stream_completion_with_rate_limit_info(
api_url: &str,
api_key: &str,
request: Request,
+ beta_headers: String,
) -> Result<
(
BoxStream<'static, Result<Event, AnthropicError>>,
@@ -518,15 +518,13 @@ pub async fn stream_completion_with_rate_limit_info(
stream: true,
};
let uri = format!("{api_url}/v1/messages");
- let beta_headers = Model::from_id(&request.base.model)
- .map(|model| model.beta_headers())
- .unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
+
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
- .header("X-Api-Key", api_key)
+ .header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json");
let serialized_request =
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
@@ -177,11 +177,11 @@ impl AskPassSession {
_ = askpass_opened_rx.fuse() => {
// Note: this await can only resolve after we are dropped.
askpass_kill_master_rx.await.ok();
- return AskPassResult::CancelledByUser
+ AskPassResult::CancelledByUser
}
_ = futures::FutureExt::fuse(smol::Timer::after(connection_timeout)) => {
- return AskPassResult::Timedout
+ AskPassResult::Timedout
}
}
}
@@ -215,7 +215,7 @@ pub fn main(socket: &str) {
}
#[cfg(target_os = "windows")]
- while buffer.last().map_or(false, |&b| b == b'\n' || b == b'\r') {
+ while buffer.last().is_some_and(|&b| b == b'\n' || b == b'\r') {
buffer.pop();
}
if buffer.last() != Some(&b'\0') {
@@ -50,8 +50,9 @@ text.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
-workspace-hack.workspace = true
workspace.workspace = true
+workspace-hack.workspace = true
+zed_env_vars.workspace = true
[dev-dependencies]
indoc.workspace = true
@@ -590,17 +590,16 @@ impl From<&Message> for MessageMetadata {
impl MessageMetadata {
pub fn is_cache_valid(&self, buffer: &BufferSnapshot, range: &Range<usize>) -> bool {
- let result = match &self.cache {
+ match &self.cache {
Some(MessageCacheMetadata { cached_at, .. }) => !buffer.has_edits_since_in_range(
- &cached_at,
+ cached_at,
Range {
start: buffer.anchor_at(range.start, Bias::Right),
end: buffer.anchor_at(range.end, Bias::Left),
},
),
_ => false,
- };
- result
+ }
}
}
@@ -1023,9 +1022,11 @@ impl AssistantContext {
summary: new_summary,
..
} => {
- if self.summary.timestamp().map_or(true, |current_timestamp| {
- new_summary.timestamp > current_timestamp
- }) {
+ if self
+ .summary
+ .timestamp()
+ .is_none_or(|current_timestamp| new_summary.timestamp > current_timestamp)
+ {
self.summary = ContextSummary::Content(new_summary);
summary_generated = true;
}
@@ -1076,20 +1077,20 @@ impl AssistantContext {
timestamp,
..
} => {
- if let Some(slash_command) = self.invoked_slash_commands.get_mut(&id) {
- if timestamp > slash_command.timestamp {
- slash_command.timestamp = timestamp;
- match error_message {
- Some(message) => {
- slash_command.status =
- InvokedSlashCommandStatus::Error(message.into());
- }
- None => {
- slash_command.status = InvokedSlashCommandStatus::Finished;
- }
+ if let Some(slash_command) = self.invoked_slash_commands.get_mut(&id)
+ && timestamp > slash_command.timestamp
+ {
+ slash_command.timestamp = timestamp;
+ match error_message {
+ Some(message) => {
+ slash_command.status =
+ InvokedSlashCommandStatus::Error(message.into());
+ }
+ None => {
+ slash_command.status = InvokedSlashCommandStatus::Finished;
}
- cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id });
}
+ cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id });
}
}
ContextOperation::BufferOperation(_) => unreachable!(),
@@ -1339,7 +1340,7 @@ impl AssistantContext {
let is_invalid = self
.messages_metadata
.get(&message_id)
- .map_or(true, |metadata| {
+ .is_none_or(|metadata| {
!metadata.is_cache_valid(&buffer, &message.offset_range)
|| *encountered_invalid
});
@@ -1368,10 +1369,10 @@ impl AssistantContext {
continue;
}
- if let Some(last_anchor) = last_anchor {
- if message.id == last_anchor {
- hit_last_anchor = true;
- }
+ if let Some(last_anchor) = last_anchor
+ && message.id == last_anchor
+ {
+ hit_last_anchor = true;
}
new_anchor_needs_caching = new_anchor_needs_caching
@@ -1406,14 +1407,14 @@ impl AssistantContext {
if !self.pending_completions.is_empty() {
return;
}
- if let Some(cache_configuration) = cache_configuration {
- if !cache_configuration.should_speculate {
- return;
- }
+ if let Some(cache_configuration) = cache_configuration
+ && !cache_configuration.should_speculate
+ {
+ return;
}
let request = {
- let mut req = self.to_completion_request(Some(&model), cx);
+ let mut req = self.to_completion_request(Some(model), cx);
// Skip the last message because it's likely to change and
// therefore would be a waste to cache.
req.messages.pop();
@@ -1428,7 +1429,7 @@ impl AssistantContext {
let model = Arc::clone(model);
self.pending_cache_warming_task = cx.spawn(async move |this, cx| {
async move {
- match model.stream_completion(request, &cx).await {
+ match model.stream_completion(request, cx).await {
Ok(mut stream) => {
stream.next().await;
log::info!("Cache warming completed successfully");
@@ -1552,25 +1553,24 @@ impl AssistantContext {
})
.map(ToOwned::to_owned)
.collect::<SmallVec<_>>();
- if let Some(command) = self.slash_commands.command(name, cx) {
- if !command.requires_argument() || !arguments.is_empty() {
- let start_ix = offset + command_line.name.start - 1;
- let end_ix = offset
- + command_line
- .arguments
- .last()
- .map_or(command_line.name.end, |argument| argument.end);
- let source_range =
- buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
- let pending_command = ParsedSlashCommand {
- name: name.to_string(),
- arguments,
- source_range,
- status: PendingSlashCommandStatus::Idle,
- };
- updated.push(pending_command.clone());
- new_commands.push(pending_command);
- }
+ if let Some(command) = self.slash_commands.command(name, cx)
+ && (!command.requires_argument() || !arguments.is_empty())
+ {
+ let start_ix = offset + command_line.name.start - 1;
+ let end_ix = offset
+ + command_line
+ .arguments
+ .last()
+ .map_or(command_line.name.end, |argument| argument.end);
+ let source_range = buffer.anchor_after(start_ix)..buffer.anchor_after(end_ix);
+ let pending_command = ParsedSlashCommand {
+ name: name.to_string(),
+ arguments,
+ source_range,
+ status: PendingSlashCommandStatus::Idle,
+ };
+ updated.push(pending_command.clone());
+ new_commands.push(pending_command);
}
}
@@ -1661,12 +1661,12 @@ impl AssistantContext {
) -> Range<usize> {
let buffer = self.buffer.read(cx);
let start_ix = match all_annotations
- .binary_search_by(|probe| probe.range().end.cmp(&range.start, &buffer))
+ .binary_search_by(|probe| probe.range().end.cmp(&range.start, buffer))
{
Ok(ix) | Err(ix) => ix,
};
let end_ix = match all_annotations
- .binary_search_by(|probe| probe.range().start.cmp(&range.end, &buffer))
+ .binary_search_by(|probe| probe.range().start.cmp(&range.end, buffer))
{
Ok(ix) => ix + 1,
Err(ix) => ix,
@@ -1799,14 +1799,13 @@ impl AssistantContext {
});
let end = this.buffer.read(cx).anchor_before(insert_position);
- if run_commands_in_text {
- if let Some(invoked_slash_command) =
+ if run_commands_in_text
+ && let Some(invoked_slash_command) =
this.invoked_slash_commands.get_mut(&command_id)
- {
- invoked_slash_command
- .run_commands_in_ranges
- .push(start..end);
- }
+ {
+ invoked_slash_command
+ .run_commands_in_ranges
+ .push(start..end);
}
}
SlashCommandEvent::EndSection => {
@@ -1862,7 +1861,7 @@ impl AssistantContext {
{
let newline_offset = insert_position.saturating_sub(1);
if buffer.contains_str_at(newline_offset, "\n")
- && last_section_range.map_or(true, |last_section_range| {
+ && last_section_range.is_none_or(|last_section_range| {
!last_section_range
.to_offset(buffer)
.contains(&newline_offset)
@@ -2045,7 +2044,7 @@ impl AssistantContext {
let task = cx.spawn({
async move |this, cx| {
- let stream = model.stream_completion(request, &cx);
+ let stream = model.stream_completion(request, cx);
let assistant_message_id = assistant_message.id;
let mut response_latency = None;
let stream_completion = async {
@@ -2081,15 +2080,12 @@ impl AssistantContext {
match event {
LanguageModelCompletionEvent::StatusUpdate(status_update) => {
- match status_update {
- CompletionRequestStatus::UsageUpdated { amount, limit } => {
- this.update_model_request_usage(
- amount as u32,
- limit,
- cx,
- );
- }
- _ => {}
+ if let CompletionRequestStatus::UsageUpdated { amount, limit } = status_update {
+ this.update_model_request_usage(
+ amount as u32,
+ limit,
+ cx,
+ );
}
}
LanguageModelCompletionEvent::StartMessage { .. } => {}
@@ -2286,7 +2282,7 @@ impl AssistantContext {
let mut contents = self.contents(cx).peekable();
fn collect_text_content(buffer: &Buffer, range: Range<usize>) -> Option<String> {
- let text: String = buffer.text_for_range(range.clone()).collect();
+ let text: String = buffer.text_for_range(range).collect();
if text.trim().is_empty() {
None
} else {
@@ -2315,10 +2311,7 @@ impl AssistantContext {
let mut request_message = LanguageModelRequestMessage {
role: message.role,
content: Vec::new(),
- cache: message
- .cache
- .as_ref()
- .map_or(false, |cache| cache.is_anchor),
+ cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor),
};
while let Some(content) = contents.peek() {
@@ -2708,7 +2701,7 @@ impl AssistantContext {
self.summary_task = cx.spawn(async move |this, cx| {
let result = async {
- let stream = model.model.stream_completion_text(request, &cx);
+ let stream = model.model.stream_completion_text(request, cx);
let mut messages = stream.await?;
let mut replaced = !replace_old;
@@ -2741,10 +2734,10 @@ impl AssistantContext {
}
this.read_with(cx, |this, _cx| {
- if let Some(summary) = this.summary.content() {
- if summary.text.is_empty() {
- bail!("Model generated an empty summary");
- }
+ if let Some(summary) = this.summary.content()
+ && summary.text.is_empty()
+ {
+ bail!("Model generated an empty summary");
}
Ok(())
})??;
@@ -2799,7 +2792,7 @@ impl AssistantContext {
let mut current_message = messages.next();
while let Some(offset) = offsets.next() {
// Locate the message that contains the offset.
- while current_message.as_ref().map_or(false, |message| {
+ while current_message.as_ref().is_some_and(|message| {
!message.offset_range.contains(&offset) && messages.peek().is_some()
}) {
current_message = messages.next();
@@ -2809,7 +2802,7 @@ impl AssistantContext {
};
// Skip offsets that are in the same message.
- while offsets.peek().map_or(false, |offset| {
+ while offsets.peek().is_some_and(|offset| {
message.offset_range.contains(offset) || messages.peek().is_none()
}) {
offsets.next();
@@ -2924,18 +2917,18 @@ impl AssistantContext {
fs.create_dir(contexts_dir().as_ref()).await?;
// rename before write ensures that only one file exists
- if let Some(old_path) = old_path.as_ref() {
- if new_path.as_path() != old_path.as_ref() {
- fs.rename(
- &old_path,
- &new_path,
- RenameOptions {
- overwrite: true,
- ignore_if_exists: true,
- },
- )
- .await?;
- }
+ if let Some(old_path) = old_path.as_ref()
+ && new_path.as_path() != old_path.as_ref()
+ {
+ fs.rename(
+ old_path,
+ &new_path,
+ RenameOptions {
+ overwrite: true,
+ ignore_if_exists: true,
+ },
+ )
+ .await?;
}
// update path before write in case it fails
@@ -764,7 +764,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
let network = Arc::new(Mutex::new(Network::new(rng.clone())));
let mut contexts = Vec::new();
- let num_peers = rng.gen_range(min_peers..=max_peers);
+ let num_peers = rng.random_range(min_peers..=max_peers);
let context_id = ContextId::new();
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
for i in 0..num_peers {
@@ -806,10 +806,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|| !network.lock().is_idle()
|| network.lock().contains_disconnected_peers()
{
- let context_index = rng.gen_range(0..contexts.len());
+ let context_index = rng.random_range(0..contexts.len());
let context = &contexts[context_index];
- match rng.gen_range(0..100) {
+ match rng.random_range(0..100) {
0..=29 if mutation_count > 0 => {
log::info!("Context {}: edit buffer", context_index);
context.update(cx, |context, cx| {
@@ -874,10 +874,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
merge_same_roles: true,
})];
- let num_sections = rng.gen_range(0..=3);
+ let num_sections = rng.random_range(0..=3);
let mut section_start = 0;
for _ in 0..num_sections {
- let mut section_end = rng.gen_range(section_start..=output_text.len());
+ let mut section_end = rng.random_range(section_start..=output_text.len());
while !output_text.is_char_boundary(section_end) {
section_end += 1;
}
@@ -924,7 +924,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
75..=84 if mutation_count > 0 => {
context.update(cx, |context, cx| {
if let Some(message) = context.messages(cx).choose(&mut rng) {
- let new_status = match rng.gen_range(0..3) {
+ let new_status = match rng.random_range(0..3) {
0 => MessageStatus::Done,
1 => MessageStatus::Pending,
_ => MessageStatus::Error(SharedString::from("Random error")),
@@ -971,7 +971,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
network.lock().broadcast(replica_id, ops_to_send);
context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx));
- } else if rng.gen_bool(0.1) && replica_id != 0 {
+ } else if rng.random_bool(0.1) && replica_id != 0 {
log::info!("Context {}: disconnecting", context_index);
network.lock().disconnect_peer(replica_id);
} else if network.lock().has_unreceived(replica_id) {
@@ -1055,7 +1055,7 @@ fn test_mark_cache_anchors(cx: &mut App) {
assert_eq!(
messages_cache(&context, cx)
.iter()
- .filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
+ .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
.count(),
0,
"Empty messages should not have any cache anchors."
@@ -1083,7 +1083,7 @@ fn test_mark_cache_anchors(cx: &mut App) {
assert_eq!(
messages_cache(&context, cx)
.iter()
- .filter(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
+ .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
.count(),
0,
"Messages should not be marked for cache before going over the token minimum."
@@ -1098,7 +1098,7 @@ fn test_mark_cache_anchors(cx: &mut App) {
assert_eq!(
messages_cache(&context, cx)
.iter()
- .map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
+ .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
.collect::<Vec<bool>>(),
vec![true, true, false],
"Last message should not be an anchor on speculative request."
@@ -1116,7 +1116,7 @@ fn test_mark_cache_anchors(cx: &mut App) {
assert_eq!(
messages_cache(&context, cx)
.iter()
- .map(|(_, cache)| cache.as_ref().map_or(false, |cache| cache.is_anchor))
+ .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor))
.collect::<Vec<bool>>(),
vec![false, true, true, false],
"Most recent message should also be cached if not a speculative request."
@@ -1300,7 +1300,7 @@ fn test_summarize_error(
context.assist(cx);
});
- simulate_successful_response(&model, cx);
+ simulate_successful_response(model, cx);
context.read_with(cx, |context, _| {
assert!(!context.summary().content().unwrap().done);
@@ -1321,7 +1321,7 @@ fn test_summarize_error(
fn setup_context_editor_with_fake_model(
cx: &mut TestAppContext,
) -> (Entity<AssistantContext>, Arc<FakeLanguageModel>) {
- let registry = Arc::new(LanguageRegistry::test(cx.executor().clone()));
+ let registry = Arc::new(LanguageRegistry::test(cx.executor()));
let fake_provider = Arc::new(FakeLanguageModelProvider::default());
let fake_model = Arc::new(fake_provider.test_model());
@@ -1376,7 +1376,7 @@ fn messages_cache(
context
.read(cx)
.messages(cx)
- .map(|message| (message.id, message.cache.clone()))
+ .map(|message| (message.id, message.cache))
.collect()
}
@@ -1436,6 +1436,6 @@ impl SlashCommand for FakeSlashCommand {
sections: vec![],
run_commands_in_text: false,
}
- .to_event_stream()))
+ .into_event_stream()))
}
}
@@ -24,6 +24,7 @@ use rpc::AnyProtoClient;
use std::sync::LazyLock;
use std::{cmp::Reverse, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration};
use util::{ResultExt, TryFutureExt};
+use zed_env_vars::ZED_STATELESS;
pub(crate) fn init(client: &AnyProtoClient) {
client.add_entity_message_handler(ContextStore::handle_advertise_contexts);
@@ -320,7 +321,7 @@ impl ContextStore {
.client
.subscribe_to_entity(remote_id)
.log_err()
- .map(|subscription| subscription.set_entity(&cx.entity(), &mut cx.to_async()));
+ .map(|subscription| subscription.set_entity(&cx.entity(), &cx.to_async()));
self.advertise_contexts(cx);
} else {
self.client_subscription = None;
@@ -788,8 +789,6 @@ impl ContextStore {
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(async move |this, cx| {
- pub static ZED_STATELESS: LazyLock<bool> =
- LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
if *ZED_STATELESS {
return Ok(());
}
@@ -862,7 +861,7 @@ impl ContextStore {
ContextServerStatus::Running => {
self.load_context_server_slash_commands(
server_id.clone(),
- context_server_store.clone(),
+ context_server_store,
cx,
);
}
@@ -894,34 +893,33 @@ impl ContextStore {
return;
};
- if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
- if let Some(response) = protocol
+ if protocol.capable(context_server::protocol::ServerCapability::Prompts)
+ && let Some(response) = protocol
.request::<context_server::types::requests::PromptsList>(())
.await
.log_err()
- {
- let slash_command_ids = response
- .prompts
- .into_iter()
- .filter(assistant_slash_commands::acceptable_prompt)
- .map(|prompt| {
- log::info!("registering context server command: {:?}", prompt.name);
- slash_command_working_set.insert(Arc::new(
- assistant_slash_commands::ContextServerSlashCommand::new(
- context_server_store.clone(),
- server.id(),
- prompt,
- ),
- ))
- })
- .collect::<Vec<_>>();
-
- this.update(cx, |this, _cx| {
- this.context_server_slash_command_ids
- .insert(server_id.clone(), slash_command_ids);
+ {
+ let slash_command_ids = response
+ .prompts
+ .into_iter()
+ .filter(assistant_slash_commands::acceptable_prompt)
+ .map(|prompt| {
+ log::info!("registering context server command: {:?}", prompt.name);
+ slash_command_working_set.insert(Arc::new(
+ assistant_slash_commands::ContextServerSlashCommand::new(
+ context_server_store.clone(),
+ server.id(),
+ prompt,
+ ),
+ ))
})
- .log_err();
- }
+ .collect::<Vec<_>>();
+
+ this.update(cx, |this, _cx| {
+ this.context_server_slash_command_ids
+ .insert(server_id.clone(), slash_command_ids);
+ })
+ .log_err();
}
})
.detach();
@@ -161,7 +161,7 @@ impl SlashCommandOutput {
}
/// Returns this [`SlashCommandOutput`] as a stream of [`SlashCommandEvent`]s.
- pub fn to_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> {
+ pub fn into_event_stream(mut self) -> BoxStream<'static, Result<SlashCommandEvent>> {
self.ensure_valid_section_ranges();
let mut events = Vec::new();
@@ -363,7 +363,7 @@ mod tests {
run_commands_in_text: false,
};
- let events = output.clone().to_event_stream().collect::<Vec<_>>().await;
+ let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
let events = events
.into_iter()
.filter_map(|event| event.ok())
@@ -386,7 +386,7 @@ mod tests {
);
let new_output =
- SlashCommandOutput::from_event_stream(output.clone().to_event_stream())
+ SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
.await
.unwrap();
@@ -415,7 +415,7 @@ mod tests {
run_commands_in_text: false,
};
- let events = output.clone().to_event_stream().collect::<Vec<_>>().await;
+ let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
let events = events
.into_iter()
.filter_map(|event| event.ok())
@@ -452,7 +452,7 @@ mod tests {
);
let new_output =
- SlashCommandOutput::from_event_stream(output.clone().to_event_stream())
+ SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
.await
.unwrap();
@@ -493,7 +493,7 @@ mod tests {
run_commands_in_text: false,
};
- let events = output.clone().to_event_stream().collect::<Vec<_>>().await;
+ let events = output.clone().into_event_stream().collect::<Vec<_>>().await;
let events = events
.into_iter()
.filter_map(|event| event.ok())
@@ -562,7 +562,7 @@ mod tests {
);
let new_output =
- SlashCommandOutput::from_event_stream(output.clone().to_event_stream())
+ SlashCommandOutput::from_event_stream(output.clone().into_event_stream())
.await
.unwrap();
@@ -166,7 +166,7 @@ impl SlashCommand for ExtensionSlashCommand {
.collect(),
run_commands_in_text: false,
}
- .to_event_stream())
+ .into_event_stream())
})
}
}
@@ -27,7 +27,6 @@ globset.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
-indexed_docs.workspace = true
language.workspace = true
project.workspace = true
prompt_store.workspace = true
@@ -3,7 +3,6 @@ mod context_server_command;
mod default_command;
mod delta_command;
mod diagnostics_command;
-mod docs_command;
mod fetch_command;
mod file_command;
mod now_command;
@@ -18,7 +17,6 @@ pub use crate::context_server_command::*;
pub use crate::default_command::*;
pub use crate::delta_command::*;
pub use crate::diagnostics_command::*;
-pub use crate::docs_command::*;
pub use crate::fetch_command::*;
pub use crate::file_command::*;
pub use crate::now_command::*;
@@ -150,7 +150,7 @@ impl SlashCommand for CargoWorkspaceSlashCommand {
}],
run_commands_in_text: false,
}
- .to_event_stream())
+ .into_event_stream())
})
});
output.unwrap_or_else(|error| Task::ready(Err(error)))
@@ -39,12 +39,12 @@ impl SlashCommand for ContextServerSlashCommand {
fn label(&self, cx: &App) -> language::CodeLabel {
let mut parts = vec![self.prompt.name.as_str()];
- if let Some(args) = &self.prompt.arguments {
- if let Some(arg) = args.first() {
- parts.push(arg.name.as_str());
- }
+ if let Some(args) = &self.prompt.arguments
+ && let Some(arg) = args.first()
+ {
+ parts.push(arg.name.as_str());
}
- create_label_for_command(&parts[0], &parts[1..], cx)
+ create_label_for_command(parts[0], &parts[1..], cx)
}
fn description(&self) -> String {
@@ -62,9 +62,10 @@ impl SlashCommand for ContextServerSlashCommand {
}
fn requires_argument(&self) -> bool {
- self.prompt.arguments.as_ref().map_or(false, |args| {
- args.iter().any(|arg| arg.required == Some(true))
- })
+ self.prompt
+ .arguments
+ .as_ref()
+ .is_some_and(|args| args.iter().any(|arg| arg.required == Some(true)))
}
fn complete_argument(
@@ -190,7 +191,7 @@ impl SlashCommand for ContextServerSlashCommand {
text: prompt,
run_commands_in_text: false,
}
- .to_event_stream())
+ .into_event_stream())
})
} else {
Task::ready(Err(anyhow!("Context server not found")))
@@ -85,7 +85,7 @@ impl SlashCommand for DefaultSlashCommand {
text,
run_commands_in_text: true,
}
- .to_event_stream())
+ .into_event_stream())
})
}
}
@@ -66,23 +66,22 @@ impl SlashCommand for DeltaSlashCommand {
.metadata
.as_ref()
.and_then(|value| serde_json::from_value::<FileCommandMetadata>(value.clone()).ok())
+ && paths.insert(metadata.path.clone())
{
- if paths.insert(metadata.path.clone()) {
- file_command_old_outputs.push(
- context_buffer
- .as_rope()
- .slice(section.range.to_offset(&context_buffer)),
- );
- file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
- std::slice::from_ref(&metadata.path),
- context_slash_command_output_sections,
- context_buffer.clone(),
- workspace.clone(),
- delegate.clone(),
- window,
- cx,
- ));
- }
+ file_command_old_outputs.push(
+ context_buffer
+ .as_rope()
+ .slice(section.range.to_offset(&context_buffer)),
+ );
+ file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
+ std::slice::from_ref(&metadata.path),
+ context_slash_command_output_sections,
+ context_buffer.clone(),
+ workspace.clone(),
+ delegate.clone(),
+ window,
+ cx,
+ ));
}
}
@@ -95,31 +94,31 @@ impl SlashCommand for DeltaSlashCommand {
.into_iter()
.zip(file_command_new_outputs)
{
- if let Ok(new_output) = new_output {
- if let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await
- {
- if let Some(file_command_range) = new_output.sections.first() {
- let new_text = &new_output.text[file_command_range.range.clone()];
- if old_text.chars().ne(new_text.chars()) {
- changes_detected = true;
- output.sections.extend(new_output.sections.into_iter().map(
- |section| SlashCommandOutputSection {
- range: output.text.len() + section.range.start
- ..output.text.len() + section.range.end,
- icon: section.icon,
- label: section.label,
- metadata: section.metadata,
- },
- ));
- output.text.push_str(&new_output.text);
- }
- }
+ if let Ok(new_output) = new_output
+ && let Ok(new_output) = SlashCommandOutput::from_event_stream(new_output).await
+ && let Some(file_command_range) = new_output.sections.first()
+ {
+ let new_text = &new_output.text[file_command_range.range.clone()];
+ if old_text.chars().ne(new_text.chars()) {
+ changes_detected = true;
+ output
+ .sections
+ .extend(new_output.sections.into_iter().map(|section| {
+ SlashCommandOutputSection {
+ range: output.text.len() + section.range.start
+ ..output.text.len() + section.range.end,
+ icon: section.icon,
+ label: section.label,
+ metadata: section.metadata,
+ }
+ }));
+ output.text.push_str(&new_output.text);
}
}
}
anyhow::ensure!(changes_detected, "no new changes detected");
- Ok(output.to_event_stream())
+ Ok(output.into_event_stream())
})
}
}
@@ -44,7 +44,7 @@ impl DiagnosticsSlashCommand {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
- path: entry.path.clone(),
+ path: entry.path,
path_prefix: path_prefix.clone(),
is_dir: false, // Diagnostics can't be produced for directories
distance_to_relative_ancestor: 0,
@@ -61,7 +61,7 @@ impl DiagnosticsSlashCommand {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
- .map_or(false, |entry| entry.is_ignored),
+ .is_some_and(|entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Entries,
}
@@ -189,7 +189,7 @@ impl SlashCommand for DiagnosticsSlashCommand {
window.spawn(cx, async move |_| {
task.await?
- .map(|output| output.to_event_stream())
+ .map(|output| output.into_event_stream())
.context("No diagnostics found")
})
}
@@ -249,7 +249,7 @@ fn collect_diagnostics(
let worktree = worktree.read(cx);
let worktree_root_path = Path::new(worktree.root_name());
let relative_path = path.strip_prefix(worktree_root_path).ok()?;
- worktree.absolutize(&relative_path).ok()
+ worktree.absolutize(relative_path).ok()
})
})
.is_some()
@@ -280,10 +280,10 @@ fn collect_diagnostics(
let mut project_summary = DiagnosticSummary::default();
for (project_path, path, summary) in diagnostic_summaries {
- if let Some(path_matcher) = &options.path_matcher {
- if !path_matcher.is_match(&path) {
- continue;
- }
+ if let Some(path_matcher) = &options.path_matcher
+ && !path_matcher.is_match(&path)
+ {
+ continue;
}
project_summary.error_count += summary.error_count;
@@ -365,7 +365,7 @@ pub fn collect_buffer_diagnostics(
) {
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
- collect_diagnostic(output, entry, &snapshot, include_warnings)
+ collect_diagnostic(output, entry, snapshot, include_warnings)
}
}
@@ -396,7 +396,7 @@ fn collect_diagnostic(
let start_row = range.start.row.saturating_sub(EXCERPT_EXPANSION_SIZE);
let end_row = (range.end.row + EXCERPT_EXPANSION_SIZE).min(snapshot.max_point().row) + 1;
let excerpt_range =
- Point::new(start_row, 0).to_offset(&snapshot)..Point::new(end_row, 0).to_offset(&snapshot);
+ Point::new(start_row, 0).to_offset(snapshot)..Point::new(end_row, 0).to_offset(snapshot);
output.text.push_str("```");
if let Some(language_name) = snapshot.language().map(|l| l.code_fence_block_name()) {
@@ -1,543 +0,0 @@
-use std::path::Path;
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-use std::time::Duration;
-
-use anyhow::{Context as _, Result, anyhow, bail};
-use assistant_slash_command::{
- ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
- SlashCommandResult,
-};
-use gpui::{App, BackgroundExecutor, Entity, Task, WeakEntity};
-use indexed_docs::{
- DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
- ProviderId,
-};
-use language::{BufferSnapshot, LspAdapterDelegate};
-use project::{Project, ProjectPath};
-use ui::prelude::*;
-use util::{ResultExt, maybe};
-use workspace::Workspace;
-
-pub struct DocsSlashCommand;
-
-impl DocsSlashCommand {
- pub const NAME: &'static str = "docs";
-
- fn path_to_cargo_toml(project: Entity<Project>, cx: &mut App) -> Option<Arc<Path>> {
- let worktree = project.read(cx).worktrees(cx).next()?;
- let worktree = worktree.read(cx);
- let entry = worktree.entry_for_path("Cargo.toml")?;
- let path = ProjectPath {
- worktree_id: worktree.id(),
- path: entry.path.clone(),
- };
- Some(Arc::from(
- project.read(cx).absolute_path(&path, cx)?.as_path(),
- ))
- }
-
- /// Ensures that the indexed doc providers for Rust are registered.
- ///
- /// Ideally we would do this sooner, but we need to wait until we're able to
- /// access the workspace so we can read the project.
- fn ensure_rust_doc_providers_are_registered(
- &self,
- workspace: Option<WeakEntity<Workspace>>,
- cx: &mut App,
- ) {
- let indexed_docs_registry = IndexedDocsRegistry::global(cx);
- if indexed_docs_registry
- .get_provider_store(LocalRustdocProvider::id())
- .is_none()
- {
- let index_provider_deps = maybe!({
- let workspace = workspace
- .as_ref()
- .context("no workspace")?
- .upgrade()
- .context("workspace dropped")?;
- let project = workspace.read(cx).project().clone();
- let fs = project.read(cx).fs().clone();
- let cargo_workspace_root = Self::path_to_cargo_toml(project, cx)
- .and_then(|path| path.parent().map(|path| path.to_path_buf()))
- .context("no Cargo workspace root found")?;
-
- anyhow::Ok((fs, cargo_workspace_root))
- });
-
- if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() {
- indexed_docs_registry.register_provider(Box::new(LocalRustdocProvider::new(
- fs,
- cargo_workspace_root,
- )));
- }
- }
-
- if indexed_docs_registry
- .get_provider_store(DocsDotRsProvider::id())
- .is_none()
- {
- let http_client = maybe!({
- let workspace = workspace
- .as_ref()
- .context("no workspace")?
- .upgrade()
- .context("workspace was dropped")?;
- let project = workspace.read(cx).project().clone();
- anyhow::Ok(project.read(cx).client().http_client())
- });
-
- if let Some(http_client) = http_client.log_err() {
- indexed_docs_registry
- .register_provider(Box::new(DocsDotRsProvider::new(http_client)));
- }
- }
- }
-
- /// Runs just-in-time indexing for a given package, in case the slash command
- /// is run without any entries existing in the index.
- fn run_just_in_time_indexing(
- store: Arc<IndexedDocsStore>,
- key: String,
- package: PackageName,
- executor: BackgroundExecutor,
- ) -> Task<()> {
- executor.clone().spawn(async move {
- let (prefix, needs_full_index) = if let Some((prefix, _)) = key.split_once('*') {
- // If we have a wildcard in the search, we want to wait until
- // we've completely finished indexing so we get a full set of
- // results for the wildcard.
- (prefix.to_string(), true)
- } else {
- (key, false)
- };
-
- // If we already have some entries, we assume that we've indexed the package before
- // and don't need to do it again.
- let has_any_entries = store
- .any_with_prefix(prefix.clone())
- .await
- .unwrap_or_default();
- if has_any_entries {
- return ();
- };
-
- let index_task = store.clone().index(package.clone());
-
- if needs_full_index {
- _ = index_task.await;
- } else {
- loop {
- executor.timer(Duration::from_millis(200)).await;
-
- if store
- .any_with_prefix(prefix.clone())
- .await
- .unwrap_or_default()
- || !store.is_indexing(&package)
- {
- break;
- }
- }
- }
- })
- }
-}
-
-impl SlashCommand for DocsSlashCommand {
- fn name(&self) -> String {
- Self::NAME.into()
- }
-
- fn description(&self) -> String {
- "insert docs".into()
- }
-
- fn menu_text(&self) -> String {
- "Insert Documentation".into()
- }
-
- fn requires_argument(&self) -> bool {
- true
- }
-
- fn complete_argument(
- self: Arc<Self>,
- arguments: &[String],
- _cancel: Arc<AtomicBool>,
- workspace: Option<WeakEntity<Workspace>>,
- _: &mut Window,
- cx: &mut App,
- ) -> Task<Result<Vec<ArgumentCompletion>>> {
- self.ensure_rust_doc_providers_are_registered(workspace, cx);
-
- let indexed_docs_registry = IndexedDocsRegistry::global(cx);
- let args = DocsSlashCommandArgs::parse(arguments);
- let store = args
- .provider()
- .context("no docs provider specified")
- .and_then(|provider| IndexedDocsStore::try_global(provider, cx));
- cx.background_spawn(async move {
- fn build_completions(items: Vec<String>) -> Vec<ArgumentCompletion> {
- items
- .into_iter()
- .map(|item| ArgumentCompletion {
- label: item.clone().into(),
- new_text: item.to_string(),
- after_completion: assistant_slash_command::AfterCompletion::Run,
- replace_previous_arguments: false,
- })
- .collect()
- }
-
- match args {
- DocsSlashCommandArgs::NoProvider => {
- let providers = indexed_docs_registry.list_providers();
- if providers.is_empty() {
- return Ok(vec![ArgumentCompletion {
- label: "No available docs providers.".into(),
- new_text: String::new(),
- after_completion: false.into(),
- replace_previous_arguments: false,
- }]);
- }
-
- Ok(providers
- .into_iter()
- .map(|provider| ArgumentCompletion {
- label: provider.to_string().into(),
- new_text: provider.to_string(),
- after_completion: false.into(),
- replace_previous_arguments: false,
- })
- .collect())
- }
- DocsSlashCommandArgs::SearchPackageDocs {
- provider,
- package,
- index,
- } => {
- let store = store?;
-
- if index {
- // We don't need to hold onto this task, as the `IndexedDocsStore` will hold it
- // until it completes.
- drop(store.clone().index(package.as_str().into()));
- }
-
- let suggested_packages = store.clone().suggest_packages().await?;
- let search_results = store.search(package).await;
-
- let mut items = build_completions(search_results);
- let workspace_crate_completions = suggested_packages
- .into_iter()
- .filter(|package_name| {
- !items
- .iter()
- .any(|item| item.label.text() == package_name.as_ref())
- })
- .map(|package_name| ArgumentCompletion {
- label: format!("{package_name} (unindexed)").into(),
- new_text: format!("{package_name}"),
- after_completion: true.into(),
- replace_previous_arguments: false,
- })
- .collect::<Vec<_>>();
- items.extend(workspace_crate_completions);
-
- if items.is_empty() {
- return Ok(vec![ArgumentCompletion {
- label: format!(
- "Enter a {package_term} name.",
- package_term = package_term(&provider)
- )
- .into(),
- new_text: provider.to_string(),
- after_completion: false.into(),
- replace_previous_arguments: false,
- }]);
- }
-
- Ok(items)
- }
- DocsSlashCommandArgs::SearchItemDocs { item_path, .. } => {
- let store = store?;
- let items = store.search(item_path).await;
- Ok(build_completions(items))
- }
- }
- })
- }
-
- fn run(
- self: Arc<Self>,
- arguments: &[String],
- _context_slash_command_output_sections: &[SlashCommandOutputSection<language::Anchor>],
- _context_buffer: BufferSnapshot,
- _workspace: WeakEntity<Workspace>,
- _delegate: Option<Arc<dyn LspAdapterDelegate>>,
- _: &mut Window,
- cx: &mut App,
- ) -> Task<SlashCommandResult> {
- if arguments.is_empty() {
- return Task::ready(Err(anyhow!("missing an argument")));
- };
-
- let args = DocsSlashCommandArgs::parse(arguments);
- let executor = cx.background_executor().clone();
- let task = cx.background_spawn({
- let store = args
- .provider()
- .context("no docs provider specified")
- .and_then(|provider| IndexedDocsStore::try_global(provider, cx));
- async move {
- let (provider, key) = match args.clone() {
- DocsSlashCommandArgs::NoProvider => bail!("no docs provider specified"),
- DocsSlashCommandArgs::SearchPackageDocs {
- provider, package, ..
- } => (provider, package),
- DocsSlashCommandArgs::SearchItemDocs {
- provider,
- item_path,
- ..
- } => (provider, item_path),
- };
-
- if key.trim().is_empty() {
- bail!(
- "no {package_term} name provided",
- package_term = package_term(&provider)
- );
- }
-
- let store = store?;
-
- if let Some(package) = args.package() {
- Self::run_just_in_time_indexing(store.clone(), key.clone(), package, executor)
- .await;
- }
-
- let (text, ranges) = if let Some((prefix, _)) = key.split_once('*') {
- let docs = store.load_many_by_prefix(prefix.to_string()).await?;
-
- let mut text = String::new();
- let mut ranges = Vec::new();
-
- for (key, docs) in docs {
- let prev_len = text.len();
-
- text.push_str(&docs.0);
- text.push_str("\n");
- ranges.push((key, prev_len..text.len()));
- text.push_str("\n");
- }
-
- (text, ranges)
- } else {
- let item_docs = store.load(key.clone()).await?;
- let text = item_docs.to_string();
- let range = 0..text.len();
-
- (text, vec![(key, range)])
- };
-
- anyhow::Ok((provider, text, ranges))
- }
- });
-
- cx.foreground_executor().spawn(async move {
- let (provider, text, ranges) = task.await?;
- Ok(SlashCommandOutput {
- text,
- sections: ranges
- .into_iter()
- .map(|(key, range)| SlashCommandOutputSection {
- range,
- icon: IconName::FileDoc,
- label: format!("docs ({provider}): {key}",).into(),
- metadata: None,
- })
- .collect(),
- run_commands_in_text: false,
- }
- .to_event_stream())
- })
- }
-}
-
-fn is_item_path_delimiter(char: char) -> bool {
- !char.is_alphanumeric() && char != '-' && char != '_'
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub enum DocsSlashCommandArgs {
- NoProvider,
- SearchPackageDocs {
- provider: ProviderId,
- package: String,
- index: bool,
- },
- SearchItemDocs {
- provider: ProviderId,
- package: String,
- item_path: String,
- },
-}
-
-impl DocsSlashCommandArgs {
- pub fn parse(arguments: &[String]) -> Self {
- let Some(provider) = arguments
- .get(0)
- .cloned()
- .filter(|arg| !arg.trim().is_empty())
- else {
- return Self::NoProvider;
- };
- let provider = ProviderId(provider.into());
- let Some(argument) = arguments.get(1) else {
- return Self::NoProvider;
- };
-
- if let Some((package, rest)) = argument.split_once(is_item_path_delimiter) {
- if rest.trim().is_empty() {
- Self::SearchPackageDocs {
- provider,
- package: package.to_owned(),
- index: true,
- }
- } else {
- Self::SearchItemDocs {
- provider,
- package: package.to_owned(),
- item_path: argument.to_owned(),
- }
- }
- } else {
- Self::SearchPackageDocs {
- provider,
- package: argument.to_owned(),
- index: false,
- }
- }
- }
-
- pub fn provider(&self) -> Option<ProviderId> {
- match self {
- Self::NoProvider => None,
- Self::SearchPackageDocs { provider, .. } | Self::SearchItemDocs { provider, .. } => {
- Some(provider.clone())
- }
- }
- }
-
- pub fn package(&self) -> Option<PackageName> {
- match self {
- Self::NoProvider => None,
- Self::SearchPackageDocs { package, .. } | Self::SearchItemDocs { package, .. } => {
- Some(package.as_str().into())
- }
- }
- }
-}
-
-/// Returns the term used to refer to a package.
-fn package_term(provider: &ProviderId) -> &'static str {
- if provider == &DocsDotRsProvider::id() || provider == &LocalRustdocProvider::id() {
- return "crate";
- }
-
- "package"
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_parse_docs_slash_command_args() {
- assert_eq!(
- DocsSlashCommandArgs::parse(&["".to_string()]),
- DocsSlashCommandArgs::NoProvider
- );
- assert_eq!(
- DocsSlashCommandArgs::parse(&["rustdoc".to_string()]),
- DocsSlashCommandArgs::NoProvider
- );
-
- assert_eq!(
- DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "".to_string()]),
- DocsSlashCommandArgs::SearchPackageDocs {
- provider: ProviderId("rustdoc".into()),
- package: "".into(),
- index: false
- }
- );
- assert_eq!(
- DocsSlashCommandArgs::parse(&["gleam".to_string(), "".to_string()]),
- DocsSlashCommandArgs::SearchPackageDocs {
- provider: ProviderId("gleam".into()),
- package: "".into(),
- index: false
- }
- );
-
- assert_eq!(
- DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui".to_string()]),
- DocsSlashCommandArgs::SearchPackageDocs {
- provider: ProviderId("rustdoc".into()),
- package: "gpui".into(),
- index: false,
- }
- );
- assert_eq!(
- DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib".to_string()]),
- DocsSlashCommandArgs::SearchPackageDocs {
- provider: ProviderId("gleam".into()),
- package: "gleam_stdlib".into(),
- index: false
- }
- );
-
- // Adding an item path delimiter indicates we can start indexing.
- assert_eq!(
- DocsSlashCommandArgs::parse(&["rustdoc".to_string(), "gpui:".to_string()]),
- DocsSlashCommandArgs::SearchPackageDocs {
- provider: ProviderId("rustdoc".into()),
- package: "gpui".into(),
- index: true,
- }
- );
- assert_eq!(
- DocsSlashCommandArgs::parse(&["gleam".to_string(), "gleam_stdlib/".to_string()]),
- DocsSlashCommandArgs::SearchPackageDocs {
- provider: ProviderId("gleam".into()),
- package: "gleam_stdlib".into(),
- index: true
- }
- );
-
- assert_eq!(
- DocsSlashCommandArgs::parse(&[
- "rustdoc".to_string(),
- "gpui::foo::bar::Baz".to_string()
- ]),
- DocsSlashCommandArgs::SearchItemDocs {
- provider: ProviderId("rustdoc".into()),
- package: "gpui".into(),
- item_path: "gpui::foo::bar::Baz".into()
- }
- );
- assert_eq!(
- DocsSlashCommandArgs::parse(&[
- "gleam".to_string(),
- "gleam_stdlib/gleam/int".to_string()
- ]),
- DocsSlashCommandArgs::SearchItemDocs {
- provider: ProviderId("gleam".into()),
- package: "gleam_stdlib".into(),
- item_path: "gleam_stdlib/gleam/int".into()
- }
- );
- }
-}
@@ -177,7 +177,7 @@ impl SlashCommand for FetchSlashCommand {
}],
run_commands_in_text: false,
}
- .to_event_stream())
+ .into_event_stream())
})
}
}
@@ -92,7 +92,7 @@ impl FileSlashCommand {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
- .map_or(false, |entry| entry.is_ignored),
+ .is_some_and(|entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Entries,
}
@@ -223,7 +223,7 @@ fn collect_files(
cx: &mut App,
) -> impl Stream<Item = Result<SlashCommandEvent>> + use<> {
let Ok(matchers) = glob_inputs
- .into_iter()
+ .iter()
.map(|glob_input| {
custom_path_matcher::PathMatcher::new(&[glob_input.to_owned()])
.with_context(|| format!("invalid path {glob_input}"))
@@ -371,7 +371,7 @@ fn collect_files(
&mut output,
)
.log_err();
- let mut buffer_events = output.to_event_stream();
+ let mut buffer_events = output.into_event_stream();
while let Some(event) = buffer_events.next().await {
events_tx.unbounded_send(event)?;
}
@@ -379,7 +379,7 @@ fn collect_files(
}
}
- while let Some(_) = directory_stack.pop() {
+ while directory_stack.pop().is_some() {
events_tx.unbounded_send(Ok(SlashCommandEvent::EndSection))?;
}
}
@@ -491,8 +491,8 @@ mod custom_path_matcher {
impl PathMatcher {
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
let globs = globs
- .into_iter()
- .map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string()))
+ .iter()
+ .map(|glob| Glob::new(&SanitizedPath::new(glob).to_glob_string()))
.collect::<Result<Vec<_>, _>>()?;
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
let sources_with_trailing_slash = globs
@@ -536,7 +536,7 @@ mod custom_path_matcher {
let path_str = path.to_string_lossy();
let separator = std::path::MAIN_SEPARATOR_STR;
if path_str.ends_with(separator) {
- return false;
+ false
} else {
self.glob.is_match(path_str.to_string() + separator)
}
@@ -66,6 +66,6 @@ impl SlashCommand for NowSlashCommand {
}],
run_commands_in_text: false,
}
- .to_event_stream()))
+ .into_event_stream()))
}
}
@@ -80,7 +80,7 @@ impl SlashCommand for PromptSlashCommand {
};
let store = PromptStore::global(cx);
- let title = SharedString::from(title.clone());
+ let title = SharedString::from(title);
let prompt = cx.spawn({
let title = title.clone();
async move |cx| {
@@ -117,7 +117,7 @@ impl SlashCommand for PromptSlashCommand {
}],
run_commands_in_text: true,
}
- .to_event_stream())
+ .into_event_stream())
})
}
}
@@ -1,4 +1,4 @@
-use anyhow::{Context as _, Result, anyhow};
+use anyhow::{Result, anyhow};
use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
@@ -70,9 +70,7 @@ impl SlashCommand for OutlineSlashCommand {
let path = snapshot.resolve_file_path(cx, true);
cx.background_spawn(async move {
- let outline = snapshot
- .outline(None)
- .context("no symbols for active tab")?;
+ let outline = snapshot.outline(None);
let path = path.as_deref().unwrap_or(Path::new("untitled"));
let mut outline_text = format!("Symbols for {}:\n", path.display());
@@ -92,7 +90,7 @@ impl SlashCommand for OutlineSlashCommand {
text: outline_text,
run_commands_in_text: false,
}
- .to_event_stream())
+ .into_event_stream())
})
});
@@ -157,7 +157,7 @@ impl SlashCommand for TabSlashCommand {
for (full_path, buffer, _) in tab_items_search.await? {
append_buffer_to_output(&buffer, full_path.as_deref(), &mut output).log_err();
}
- Ok(output.to_event_stream())
+ Ok(output.into_event_stream())
})
}
}
@@ -195,16 +195,14 @@ fn tab_items_for_queries(
}
for editor in workspace.items_of_type::<Editor>(cx) {
- if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
- if let Some(timestamp) =
+ if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton()
+ && let Some(timestamp) =
timestamps_by_entity_id.get(&editor.entity_id())
- {
- if visited_buffers.insert(buffer.read(cx).remote_id()) {
- let snapshot = buffer.read(cx).snapshot();
- let full_path = snapshot.resolve_file_path(cx, true);
- open_buffers.push((full_path, snapshot, *timestamp));
- }
- }
+ && visited_buffers.insert(buffer.read(cx).remote_id())
+ {
+ let snapshot = buffer.read(cx).snapshot();
+ let full_path = snapshot.resolve_file_path(cx, true);
+ open_buffers.push((full_path, snapshot, *timestamp));
}
}
@@ -41,9 +41,7 @@ pub async fn file_outline(
}
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
- let outline = snapshot
- .outline(None)
- .context("No outline information available for this file at path {path}")?;
+ let outline = snapshot.outline(None);
render_outline(
outline
@@ -24,16 +24,16 @@ pub fn adapt_schema_to_format(
fn preprocess_json_schema(json: &mut Value) -> Result<()> {
// `additionalProperties` defaults to `false` unless explicitly specified.
// This prevents models from hallucinating tool parameters.
- if let Value::Object(obj) = json {
- if matches!(obj.get("type"), Some(Value::String(s)) if s == "object") {
- if !obj.contains_key("additionalProperties") {
- obj.insert("additionalProperties".to_string(), Value::Bool(false));
- }
+ if let Value::Object(obj) = json
+ && matches!(obj.get("type"), Some(Value::String(s)) if s == "object")
+ {
+ if !obj.contains_key("additionalProperties") {
+ obj.insert("additionalProperties".to_string(), Value::Bool(false));
+ }
- // OpenAI API requires non-missing `properties`
- if !obj.contains_key("properties") {
- obj.insert("properties".to_string(), Value::Object(Default::default()));
- }
+ // OpenAI API requires non-missing `properties`
+ if !obj.contains_key("properties") {
+ obj.insert("properties".to_string(), Value::Object(Default::default()));
}
}
Ok(())
@@ -59,10 +59,10 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
("optional", |value| value.is_boolean()),
];
for (key, predicate) in KEYS_TO_REMOVE {
- if let Some(value) = obj.get(key) {
- if predicate(value) {
- obj.remove(key);
- }
+ if let Some(value) = obj.get(key)
+ && predicate(value)
+ {
+ obj.remove(key);
}
}
@@ -77,12 +77,12 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
}
// Handle oneOf -> anyOf conversion
- if let Some(subschemas) = obj.get_mut("oneOf") {
- if subschemas.is_array() {
- let subschemas_clone = subschemas.clone();
- obj.remove("oneOf");
- obj.insert("anyOf".to_string(), subschemas_clone);
- }
+ if let Some(subschemas) = obj.get_mut("oneOf")
+ && subschemas.is_array()
+ {
+ let subschemas_clone = subschemas.clone();
+ obj.remove("oneOf");
+ obj.insert("anyOf".to_string(), subschemas_clone);
}
// Recursively process all nested objects and arrays
@@ -156,13 +156,13 @@ fn resolve_context_server_tool_name_conflicts(
if duplicated_tool_names.is_empty() {
return context_server_tools
- .into_iter()
+ .iter()
.map(|tool| (resolve_tool_name(tool).into(), tool.clone()))
.collect();
}
context_server_tools
- .into_iter()
+ .iter()
.filter_map(|tool| {
let mut tool_name = resolve_tool_name(tool);
if !duplicated_tool_names.contains(&tool_name) {
@@ -72,11 +72,10 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
register_web_search_tool(&LanguageModelRegistry::global(cx), cx);
cx.subscribe(
&LanguageModelRegistry::global(cx),
- move |registry, event, cx| match event {
- language_model::Event::DefaultModelChanged => {
+ move |registry, event, cx| {
+ if let language_model::Event::DefaultModelChanged = event {
register_web_search_tool(®istry, cx);
}
- _ => {}
},
)
.detach();
@@ -86,7 +85,7 @@ fn register_web_search_tool(registry: &Entity<LanguageModelRegistry>, cx: &mut A
let using_zed_provider = registry
.read(cx)
.default_model()
- .map_or(false, |default| default.is_provided_by_zed());
+ .is_some_and(|default| default.is_provided_by_zed());
if using_zed_provider {
ToolRegistry::global(cx).register_tool(WebSearchTool);
} else {
@@ -35,7 +35,7 @@ impl Tool for DeletePathTool {
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
- false
+ true
}
fn may_perform_edits(&self) -> bool {
@@ -672,29 +672,30 @@ impl EditAgent {
cx: &mut AsyncApp,
) -> Result<BoxStream<'static, Result<String, LanguageModelCompletionError>>> {
let mut messages_iter = conversation.messages.iter_mut();
- if let Some(last_message) = messages_iter.next_back() {
- if last_message.role == Role::Assistant {
- let old_content_len = last_message.content.len();
- last_message
- .content
- .retain(|content| !matches!(content, MessageContent::ToolUse(_)));
- let new_content_len = last_message.content.len();
-
- // We just removed pending tool uses from the content of the
- // last message, so it doesn't make sense to cache it anymore
- // (e.g., the message will look very different on the next
- // request). Thus, we move the flag to the message prior to it,
- // as it will still be a valid prefix of the conversation.
- if old_content_len != new_content_len && last_message.cache {
- if let Some(prev_message) = messages_iter.next_back() {
- last_message.cache = false;
- prev_message.cache = true;
- }
- }
+ if let Some(last_message) = messages_iter.next_back()
+ && last_message.role == Role::Assistant
+ {
+ let old_content_len = last_message.content.len();
+ last_message
+ .content
+ .retain(|content| !matches!(content, MessageContent::ToolUse(_)));
+ let new_content_len = last_message.content.len();
+
+ // We just removed pending tool uses from the content of the
+ // last message, so it doesn't make sense to cache it anymore
+ // (e.g., the message will look very different on the next
+ // request). Thus, we move the flag to the message prior to it,
+ // as it will still be a valid prefix of the conversation.
+ if old_content_len != new_content_len
+ && last_message.cache
+ && let Some(prev_message) = messages_iter.next_back()
+ {
+ last_message.cache = false;
+ prev_message.cache = true;
+ }
- if last_message.content.is_empty() {
- conversation.messages.pop();
- }
+ if last_message.content.is_empty() {
+ conversation.messages.pop();
}
}
@@ -1314,17 +1315,17 @@ mod tests {
#[gpui::test(iterations = 100)]
async fn test_random_indents(mut rng: StdRng) {
- let len = rng.gen_range(1..=100);
+ let len = rng.random_range(1..=100);
let new_text = util::RandomCharIter::new(&mut rng)
.with_simple_text()
.take(len)
.collect::<String>();
let new_text = new_text
.split('\n')
- .map(|line| format!("{}{}", " ".repeat(rng.gen_range(0..=8)), line))
+ .map(|line| format!("{}{}", " ".repeat(rng.random_range(0..=8)), line))
.collect::<Vec<_>>()
.join("\n");
- let delta = IndentDelta::Spaces(rng.gen_range(-4..=4));
+ let delta = IndentDelta::Spaces(rng.random_range(-4i8..=4i8) as isize);
let chunks = to_random_chunks(&mut rng, &new_text);
let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| {
@@ -1356,7 +1357,7 @@ mod tests {
}
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
- let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
+ let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());
@@ -204,7 +204,7 @@ mod tests {
}
fn parse_random_chunks(input: &str, parser: &mut CreateFileParser, rng: &mut StdRng) -> String {
- let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
+ let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());
@@ -996,7 +996,7 @@ mod tests {
}
fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {
- let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
+ let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());
@@ -1153,8 +1153,7 @@ impl EvalInput {
.expect("Conversation must end with an edit_file tool use")
.clone();
- let edit_file_input: EditFileToolInput =
- serde_json::from_value(tool_use.input.clone()).unwrap();
+ let edit_file_input: EditFileToolInput = serde_json::from_value(tool_use.input).unwrap();
EvalInput {
conversation,
@@ -1283,14 +1282,14 @@ impl EvalAssertion {
// Parse the score from the response
let re = regex::Regex::new(r"<score>(\d+)</score>").unwrap();
- if let Some(captures) = re.captures(&output) {
- if let Some(score_match) = captures.get(1) {
- let score = score_match.as_str().parse().unwrap_or(0);
- return Ok(EvalAssertionOutcome {
- score,
- message: Some(output),
- });
- }
+ if let Some(captures) = re.captures(&output)
+ && let Some(score_match) = captures.get(1)
+ {
+ let score = score_match.as_str().parse().unwrap_or(0);
+ return Ok(EvalAssertionOutcome {
+ score,
+ message: Some(output),
+ });
}
anyhow::bail!("No score found in response. Raw output: {output}");
@@ -1400,7 +1399,7 @@ fn eval(
}
fn run_eval(eval: EvalInput, tx: mpsc::Sender<Result<EvalOutput>>) {
- let dispatcher = gpui::TestDispatcher::new(StdRng::from_entropy());
+ let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
let mut cx = TestAppContext::build(dispatcher, None);
let output = cx.executor().block_test(async {
let test = EditAgentTest::new(&mut cx).await;
@@ -1460,7 +1459,7 @@ impl EditAgentTest {
async fn new(cx: &mut TestAppContext) -> Self {
cx.executor().allow_parking();
- let fs = FakeFs::new(cx.executor().clone());
+ let fs = FakeFs::new(cx.executor());
cx.update(|cx| {
settings::init(cx);
gpui_tokio::init(cx);
@@ -1475,7 +1474,7 @@ impl EditAgentTest {
Project::init_settings(cx);
language::init(cx);
language_model::init(client.clone(), cx);
- language_models::init(user_store.clone(), client.clone(), cx);
+ language_models::init(user_store, client.clone(), cx);
crate::init(client.http_client(), cx);
});
@@ -1521,7 +1520,15 @@ impl EditAgentTest {
selected_model: &SelectedModel,
cx: &mut AsyncApp,
) -> Result<Arc<dyn LanguageModel>> {
- let (provider, model) = cx.update(|cx| {
+ cx.update(|cx| {
+ let registry = LanguageModelRegistry::read_global(cx);
+ let provider = registry
+ .provider(&selected_model.provider)
+ .expect("Provider not found");
+ provider.authenticate(cx)
+ })?
+ .await?;
+ cx.update(|cx| {
let models = LanguageModelRegistry::read_global(cx);
let model = models
.available_models(cx)
@@ -1530,11 +1537,8 @@ impl EditAgentTest {
&& model.id() == selected_model.model
})
.expect("Model not found");
- let provider = models.provider(&model.provider_id()).unwrap();
- (provider, model)
- })?;
- cx.update(|cx| provider.authenticate(cx))?.await?;
- Ok(model)
+ model
+ })
}
async fn eval(&self, eval: EvalInput, cx: &mut TestAppContext) -> Result<EvalOutput> {
@@ -1586,7 +1590,7 @@ impl EditAgentTest {
let has_system_prompt = eval
.conversation
.first()
- .map_or(false, |msg| msg.role == Role::System);
+ .is_some_and(|msg| msg.role == Role::System);
let messages = if has_system_prompt {
eval.conversation
} else {
@@ -1708,7 +1712,7 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
};
if let Some(retry_after) = retry_delay {
- let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
+ let jitter = retry_after.mul_f64(rand::rng().random_range(0.0..1.0));
eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}");
Timer::after(retry_after + jitter).await;
} else {
@@ -319,7 +319,7 @@ mod tests {
);
let snapshot = buffer.snapshot();
- let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+ let mut finder = StreamingFuzzyMatcher::new(snapshot);
assert_eq!(push(&mut finder, ""), None);
assert_eq!(finish(finder), None);
}
@@ -333,7 +333,7 @@ mod tests {
);
let snapshot = buffer.snapshot();
- let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+ let mut finder = StreamingFuzzyMatcher::new(snapshot);
// Push partial query
assert_eq!(push(&mut finder, "This"), None);
@@ -365,7 +365,7 @@ mod tests {
);
let snapshot = buffer.snapshot();
- let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+ let mut finder = StreamingFuzzyMatcher::new(snapshot);
// Push a fuzzy query that should match the first function
assert_eq!(
@@ -391,7 +391,7 @@ mod tests {
);
let snapshot = buffer.snapshot();
- let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+ let mut finder = StreamingFuzzyMatcher::new(snapshot);
// No match initially
assert_eq!(push(&mut finder, "Lin"), None);
@@ -420,7 +420,7 @@ mod tests {
);
let snapshot = buffer.snapshot();
- let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+ let mut finder = StreamingFuzzyMatcher::new(snapshot);
// Push text in small chunks across line boundaries
assert_eq!(push(&mut finder, "jumps "), None); // No newline yet
@@ -458,7 +458,7 @@ mod tests {
);
let snapshot = buffer.snapshot();
- let mut finder = StreamingFuzzyMatcher::new(snapshot.clone());
+ let mut finder = StreamingFuzzyMatcher::new(snapshot);
assert_eq!(
push(&mut finder, "impl Debug for User {\n"),
@@ -711,7 +711,7 @@ mod tests {
"Expected to match `second_function` based on the line hint"
);
- let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
+ let mut matcher = StreamingFuzzyMatcher::new(snapshot);
matcher.push(query, None);
matcher.finish();
let best_match = matcher.select_best_match();
@@ -727,7 +727,7 @@ mod tests {
let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.clone());
let snapshot = buffer.snapshot();
- let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
+ let mut matcher = StreamingFuzzyMatcher::new(snapshot);
// Split query into random chunks
let chunks = to_random_chunks(rng, query);
@@ -771,7 +771,7 @@ mod tests {
}
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
- let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
+ let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());
@@ -794,10 +794,8 @@ mod tests {
fn finish(mut finder: StreamingFuzzyMatcher) -> Option<String> {
let snapshot = finder.snapshot.clone();
let matches = finder.finish();
- if let Some(range) = matches.first() {
- Some(snapshot.text_for_range(range.clone()).collect::<String>())
- } else {
- None
- }
+ matches
+ .first()
+ .map(|range| snapshot.text_for_range(range.clone()).collect::<String>())
}
}
@@ -11,11 +11,13 @@ use assistant_tool::{
AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
-use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
+use editor::{
+ Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines,
+};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
- TextStyleRefinement, Transformation, WeakEntity, percentage, pulsating_between, px,
+ TextStyleRefinement, WeakEntity, pulsating_between, px,
};
use indoc::formatdoc;
use language::{
@@ -42,7 +44,7 @@ use std::{
time::Duration,
};
use theme::ThemeSettings;
-use ui::{Disclosure, Tooltip, prelude::*};
+use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
use util::ResultExt;
use workspace::Workspace;
@@ -155,10 +157,10 @@ impl Tool for EditFileTool {
// It's also possible that the global config dir is configured to be inside the project,
// so check for that edge case too.
- if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
- if canonical_path.starts_with(paths::config_dir()) {
- return true;
- }
+ if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
+ && canonical_path.starts_with(paths::config_dir())
+ {
+ return true;
}
// Check if path is inside the global config directory
@@ -199,10 +201,10 @@ impl Tool for EditFileTool {
.any(|c| c.as_os_str() == local_settings_folder.as_os_str())
{
description.push_str(" (local settings)");
- } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
- if canonical_path.starts_with(paths::config_dir()) {
- description.push_str(" (global settings)");
- }
+ } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path)
+ && canonical_path.starts_with(paths::config_dir())
+ {
+ description.push_str(" (global settings)");
}
description
@@ -376,7 +378,7 @@ impl Tool for EditFileTool {
let output = EditFileToolOutput {
original_path: project_path.path.to_path_buf(),
- new_text: new_text.clone(),
+ new_text,
old_text,
raw_output: Some(agent_output),
};
@@ -474,7 +476,7 @@ impl Tool for EditFileTool {
PathKey::for_buffer(&buffer, cx),
buffer,
diff_hunk_ranges,
- editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff, cx);
@@ -536,7 +538,7 @@ fn resolve_path(
let parent_entry = parent_project_path
.as_ref()
- .and_then(|path| project.entry_for_path(&path, cx))
+ .and_then(|path| project.entry_for_path(path, cx))
.context("Can't create file: parent directory doesn't exist")?;
anyhow::ensure!(
@@ -643,7 +645,7 @@ impl EditFileToolCard {
diff
});
- self.buffer = Some(buffer.clone());
+ self.buffer = Some(buffer);
self.base_text = Some(base_text.into());
self.buffer_diff = Some(buffer_diff.clone());
@@ -703,7 +705,7 @@ impl EditFileToolCard {
PathKey::for_buffer(buffer, cx),
buffer.clone(),
ranges,
- editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ multibuffer_context_lines(cx),
cx,
);
let end = multibuffer.len(cx);
@@ -723,13 +725,13 @@ impl EditFileToolCard {
let buffer = buffer.read(cx);
let diff = diff.read(cx);
let mut ranges = diff
- .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx)
- .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer))
+ .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
+ .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::<Vec<_>>();
ranges.extend(
self.revealed_ranges
.iter()
- .map(|range| range.to_point(&buffer)),
+ .map(|range| range.to_point(buffer)),
);
ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end)));
@@ -776,7 +778,6 @@ impl EditFileToolCard {
let buffer_diff = cx.spawn({
let buffer = buffer.clone();
- let language_registry = language_registry.clone();
async move |_this, cx| {
build_buffer_diff(base_text, &buffer, &language_registry, cx).await
}
@@ -792,7 +793,7 @@ impl EditFileToolCard {
path_key,
buffer,
ranges,
- editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff.clone(), cx);
@@ -863,7 +864,6 @@ impl ToolCard for EditFileToolCard {
)
.on_click({
let path = self.path.clone();
- let workspace = workspace.clone();
move |_, window, cx| {
workspace
.update(cx, {
@@ -939,11 +939,7 @@ impl ToolCard for EditFileToolCard {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- ),
+ .with_rotate_animation(2),
)
})
.when_some(error_message, |header, error_message| {
@@ -1356,8 +1352,7 @@ mod tests {
mode: mode.clone(),
};
- let result = cx.update(|cx| resolve_path(&input, project, cx));
- result
+ cx.update(|cx| resolve_path(&input, project, cx))
}
fn assert_resolved_path_eq(path: anyhow::Result<ProjectPath>, expected: &str) {
@@ -118,7 +118,7 @@ impl Tool for FetchTool {
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
- false
+ true
}
fn may_perform_edits(&self) -> bool {
@@ -234,7 +234,7 @@ impl ToolCard for FindPathToolCard {
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
- let matches_label: SharedString = if self.paths.len() == 0 {
+ let matches_label: SharedString = if self.paths.is_empty() {
"No matches".into()
} else if self.paths.len() == 1 {
"1 match".into()
@@ -435,8 +435,8 @@ mod test {
assert_eq!(
matches,
&[
- PathBuf::from("root/apple/banana/carrot"),
- PathBuf::from("root/apple/bandana/carbonara")
+ PathBuf::from(path!("root/apple/banana/carrot")),
+ PathBuf::from(path!("root/apple/bandana/carbonara"))
]
);
@@ -447,8 +447,8 @@ mod test {
assert_eq!(
matches,
&[
- PathBuf::from("root/apple/banana/carrot"),
- PathBuf::from("root/apple/bandana/carbonara")
+ PathBuf::from(path!("root/apple/banana/carrot")),
+ PathBuf::from(path!("root/apple/bandana/carbonara"))
]
);
}
@@ -188,15 +188,14 @@ impl Tool for GrepTool {
// Check if this file should be excluded based on its worktree settings
if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| {
project.find_project_path(&path, cx)
- }) {
- if cx.update(|cx| {
+ })
+ && cx.update(|cx| {
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
worktree_settings.is_path_excluded(&project_path.path)
|| worktree_settings.is_path_private(&project_path.path)
}).unwrap_or(false) {
continue;
}
- }
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await?;
@@ -268,10 +267,8 @@ impl Tool for GrepTool {
let end_row = range.end.row;
output.push_str("\n### ");
- if let Some(parent_symbols) = &parent_symbols {
- for symbol in parent_symbols {
- write!(output, "{} › ", symbol.text)?;
- }
+ for symbol in parent_symbols {
+ write!(output, "{} › ", symbol.text)?;
}
if range.start.row == end_row {
@@ -284,12 +281,11 @@ impl Tool for GrepTool {
output.extend(snapshot.text_for_range(range));
output.push_str("\n```\n");
- if let Some(ancestor_range) = ancestor_range {
- if end_row < ancestor_range.end.row {
+ if let Some(ancestor_range) = ancestor_range
+ && end_row < ancestor_range.end.row {
let remaining_lines = ancestor_range.end.row - end_row;
writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?;
}
- }
matches_found += 1;
}
@@ -329,7 +325,7 @@ mod tests {
init_test(cx);
cx.executor().allow_parking();
- let fs = FakeFs::new(cx.executor().clone());
+ let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
serde_json::json!({
@@ -417,7 +413,7 @@ mod tests {
init_test(cx);
cx.executor().allow_parking();
- let fs = FakeFs::new(cx.executor().clone());
+ let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
serde_json::json!({
@@ -496,7 +492,7 @@ mod tests {
init_test(cx);
cx.executor().allow_parking();
- let fs = FakeFs::new(cx.executor().clone());
+ let fs = FakeFs::new(cx.executor());
// Create test file with syntax structures
fs.insert_tree(
@@ -894,7 +890,7 @@ mod tests {
})
.await;
let results = result.unwrap();
- let paths = extract_paths_from_results(&results.content.as_str().unwrap());
+ let paths = extract_paths_from_results(results.content.as_str().unwrap());
assert!(
paths.is_empty(),
"grep_tool should not find files outside the project worktree"
@@ -920,7 +916,7 @@ mod tests {
})
.await;
let results = result.unwrap();
- let paths = extract_paths_from_results(&results.content.as_str().unwrap());
+ let paths = extract_paths_from_results(results.content.as_str().unwrap());
assert!(
paths.iter().any(|p| p.contains("allowed_file.rs")),
"grep_tool should be able to search files inside worktrees"
@@ -946,7 +942,7 @@ mod tests {
})
.await;
let results = result.unwrap();
- let paths = extract_paths_from_results(&results.content.as_str().unwrap());
+ let paths = extract_paths_from_results(results.content.as_str().unwrap());
assert!(
paths.is_empty(),
"grep_tool should not search files in .secretdir (file_scan_exclusions)"
@@ -971,7 +967,7 @@ mod tests {
})
.await;
let results = result.unwrap();
- let paths = extract_paths_from_results(&results.content.as_str().unwrap());
+ let paths = extract_paths_from_results(results.content.as_str().unwrap());
assert!(
paths.is_empty(),
"grep_tool should not search .mymetadata files (file_scan_exclusions)"
@@ -997,7 +993,7 @@ mod tests {
})
.await;
let results = result.unwrap();
- let paths = extract_paths_from_results(&results.content.as_str().unwrap());
+ let paths = extract_paths_from_results(results.content.as_str().unwrap());
assert!(
paths.is_empty(),
"grep_tool should not search .mysecrets (private_files)"
@@ -1022,7 +1018,7 @@ mod tests {
})
.await;
let results = result.unwrap();
- let paths = extract_paths_from_results(&results.content.as_str().unwrap());
+ let paths = extract_paths_from_results(results.content.as_str().unwrap());
assert!(
paths.is_empty(),
"grep_tool should not search .privatekey files (private_files)"
@@ -1047,7 +1043,7 @@ mod tests {
})
.await;
let results = result.unwrap();
- let paths = extract_paths_from_results(&results.content.as_str().unwrap());
+ let paths = extract_paths_from_results(results.content.as_str().unwrap());
assert!(
paths.is_empty(),
"grep_tool should not search .mysensitive files (private_files)"
@@ -1073,7 +1069,7 @@ mod tests {
})
.await;
let results = result.unwrap();
- let paths = extract_paths_from_results(&results.content.as_str().unwrap());
+ let paths = extract_paths_from_results(results.content.as_str().unwrap());
assert!(
paths.iter().any(|p| p.contains("normal_file.rs")),
"Should be able to search normal files"
@@ -1100,7 +1096,7 @@ mod tests {
})
.await;
let results = result.unwrap();
- let paths = extract_paths_from_results(&results.content.as_str().unwrap());
+ let paths = extract_paths_from_results(results.content.as_str().unwrap());
assert!(
paths.is_empty(),
"grep_tool should not allow escaping project boundaries with relative paths"
@@ -1206,7 +1202,7 @@ mod tests {
.unwrap();
let content = result.content.as_str().unwrap();
- let paths = extract_paths_from_results(&content);
+ let paths = extract_paths_from_results(content);
// Should find matches in non-private files
assert!(
@@ -1271,7 +1267,7 @@ mod tests {
.unwrap();
let content = result.content.as_str().unwrap();
- let paths = extract_paths_from_results(&content);
+ let paths = extract_paths_from_results(content);
// Should only find matches in worktree1 *.rs files (excluding private ones)
assert!(
@@ -81,7 +81,7 @@ fn fit_patch_to_size(patch: &str, max_size: usize) -> String {
// Compression level 1: remove context lines in diff bodies, but
// leave the counts and positions of inserted/deleted lines
let mut current_size = patch.len();
- let mut file_patches = split_patch(&patch);
+ let mut file_patches = split_patch(patch);
file_patches.sort_by_key(|patch| patch.len());
let compressed_patches = file_patches
.iter()
@@ -68,7 +68,7 @@ impl Tool for ReadFileTool {
}
fn icon(&self) -> IconName {
- IconName::ToolRead
+ IconName::ToolSearch
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -201,7 +201,7 @@ impl Tool for ReadFileTool {
buffer
.file()
.as_ref()
- .map_or(true, |file| !file.disk_state().exists())
+ .is_none_or(|file| !file.disk_state().exists())
})? {
anyhow::bail!("{file_path} not found");
}
@@ -43,12 +43,11 @@ impl Transform for ToJsonSchemaSubsetTransform {
fn transform(&mut self, schema: &mut Schema) {
// Ensure that the type field is not an array, this happens when we use
// Option<T>, the type will be [T, "null"].
- if let Some(type_field) = schema.get_mut("type") {
- if let Some(types) = type_field.as_array() {
- if let Some(first_type) = types.first() {
- *type_field = first_type.clone();
- }
- }
+ if let Some(type_field) = schema.get_mut("type")
+ && let Some(types) = type_field.as_array()
+ && let Some(first_type) = types.first()
+ {
+ *type_field = first_type.clone();
}
// oneOf is not supported, use anyOf instead
@@ -8,14 +8,14 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt as _, future::Shared};
use gpui::{
- Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task,
- TextStyleRefinement, Transformation, WeakEntity, Window, percentage,
+ AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement,
+ WeakEntity, Window,
};
use language::LineEnding;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
-use project::{Project, terminals::TerminalKind};
+use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -28,7 +28,7 @@ use std::{
};
use terminal_view::TerminalView;
use theme::ThemeSettings;
-use ui::{Disclosure, Tooltip, prelude::*};
+use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
use util::{
ResultExt, get_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
time::duration_alt_display,
@@ -59,12 +59,9 @@ impl TerminalTool {
}
if which::which("bash").is_ok() {
- log::info!("agent selected bash for terminal tool");
"bash".into()
} else {
- let shell = get_system_shell();
- log::info!("agent selected {shell} for terminal tool");
- shell
+ get_system_shell()
}
});
Self {
@@ -105,7 +102,7 @@ impl Tool for TerminalTool {
let first_line = lines.next().unwrap_or_default();
let remaining_line_count = lines.count();
match remaining_line_count {
- 0 => MarkdownInlineCode(&first_line).to_string(),
+ 0 => MarkdownInlineCode(first_line).to_string(),
1 => MarkdownInlineCode(&format!(
"{} - {} more line",
first_line, remaining_line_count
@@ -216,21 +213,20 @@ impl Tool for TerminalTool {
async move |cx| {
let program = program.await;
let env = env.await;
- let terminal = project
+ project
.update(cx, |project, cx| {
- project.create_terminal(
- TerminalKind::Task(task::SpawnInTerminal {
+ project.create_terminal_task(
+ task::SpawnInTerminal {
command: Some(program),
args,
cwd,
env,
..Default::default()
- }),
+ },
cx,
)
})?
- .await;
- terminal
+ .await
}
});
@@ -353,7 +349,7 @@ fn process_content(
if is_empty {
"Command executed successfully.".to_string()
} else {
- content.to_string()
+ content
}
}
Some(exit_status) => {
@@ -387,7 +383,7 @@ fn working_dir(
let project = project.read(cx);
let cd = &input.cd;
- if cd == "." || cd == "" {
+ if cd == "." || cd.is_empty() {
// Accept "." or "" as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
@@ -412,10 +408,8 @@ fn working_dir(
{
return Ok(Some(input_path.into()));
}
- } else {
- if let Some(worktree) = project.worktree_for_root_name(cd, cx) {
- return Ok(Some(worktree.read(cx).abs_path().to_path_buf()));
- }
+ } else if let Some(worktree) = project.worktree_for_root_name(cd, cx) {
+ return Ok(Some(worktree.read(cx).abs_path().to_path_buf()));
}
anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees.");
@@ -528,11 +522,7 @@ impl ToolCard for TerminalToolCard {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- ),
+ .with_rotate_animation(2),
)
})
.when(tool_failed || command_failed, |header| {
@@ -101,14 +101,11 @@ impl RenderOnce for ToolCallCardHeader {
})
.when_some(secondary_text, |this, secondary_text| {
this.child(bullet_divider())
- .child(div().text_size(font_size).child(secondary_text.clone()))
+ .child(div().text_size(font_size).child(secondary_text))
})
.when_some(code_path, |this, code_path| {
- this.child(bullet_divider()).child(
- Label::new(code_path.clone())
- .size(LabelSize::Small)
- .inline_code(cx),
- )
+ this.child(bullet_divider())
+ .child(Label::new(code_path).size(LabelSize::Small).inline_code(cx))
})
.with_animation(
"loading-label",
@@ -193,10 +193,7 @@ impl ToolCard for WebSearchToolCard {
)
}
})
- .on_click({
- let url = url.clone();
- move |_, _, cx| cx.open_url(&url)
- })
+ .on_click(move |_, _, cx| cx.open_url(&url))
}))
.into_any(),
),
@@ -14,10 +14,19 @@ doctest = false
[dependencies]
anyhow.workspace = true
+async-tar.workspace = true
collections.workspace = true
-derive_more.workspace = true
+crossbeam.workspace = true
gpui.workspace = true
+log.workspace = true
parking_lot.workspace = true
-rodio = { workspace = true, features = ["wav", "playback", "tracing"] }
+rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] }
+schemars.workspace = true
+serde.workspace = true
+settings.workspace = true
+smol.workspace = true
util.workspace = true
workspace-hack.workspace = true
+
+[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
+libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" }
@@ -1,54 +0,0 @@
-use std::{io::Cursor, sync::Arc};
-
-use anyhow::{Context as _, Result};
-use collections::HashMap;
-use gpui::{App, AssetSource, Global};
-use rodio::{Decoder, Source, source::Buffered};
-
-type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
-
-pub struct SoundRegistry {
- cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
- assets: Box<dyn AssetSource>,
-}
-
-struct GlobalSoundRegistry(Arc<SoundRegistry>);
-
-impl Global for GlobalSoundRegistry {}
-
-impl SoundRegistry {
- pub fn new(source: impl AssetSource) -> Arc<Self> {
- Arc::new(Self {
- cache: Default::default(),
- assets: Box::new(source),
- })
- }
-
- pub fn global(cx: &App) -> Arc<Self> {
- cx.global::<GlobalSoundRegistry>().0.clone()
- }
-
- pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) {
- cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source)));
- }
-
- pub fn get(&self, name: &str) -> Result<impl Source<Item = f32> + use<>> {
- if let Some(wav) = self.cache.lock().get(name) {
- return Ok(wav.clone());
- }
-
- let path = format!("sounds/{}.wav", name);
- let bytes = self
- .assets
- .load(&path)?
- .map(anyhow::Ok)
- .with_context(|| format!("No asset available for path {path}"))??
- .into_owned();
- let cursor = Cursor::new(bytes);
- let source = Decoder::new(cursor)?.buffered();
-
- self.cache.lock().insert(name.to_string(), source.clone());
-
- Ok(source)
- }
-}
@@ -1,16 +1,54 @@
-use assets::SoundRegistry;
-use derive_more::{Deref, DerefMut};
-use gpui::{App, AssetSource, BorrowAppContext, Global};
-use rodio::{OutputStream, OutputStreamBuilder};
+use anyhow::{Context as _, Result};
+use collections::HashMap;
+use gpui::{App, AsyncApp, BackgroundExecutor, BorrowAppContext, Global};
+use libwebrtc::native::apm;
+use log::info;
+use parking_lot::Mutex;
+use rodio::{
+ Decoder, OutputStream, OutputStreamBuilder, Source,
+ cpal::Sample,
+ mixer::Mixer,
+ nz,
+ source::{Buffered, LimitSettings, UniformSourceIterator},
+};
+use settings::Settings;
+use std::{
+ io::Cursor,
+ num::NonZero,
+ path::PathBuf,
+ sync::{Arc, atomic::Ordering},
+ time::Duration,
+};
use util::ResultExt;
-mod assets;
+mod audio_settings;
+mod replays;
+mod rodio_ext;
+pub use audio_settings::AudioSettings;
+pub use rodio_ext::RodioExt;
-pub fn init(source: impl AssetSource, cx: &mut App) {
- SoundRegistry::set_global(source, cx);
- cx.set_global(GlobalAudio(Audio::new()));
+use crate::audio_settings::LIVE_SETTINGS;
+
+// NOTE: We used to use WebRTC's mixer which only supported
+// 16kHz, 32kHz and 48kHz. As 48 is the most common "next step up"
+// for audio output devices like speakers/bluetooth, we just hard-code
+// this; and downsample when we need to.
+//
+// Since most noise cancelling requires 16kHz we will move to
+// that in the future.
+pub const SAMPLE_RATE: NonZero<u32> = nz!(48000);
+pub const CHANNEL_COUNT: NonZero<u16> = nz!(2);
+pub const BUFFER_SIZE: usize = // echo canceller and livekit want 10ms of audio
+ (SAMPLE_RATE.get() as usize / 100) * CHANNEL_COUNT.get() as usize;
+
+pub const REPLAY_DURATION: Duration = Duration::from_secs(30);
+
+pub fn init(cx: &mut App) {
+ AudioSettings::register(cx);
+ LIVE_SETTINGS.initialize(cx);
}
+#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
pub enum Sound {
Joined,
Leave,
@@ -35,49 +73,196 @@ impl Sound {
}
}
-#[derive(Default)]
pub struct Audio {
output_handle: Option<OutputStream>,
+ output_mixer: Option<Mixer>,
+ pub echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
+ source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
+ replays: replays::Replays,
}
-#[derive(Deref, DerefMut)]
-struct GlobalAudio(Audio);
+impl Default for Audio {
+ fn default() -> Self {
+ Self {
+ output_handle: Default::default(),
+ output_mixer: Default::default(),
+ echo_canceller: Arc::new(Mutex::new(apm::AudioProcessingModule::new(
+ true, false, false, false,
+ ))),
+ source_cache: Default::default(),
+ replays: Default::default(),
+ }
+ }
+}
-impl Global for GlobalAudio {}
+impl Global for Audio {}
impl Audio {
- pub fn new() -> Self {
- Self::default()
- }
-
- fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
+ fn ensure_output_exists(&mut self) -> Result<&Mixer> {
if self.output_handle.is_none() {
- self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
+ self.output_handle = Some(
+ OutputStreamBuilder::open_default_stream()
+ .context("Could not open default output stream")?,
+ );
+ if let Some(output_handle) = &self.output_handle {
+ let (mixer, source) = rodio::mixer::mixer(CHANNEL_COUNT, SAMPLE_RATE);
+ // or the mixer will end immediately as its empty.
+ mixer.add(rodio::source::Zero::new(CHANNEL_COUNT, SAMPLE_RATE));
+ self.output_mixer = Some(mixer);
+
+ let echo_canceller = Arc::clone(&self.echo_canceller);
+ let source = source.inspect_buffer::<BUFFER_SIZE, _>(move |buffer| {
+ let mut buf: [i16; _] = buffer.map(|s| s.to_sample());
+ echo_canceller
+ .lock()
+ .process_reverse_stream(
+ &mut buf,
+ SAMPLE_RATE.get() as i32,
+ CHANNEL_COUNT.get().into(),
+ )
+ .expect("Audio input and output threads should not panic");
+ });
+ output_handle.mixer().add(source);
+ }
}
- self.output_handle.as_ref()
+ Ok(self
+ .output_mixer
+ .as_ref()
+ .expect("we only get here if opening the outputstream succeeded"))
+ }
+
+ pub fn save_replays(
+ &self,
+ executor: BackgroundExecutor,
+ ) -> gpui::Task<anyhow::Result<(PathBuf, Duration)>> {
+ self.replays.replays_to_tar(executor)
+ }
+
+ pub fn open_microphone(voip_parts: VoipParts) -> anyhow::Result<impl Source> {
+ let stream = rodio::microphone::MicrophoneBuilder::new()
+ .default_device()?
+ .default_config()?
+ .prefer_sample_rates([SAMPLE_RATE, SAMPLE_RATE.saturating_mul(nz!(2))])
+ .prefer_channel_counts([nz!(1), nz!(2)])
+ .prefer_buffer_sizes(512..)
+ .open_stream()?;
+ info!("Opened microphone: {:?}", stream.config());
+
+ let (replay, stream) = UniformSourceIterator::new(stream, CHANNEL_COUNT, SAMPLE_RATE)
+ .limit(LimitSettings::live_performance())
+ .process_buffer::<BUFFER_SIZE, _>(move |buffer| {
+ let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
+ if voip_parts
+ .echo_canceller
+ .lock()
+ .process_stream(
+ &mut int_buffer,
+ SAMPLE_RATE.get() as i32,
+ CHANNEL_COUNT.get() as i32,
+ )
+ .context("livekit audio processor error")
+ .log_err()
+ .is_some()
+ {
+ for (sample, processed) in buffer.iter_mut().zip(&int_buffer) {
+ *sample = (*processed).to_sample();
+ }
+ }
+ })
+ .automatic_gain_control(1.0, 4.0, 0.0, 5.0)
+ .periodic_access(Duration::from_millis(100), move |agc_source| {
+ agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
+ })
+ .replayable(REPLAY_DURATION)
+ .expect("REPLAY_DURATION is longer then 100ms");
+
+ voip_parts
+ .replays
+ .add_voip_stream("local microphone".to_string(), replay);
+ Ok(stream)
+ }
+
+ pub fn play_voip_stream(
+ source: impl rodio::Source + Send + 'static,
+ speaker_name: String,
+ is_staff: bool,
+ cx: &mut App,
+ ) -> anyhow::Result<()> {
+ let (replay_source, source) = source
+ .automatic_gain_control(1.0, 4.0, 0.0, 5.0)
+ .periodic_access(Duration::from_millis(100), move |agc_source| {
+ agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
+ })
+ .replayable(REPLAY_DURATION)
+ .expect("REPLAY_DURATION is longer then 100ms");
+
+ cx.update_default_global(|this: &mut Self, _cx| {
+ let output_mixer = this
+ .ensure_output_exists()
+ .context("Could not get output mixer")?;
+ output_mixer.add(source);
+ if is_staff {
+ this.replays.add_voip_stream(speaker_name, replay_source);
+ }
+ Ok(())
+ })
}
pub fn play_sound(sound: Sound, cx: &mut App) {
- if !cx.has_global::<GlobalAudio>() {
- return;
- }
+ cx.update_default_global(|this: &mut Self, cx| {
+ let source = this.sound_source(sound, cx).log_err()?;
+ let output_mixer = this
+ .ensure_output_exists()
+ .context("Could not get output mixer")
+ .log_err()?;
- cx.update_global::<GlobalAudio, _>(|this, cx| {
- let output_handle = this.ensure_output_exists()?;
- let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
- output_handle.mixer().add(source);
+ output_mixer.add(source);
Some(())
});
}
pub fn end_call(cx: &mut App) {
- if !cx.has_global::<GlobalAudio>() {
- return;
- }
-
- cx.update_global::<GlobalAudio, _>(|this, _| {
+ cx.update_default_global(|this: &mut Self, _cx| {
this.output_handle.take();
});
}
+
+ fn sound_source(&mut self, sound: Sound, cx: &App) -> Result<impl Source + use<>> {
+ if let Some(wav) = self.source_cache.get(&sound) {
+ return Ok(wav.clone());
+ }
+
+ let path = format!("sounds/{}.wav", sound.file());
+ let bytes = cx
+ .asset_source()
+ .load(&path)?
+ .map(anyhow::Ok)
+ .with_context(|| format!("No asset available for path {path}"))??
+ .into_owned();
+ let cursor = Cursor::new(bytes);
+ let source = Decoder::new(cursor)?.buffered();
+
+ self.source_cache.insert(sound, source.clone());
+
+ Ok(source)
+ }
+}
+
+pub struct VoipParts {
+ echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
+ replays: replays::Replays,
+}
+
+impl VoipParts {
+ pub fn new(cx: &AsyncApp) -> anyhow::Result<Self> {
+ let (apm, replays) = cx.try_read_default_global::<Audio, _>(|audio, _| {
+ (Arc::clone(&audio.echo_canceller), audio.replays.clone())
+ })?;
+
+ Ok(Self {
+ echo_canceller: apm,
+ replays,
+ })
+ }
}
@@ -0,0 +1,96 @@
+use std::sync::atomic::{AtomicBool, Ordering};
+
+use anyhow::Result;
+use gpui::App;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
+pub struct AudioSettings {
+ /// Opt into the new audio system.
+ #[serde(rename = "experimental.rodio_audio", default)]
+ pub rodio_audio: bool, // default is false
+ /// Requires 'rodio_audio: true'
+ ///
+ /// Use the new audio systems automatic gain control for your microphone.
+ /// This affects how loud you sound to others.
+ #[serde(rename = "experimental.control_input_volume", default)]
+ pub control_input_volume: bool,
+ /// Requires 'rodio_audio: true'
+ ///
+ /// Use the new audio systems automatic gain control on everyone in the
+ /// call. This makes call members who are too quite louder and those who are
+ /// too loud quieter. This only affects how things sound for you.
+ #[serde(rename = "experimental.control_output_volume", default)]
+ pub control_output_volume: bool,
+}
+
+/// Configuration of audio in Zed.
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[serde(default)]
+#[settings_key(key = "audio")]
+pub struct AudioSettingsContent {
+ /// Opt into the new audio system.
+ #[serde(rename = "experimental.rodio_audio", default)]
+ pub rodio_audio: bool, // default is false
+ /// Requires 'rodio_audio: true'
+ ///
+ /// Use the new audio systems automatic gain control for your microphone.
+ /// This affects how loud you sound to others.
+ #[serde(rename = "experimental.control_input_volume", default)]
+ pub control_input_volume: bool,
+ /// Requires 'rodio_audio: true'
+ ///
+ /// Use the new audio systems automatic gain control on everyone in the
+ /// call. This makes call members who are too quite louder and those who are
+ /// too loud quieter. This only affects how things sound for you.
+ #[serde(rename = "experimental.control_output_volume", default)]
+ pub control_output_volume: bool,
+}
+
+impl Settings for AudioSettings {
+ type FileContent = AudioSettingsContent;
+
+ fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
+ sources.json_merge()
+ }
+
+ fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
+}
+
+pub(crate) struct LiveSettings {
+ pub(crate) control_input_volume: AtomicBool,
+ pub(crate) control_output_volume: AtomicBool,
+}
+
+impl LiveSettings {
+ pub(crate) fn initialize(&self, cx: &mut App) {
+ cx.observe_global::<SettingsStore>(move |cx| {
+ LIVE_SETTINGS.control_input_volume.store(
+ AudioSettings::get_global(cx).control_input_volume,
+ Ordering::Relaxed,
+ );
+ LIVE_SETTINGS.control_output_volume.store(
+ AudioSettings::get_global(cx).control_output_volume,
+ Ordering::Relaxed,
+ );
+ })
+ .detach();
+
+ let init_settings = AudioSettings::get_global(cx);
+ LIVE_SETTINGS
+ .control_input_volume
+ .store(init_settings.control_input_volume, Ordering::Relaxed);
+ LIVE_SETTINGS
+ .control_output_volume
+ .store(init_settings.control_output_volume, Ordering::Relaxed);
+ }
+}
+
+/// Allows access to settings from the audio thread. Updated by
+/// observer of SettingsStore.
+pub(crate) static LIVE_SETTINGS: LiveSettings = LiveSettings {
+ control_input_volume: AtomicBool::new(true),
+ control_output_volume: AtomicBool::new(true),
+};
@@ -0,0 +1,77 @@
+use anyhow::{Context, anyhow};
+use async_tar::{Builder, Header};
+use gpui::{BackgroundExecutor, Task};
+
+use collections::HashMap;
+use parking_lot::Mutex;
+use rodio::Source;
+use smol::fs::File;
+use std::{io, path::PathBuf, sync::Arc, time::Duration};
+
+use crate::{REPLAY_DURATION, rodio_ext::Replay};
+
+#[derive(Default, Clone)]
+pub(crate) struct Replays(Arc<Mutex<HashMap<String, Replay>>>);
+
+impl Replays {
+ pub(crate) fn add_voip_stream(&self, stream_name: String, source: Replay) {
+ let mut map = self.0.lock();
+ map.retain(|_, replay| replay.source_is_active());
+ map.insert(stream_name, source);
+ }
+
+ pub(crate) fn replays_to_tar(
+ &self,
+ executor: BackgroundExecutor,
+ ) -> Task<anyhow::Result<(PathBuf, Duration)>> {
+ let map = Arc::clone(&self.0);
+ executor.spawn(async move {
+ let recordings: Vec<_> = map
+ .lock()
+ .iter_mut()
+ .map(|(name, replay)| {
+ let queued = REPLAY_DURATION.min(replay.duration_ready());
+ (name.clone(), replay.take_duration(queued).record())
+ })
+ .collect();
+ let longest = recordings
+ .iter()
+ .map(|(_, r)| {
+ r.total_duration()
+ .expect("SamplesBuffer always returns a total duration")
+ })
+ .max()
+ .ok_or(anyhow!("There is no audio to capture"))?;
+
+ let path = std::env::current_dir()
+ .context("Could not get current dir")?
+ .join("replays.tar");
+ let tar = File::create(&path)
+ .await
+ .context("Could not create file for tar")?;
+
+ let mut tar = Builder::new(tar);
+
+ for (name, recording) in recordings {
+ let mut writer = io::Cursor::new(Vec::new());
+ rodio::wav_to_writer(recording, &mut writer).context("failed to encode wav")?;
+ let wav_data = writer.into_inner();
+ let path = name.replace(' ', "_") + ".wav";
+ let mut header = Header::new_gnu();
+ // rw permissions for everyone
+ header.set_mode(0o666);
+ header.set_size(wav_data.len() as u64);
+ tar.append_data(&mut header, path, wav_data.as_slice())
+ .await
+ .context("failed to apped wav to tar")?;
+ }
+ tar.into_inner()
+ .await
+ .context("Could not finish writing tar")?
+ .sync_all()
+ .await
+ .context("Could not flush tar file to disk")?;
+ Ok((path, longest))
+ })
+ }
+}
@@ -0,0 +1,593 @@
+use std::{
+ sync::{
+ Arc, Mutex,
+ atomic::{AtomicBool, Ordering},
+ },
+ time::Duration,
+};
+
+use crossbeam::queue::ArrayQueue;
+use rodio::{ChannelCount, Sample, SampleRate, Source};
+
+#[derive(Debug)]
+pub struct ReplayDurationTooShort;
+
+pub trait RodioExt: Source + Sized {
+ fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
+ where
+ F: FnMut(&mut [Sample; N]);
+ fn inspect_buffer<const N: usize, F>(self, callback: F) -> InspectBuffer<N, Self, F>
+ where
+ F: FnMut(&[Sample; N]);
+ fn replayable(
+ self,
+ duration: Duration,
+ ) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort>;
+ fn take_samples(self, n: usize) -> TakeSamples<Self>;
+}
+
+impl<S: Source> RodioExt for S {
+ fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
+ where
+ F: FnMut(&mut [Sample; N]),
+ {
+ ProcessBuffer {
+ inner: self,
+ callback,
+ buffer: [0.0; N],
+ next: N,
+ }
+ }
+ fn inspect_buffer<const N: usize, F>(self, callback: F) -> InspectBuffer<N, Self, F>
+ where
+ F: FnMut(&[Sample; N]),
+ {
+ InspectBuffer {
+ inner: self,
+ callback,
+ buffer: [0.0; N],
+ free: 0,
+ }
+ }
+ /// Maintains a live replay with a history of at least `duration` seconds.
+ ///
+ /// Note:
+ /// History can be 100ms longer if the source drops before or while the
+ /// replay is being read
+ ///
+ /// # Errors
+ /// If duration is smaller then 100ms
+ fn replayable(
+ self,
+ duration: Duration,
+ ) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort> {
+ if duration < Duration::from_millis(100) {
+ return Err(ReplayDurationTooShort);
+ }
+
+ let samples_per_second = self.sample_rate().get() as usize * self.channels().get() as usize;
+ let samples_to_queue = duration.as_secs_f64() * samples_per_second as f64;
+ let samples_to_queue =
+ (samples_to_queue as usize).next_multiple_of(self.channels().get().into());
+
+ let chunk_size =
+ (samples_per_second.div_ceil(10)).next_multiple_of(self.channels().get() as usize);
+ let chunks_to_queue = samples_to_queue.div_ceil(chunk_size);
+
+ let is_active = Arc::new(AtomicBool::new(true));
+ let queue = Arc::new(ReplayQueue::new(chunks_to_queue, chunk_size));
+ Ok((
+ Replay {
+ rx: Arc::clone(&queue),
+ buffer: Vec::new().into_iter(),
+ sleep_duration: duration / 2,
+ sample_rate: self.sample_rate(),
+ channel_count: self.channels(),
+ source_is_active: is_active.clone(),
+ },
+ Replayable {
+ tx: queue,
+ inner: self,
+ buffer: Vec::with_capacity(chunk_size),
+ chunk_size,
+ is_active,
+ },
+ ))
+ }
+ fn take_samples(self, n: usize) -> TakeSamples<S> {
+ TakeSamples {
+ inner: self,
+ left_to_take: n,
+ }
+ }
+}
+
+pub struct TakeSamples<S> {
+ inner: S,
+ left_to_take: usize,
+}
+
+impl<S: Source> Iterator for TakeSamples<S> {
+ type Item = Sample;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.left_to_take == 0 {
+ None
+ } else {
+ self.left_to_take -= 1;
+ self.inner.next()
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ (0, Some(self.left_to_take))
+ }
+}
+
+impl<S: Source> Source for TakeSamples<S> {
+ fn current_span_len(&self) -> Option<usize> {
+ None // does not support spans
+ }
+
+ fn channels(&self) -> ChannelCount {
+ self.inner.channels()
+ }
+
+ fn sample_rate(&self) -> SampleRate {
+ self.inner.sample_rate()
+ }
+
+ fn total_duration(&self) -> Option<Duration> {
+ Some(Duration::from_secs_f64(
+ self.left_to_take as f64
+ / self.sample_rate().get() as f64
+ / self.channels().get() as f64,
+ ))
+ }
+}
+
+#[derive(Debug)]
+struct ReplayQueue {
+ inner: ArrayQueue<Vec<Sample>>,
+ normal_chunk_len: usize,
+ /// The last chunk in the queue may be smaller then
+ /// the normal chunk size. This is always equal to the
+ /// size of the last element in the queue.
+ /// (so normally chunk_size)
+ last_chunk: Mutex<Vec<Sample>>,
+}
+
+impl ReplayQueue {
+ fn new(queue_len: usize, chunk_size: usize) -> Self {
+ Self {
+ inner: ArrayQueue::new(queue_len),
+ normal_chunk_len: chunk_size,
+ last_chunk: Mutex::new(Vec::new()),
+ }
+ }
+ /// Returns the length in samples
+ fn len(&self) -> usize {
+ self.inner.len().saturating_sub(1) * self.normal_chunk_len
+ + self
+ .last_chunk
+ .lock()
+ .expect("Self::push_last can not poison this lock")
+ .len()
+ }
+
+ fn pop(&self) -> Option<Vec<Sample>> {
+ self.inner.pop() // removes element that was inserted first
+ }
+
+ fn push_last(&self, mut samples: Vec<Sample>) {
+ let mut last_chunk = self
+ .last_chunk
+ .lock()
+ .expect("Self::len can not poison this lock");
+ std::mem::swap(&mut *last_chunk, &mut samples);
+ }
+
+ fn push_normal(&self, samples: Vec<Sample>) {
+ let _pushed_out_of_ringbuf = self.inner.force_push(samples);
+ }
+}
+
+pub struct ProcessBuffer<const N: usize, S, F>
+where
+ S: Source + Sized,
+ F: FnMut(&mut [Sample; N]),
+{
+ inner: S,
+ callback: F,
+ /// Buffer used for both input and output.
+ buffer: [Sample; N],
+ /// Next already processed sample is at this index
+ /// in buffer.
+ ///
+ /// If this is equal to the length of the buffer we have no more samples and
+ /// we must get new ones and process them
+ next: usize,
+}
+
+impl<const N: usize, S, F> Iterator for ProcessBuffer<N, S, F>
+where
+ S: Source + Sized,
+ F: FnMut(&mut [Sample; N]),
+{
+ type Item = Sample;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.next += 1;
+ if self.next < self.buffer.len() {
+ let sample = self.buffer[self.next];
+ return Some(sample);
+ }
+
+ for sample in &mut self.buffer {
+ *sample = self.inner.next()?
+ }
+ (self.callback)(&mut self.buffer);
+
+ self.next = 0;
+ Some(self.buffer[0])
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.inner.size_hint()
+ }
+}
+
+impl<const N: usize, S, F> Source for ProcessBuffer<N, S, F>
+where
+ S: Source + Sized,
+ F: FnMut(&mut [Sample; N]),
+{
+ fn current_span_len(&self) -> Option<usize> {
+ None
+ }
+
+ fn channels(&self) -> rodio::ChannelCount {
+ self.inner.channels()
+ }
+
+ fn sample_rate(&self) -> rodio::SampleRate {
+ self.inner.sample_rate()
+ }
+
+ fn total_duration(&self) -> Option<std::time::Duration> {
+ self.inner.total_duration()
+ }
+}
+
+pub struct InspectBuffer<const N: usize, S, F>
+where
+ S: Source + Sized,
+ F: FnMut(&[Sample; N]),
+{
+ inner: S,
+ callback: F,
+ /// Stores already emitted samples, once its full we call the callback.
+ buffer: [Sample; N],
+ /// Next free element in buffer. If this is equal to the buffer length
+ /// we have no more free lements.
+ free: usize,
+}
+
+impl<const N: usize, S, F> Iterator for InspectBuffer<N, S, F>
+where
+ S: Source + Sized,
+ F: FnMut(&[Sample; N]),
+{
+ type Item = Sample;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let Some(sample) = self.inner.next() else {
+ return None;
+ };
+
+ self.buffer[self.free] = sample;
+ self.free += 1;
+
+ if self.free == self.buffer.len() {
+ (self.callback)(&self.buffer);
+ self.free = 0
+ }
+
+ Some(sample)
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.inner.size_hint()
+ }
+}
+
+impl<const N: usize, S, F> Source for InspectBuffer<N, S, F>
+where
+ S: Source + Sized,
+ F: FnMut(&[Sample; N]),
+{
+ fn current_span_len(&self) -> Option<usize> {
+ None
+ }
+
+ fn channels(&self) -> rodio::ChannelCount {
+ self.inner.channels()
+ }
+
+ fn sample_rate(&self) -> rodio::SampleRate {
+ self.inner.sample_rate()
+ }
+
+ fn total_duration(&self) -> Option<std::time::Duration> {
+ self.inner.total_duration()
+ }
+}
+
+#[derive(Debug)]
+pub struct Replayable<S: Source> {
+ inner: S,
+ buffer: Vec<Sample>,
+ chunk_size: usize,
+ tx: Arc<ReplayQueue>,
+ is_active: Arc<AtomicBool>,
+}
+
+impl<S: Source> Iterator for Replayable<S> {
+ type Item = Sample;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some(sample) = self.inner.next() {
+ self.buffer.push(sample);
+ if self.buffer.len() == self.chunk_size {
+ self.tx.push_normal(std::mem::take(&mut self.buffer));
+ }
+ Some(sample)
+ } else {
+ let last_chunk = std::mem::take(&mut self.buffer);
+ self.tx.push_last(last_chunk);
+ self.is_active.store(false, Ordering::Relaxed);
+ None
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ self.inner.size_hint()
+ }
+}
+
+impl<S: Source> Source for Replayable<S> {
+ fn current_span_len(&self) -> Option<usize> {
+ self.inner.current_span_len()
+ }
+
+ fn channels(&self) -> ChannelCount {
+ self.inner.channels()
+ }
+
+ fn sample_rate(&self) -> SampleRate {
+ self.inner.sample_rate()
+ }
+
+ fn total_duration(&self) -> Option<Duration> {
+ self.inner.total_duration()
+ }
+}
+
+#[derive(Debug)]
+pub struct Replay {
+ rx: Arc<ReplayQueue>,
+ buffer: std::vec::IntoIter<Sample>,
+ sleep_duration: Duration,
+ sample_rate: SampleRate,
+ channel_count: ChannelCount,
+ source_is_active: Arc<AtomicBool>,
+}
+
+impl Replay {
+ pub fn source_is_active(&self) -> bool {
+ // - source could return None and not drop
+ // - source could be dropped before returning None
+ self.source_is_active.load(Ordering::Relaxed) && Arc::strong_count(&self.rx) < 2
+ }
+
+ /// Duration of what is in the buffer and can be returned without blocking.
+ pub fn duration_ready(&self) -> Duration {
+ let samples_per_second = self.channels().get() as u32 * self.sample_rate().get();
+
+ let seconds_queued = self.samples_ready() as f64 / samples_per_second as f64;
+ Duration::from_secs_f64(seconds_queued)
+ }
+
+ /// Number of samples in the buffer and can be returned without blocking.
+ pub fn samples_ready(&self) -> usize {
+ self.rx.len() + self.buffer.len()
+ }
+}
+
+impl Iterator for Replay {
+ type Item = Sample;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some(sample) = self.buffer.next() {
+ return Some(sample);
+ }
+
+ loop {
+ if let Some(new_buffer) = self.rx.pop() {
+ self.buffer = new_buffer.into_iter();
+ return self.buffer.next();
+ }
+
+ if !self.source_is_active() {
+ return None;
+ }
+
+ std::thread::sleep(self.sleep_duration);
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ ((self.rx.len() + self.buffer.len()), None)
+ }
+}
+
+impl Source for Replay {
+ fn current_span_len(&self) -> Option<usize> {
+ None // source is not compatible with spans
+ }
+
+ fn channels(&self) -> ChannelCount {
+ self.channel_count
+ }
+
+ fn sample_rate(&self) -> SampleRate {
+ self.sample_rate
+ }
+
+ fn total_duration(&self) -> Option<Duration> {
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use rodio::{nz, static_buffer::StaticSamplesBuffer};
+
+ use super::*;
+
+ const SAMPLES: [Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0];
+
+ fn test_source() -> StaticSamplesBuffer {
+ StaticSamplesBuffer::new(nz!(1), nz!(1), &SAMPLES)
+ }
+
+ mod process_buffer {
+ use super::*;
+
+ #[test]
+ fn callback_gets_all_samples() {
+ let input = test_source();
+
+ let _ = input
+ .process_buffer::<{ SAMPLES.len() }, _>(|buffer| assert_eq!(*buffer, SAMPLES))
+ .count();
+ }
+ #[test]
+ fn callback_modifies_yielded() {
+ let input = test_source();
+
+ let yielded: Vec<_> = input
+ .process_buffer::<{ SAMPLES.len() }, _>(|buffer| {
+ for sample in buffer {
+ *sample += 1.0;
+ }
+ })
+ .collect();
+ assert_eq!(
+ yielded,
+ SAMPLES.into_iter().map(|s| s + 1.0).collect::<Vec<_>>()
+ )
+ }
+ #[test]
+ fn source_truncates_to_whole_buffers() {
+ let input = test_source();
+
+ let yielded = input
+ .process_buffer::<3, _>(|buffer| assert_eq!(buffer, &SAMPLES[..3]))
+ .count();
+ assert_eq!(yielded, 3)
+ }
+ }
+
+ mod inspect_buffer {
+ use super::*;
+
+ #[test]
+ fn callback_gets_all_samples() {
+ let input = test_source();
+
+ let _ = input
+ .inspect_buffer::<{ SAMPLES.len() }, _>(|buffer| assert_eq!(*buffer, SAMPLES))
+ .count();
+ }
+ #[test]
+ fn source_does_not_truncate() {
+ let input = test_source();
+
+ let yielded = input
+ .inspect_buffer::<3, _>(|buffer| assert_eq!(buffer, &SAMPLES[..3]))
+ .count();
+ assert_eq!(yielded, SAMPLES.len())
+ }
+ }
+
+ mod instant_replay {
+ use super::*;
+
+ #[test]
+ fn continues_after_history() {
+ let input = test_source();
+
+ let (mut replay, mut source) = input
+ .replayable(Duration::from_secs(3))
+ .expect("longer then 100ms");
+
+ source.by_ref().take(3).count();
+ let yielded: Vec<Sample> = replay.by_ref().take(3).collect();
+ assert_eq!(&yielded, &SAMPLES[0..3],);
+
+ source.count();
+ let yielded: Vec<Sample> = replay.collect();
+ assert_eq!(&yielded, &SAMPLES[3..5],);
+ }
+
+ #[test]
+ fn keeps_only_latest() {
+ let input = test_source();
+
+ let (mut replay, mut source) = input
+ .replayable(Duration::from_secs(2))
+ .expect("longer then 100ms");
+
+ source.by_ref().take(5).count(); // get all items but do not end the source
+ let yielded: Vec<Sample> = replay.by_ref().take(2).collect();
+ assert_eq!(&yielded, &SAMPLES[3..5]);
+ source.count(); // exhaust source
+ assert_eq!(replay.next(), None);
+ }
+
+ #[test]
+ fn keeps_correct_amount_of_seconds() {
+ let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
+
+ let (replay, mut source) = input
+ .replayable(Duration::from_secs(2))
+ .expect("longer then 100ms");
+
+ // exhaust but do not yet end source
+ source.by_ref().take(40_000).count();
+
+ // take all samples we can without blocking
+ let ready = replay.samples_ready();
+ let n_yielded = replay.take_samples(ready).count();
+
+ let max = source.sample_rate().get() * source.channels().get() as u32 * 2;
+ let margin = 16_000 / 10; // 100ms
+ assert!(n_yielded as u32 >= max - margin);
+ }
+
+ #[test]
+ fn samples_ready() {
+ let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
+ let (mut replay, source) = input
+ .replayable(Duration::from_secs(2))
+ .expect("longer then 100ms");
+ assert_eq!(replay.by_ref().samples_ready(), 0);
+
+ source.take(8000).count(); // half a second
+ let margin = 16_000 / 10; // 100ms
+ let ready = replay.samples_ready();
+ assert!(ready >= 8000 - margin);
+ }
+ }
+}
@@ -10,7 +10,7 @@ use paths::remote_servers_dir;
use release_channel::{AppCommitSha, ReleaseChannel};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, SettingsStore};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
use smol::{fs, io::AsyncReadExt};
use smol::{fs::File, process::Command};
use std::{
@@ -118,14 +118,13 @@ struct AutoUpdateSetting(bool);
/// Whether or not to automatically check for updates.
///
/// Default: true
-#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
+#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)]
#[serde(transparent)]
+#[settings_key(key = "auto_update")]
struct AutoUpdateSettingContent(bool);
impl Settings for AutoUpdateSetting {
- const KEY: Option<&'static str> = Some("auto_update");
-
- type FileContent = Option<AutoUpdateSettingContent>;
+ type FileContent = AutoUpdateSettingContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let auto_update = [
@@ -135,17 +134,19 @@ impl Settings for AutoUpdateSetting {
sources.user,
]
.into_iter()
- .find_map(|value| value.copied().flatten())
- .unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
+ .find_map(|value| value.copied())
+ .unwrap_or(*sources.default);
Ok(Self(auto_update.0))
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
- vscode.enum_setting("update.mode", current, |s| match s {
+ let mut cur = &mut Some(*current);
+ vscode.enum_setting("update.mode", &mut cur, |s| match s {
"none" | "manual" => Some(AutoUpdateSettingContent(false)),
_ => Some(AutoUpdateSettingContent(true)),
});
+ *current = cur.unwrap();
}
}
@@ -543,7 +544,7 @@ impl AutoUpdater {
async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
let (client, installed_version, previous_status, release_channel) =
- this.read_with(&mut cx, |this, cx| {
+ this.read_with(&cx, |this, cx| {
(
this.http_client.clone(),
this.current_version,
@@ -128,23 +128,20 @@ mod windows_impl {
#[test]
fn test_parse_args() {
// launch can be specified via two separate arguments
- assert_eq!(parse_args(["--launch".into(), "true".into()]).launch, true);
- assert_eq!(
- parse_args(["--launch".into(), "false".into()]).launch,
- false
- );
+ assert!(parse_args(["--launch".into(), "true".into()]).launch);
+ assert!(!parse_args(["--launch".into(), "false".into()]).launch);
// launch can be specified via one single argument
- assert_eq!(parse_args(["--launch=true".into()]).launch, true);
- assert_eq!(parse_args(["--launch=false".into()]).launch, false);
+ assert!(parse_args(["--launch=true".into()]).launch);
+ assert!(!parse_args(["--launch=false".into()]).launch);
// launch defaults to true on no arguments
- assert_eq!(parse_args([]).launch, true);
+ assert!(parse_args([]).launch);
// launch defaults to true on invalid arguments
- assert_eq!(parse_args(["--launch".into()]).launch, true);
- assert_eq!(parse_args(["--launch=".into()]).launch, true);
- assert_eq!(parse_args(["--launch=invalid".into()]).launch, true);
+ assert!(parse_args(["--launch".into()]).launch);
+ assert!(parse_args(["--launch=".into()]).launch);
+ assert!(parse_args(["--launch=invalid".into()]).launch);
}
}
}
@@ -186,11 +186,11 @@ unsafe extern "system" fn wnd_proc(
}),
WM_TERMINATE => {
with_dialog_data(hwnd, |data| {
- if let Ok(result) = data.borrow_mut().rx.recv() {
- if let Err(e) = result {
- log::error!("Failed to update Zed: {:?}", e);
- show_error(format!("Error: {:?}", e));
- }
+ if let Ok(result) = data.borrow_mut().rx.recv()
+ && let Err(e) = result
+ {
+ log::error!("Failed to update Zed: {:?}", e);
+ show_error(format!("Error: {:?}", e));
}
});
unsafe { PostQuitMessage(0) };
@@ -16,7 +16,7 @@ use crate::windows_impl::WM_JOB_UPDATED;
type Job = fn(&Path) -> Result<()>;
#[cfg(not(test))]
-pub(crate) const JOBS: [Job; 6] = [
+pub(crate) const JOBS: &[Job] = &[
// Delete old files
|app_dir| {
let zed_executable = app_dir.join("Zed.exe");
@@ -32,6 +32,12 @@ pub(crate) const JOBS: [Job; 6] = [
std::fs::remove_file(&zed_cli)
.context(format!("Failed to remove old file {}", zed_cli.display()))
},
+ |app_dir| {
+ let zed_wsl = app_dir.join("bin\\zed");
+ log::info!("Removing old file: {}", zed_wsl.display());
+ std::fs::remove_file(&zed_wsl)
+ .context(format!("Failed to remove old file {}", zed_wsl.display()))
+ },
// Copy new files
|app_dir| {
let zed_executable_source = app_dir.join("install\\Zed.exe");
@@ -65,6 +71,22 @@ pub(crate) const JOBS: [Job; 6] = [
zed_cli_dest.display()
))
},
+ |app_dir| {
+ let zed_wsl_source = app_dir.join("install\\bin\\zed");
+ let zed_wsl_dest = app_dir.join("bin\\zed");
+ log::info!(
+ "Copying new file {} to {}",
+ zed_wsl_source.display(),
+ zed_wsl_dest.display()
+ );
+ std::fs::copy(&zed_wsl_source, &zed_wsl_dest)
+ .map(|_| ())
+ .context(format!(
+ "Failed to copy new file {} to {}",
+ zed_wsl_source.display(),
+ zed_wsl_dest.display()
+ ))
+ },
// Clean up installer folder and updates folder
|app_dir| {
let updates_folder = app_dir.join("updates");
@@ -85,16 +107,12 @@ pub(crate) const JOBS: [Job; 6] = [
];
#[cfg(test)]
-pub(crate) const JOBS: [Job; 2] = [
+pub(crate) const JOBS: &[Job] = &[
|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
- "err" => Err(std::io::Error::new(
- std::io::ErrorKind::Other,
- "Simulated error",
- ))
- .context("Anyhow!"),
+ "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
@@ -105,11 +123,7 @@ pub(crate) const JOBS: [Job; 2] = [
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
match config.as_str() {
- "err" => Err(std::io::Error::new(
- std::io::ErrorKind::Other,
- "Simulated error",
- ))
- .context("Anyhow!"),
+ "err" => Err(std::io::Error::other("Simulated error")).context("Anyhow!"),
_ => panic!("Unknown ZED_AUTO_UPDATE value: {}", config),
}
} else {
@@ -1,5 +1,4 @@
use auto_update::AutoUpdater;
-use client::proto::UpdateNotification;
use editor::{Editor, MultiBuffer};
use gpui::{App, Context, DismissEvent, Entity, Window, actions, prelude::*};
use http_client::HttpClient;
@@ -88,10 +87,7 @@ fn view_release_notes_locally(
.update_in(cx, |workspace, window, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
- let buffer = project.create_local_buffer("", markdown, cx);
- project
- .mark_buffer_as_non_searchable(buffer.read(cx).remote_id(), cx);
- buffer
+ project.create_local_buffer("", markdown, false, cx)
});
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx)
@@ -114,7 +110,7 @@ fn view_release_notes_locally(
cx,
);
workspace.add_item_to_active_pane(
- Box::new(markdown_preview.clone()),
+ Box::new(markdown_preview),
None,
true,
window,
@@ -141,6 +137,8 @@ pub fn notify_if_app_was_updated(cx: &mut App) {
return;
}
+ struct UpdateNotification;
+
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
cx.spawn(async move |cx| {
let should_show_notification = should_show_notification.await?;
@@ -3,6 +3,7 @@ mod models;
use anyhow::{Context, Error, Result, anyhow};
use aws_sdk_bedrockruntime as bedrock;
pub use aws_sdk_bedrockruntime as bedrock_client;
+use aws_sdk_bedrockruntime::types::InferenceConfiguration;
pub use aws_sdk_bedrockruntime::types::{
AnyToolChoice as BedrockAnyToolChoice, AutoToolChoice as BedrockAutoToolChoice,
ContentBlock as BedrockInnerContent, Tool as BedrockTool, ToolChoice as BedrockToolChoice,
@@ -17,7 +18,8 @@ pub use bedrock::types::{
ConverseOutput as BedrockResponse, ConverseStreamOutput as BedrockStreamingResponse,
ImageBlock as BedrockImageBlock, Message as BedrockMessage,
ReasoningContentBlock as BedrockThinkingBlock, ReasoningTextBlock as BedrockThinkingTextBlock,
- ResponseStream as BedrockResponseStream, ToolResultBlock as BedrockToolResultBlock,
+ ResponseStream as BedrockResponseStream, SystemContentBlock as BedrockSystemContentBlock,
+ ToolResultBlock as BedrockToolResultBlock,
ToolResultContentBlock as BedrockToolResultContentBlock,
ToolResultStatus as BedrockToolResultStatus, ToolUseBlock as BedrockToolUseBlock,
};
@@ -54,14 +56,24 @@ pub async fn stream_completion(
)])));
}
- if request
- .tools
- .as_ref()
- .map_or(false, |t| !t.tools.is_empty())
- {
+ if request.tools.as_ref().is_some_and(|t| !t.tools.is_empty()) {
response = response.set_tool_config(request.tools);
}
+ let inference_config = InferenceConfiguration::builder()
+ .max_tokens(request.max_tokens as i32)
+ .set_temperature(request.temperature)
+ .set_top_p(request.top_p)
+ .build();
+
+ response = response.inference_config(inference_config);
+
+ if let Some(system) = request.system {
+ if !system.is_empty() {
+ response = response.system(BedrockSystemContentBlock::Text(system));
+ }
+ }
+
let output = response
.send()
.await
@@ -151,12 +151,12 @@ impl Model {
pub fn id(&self) -> &str {
match self {
- Model::ClaudeSonnet4 => "claude-4-sonnet",
- Model::ClaudeSonnet4Thinking => "claude-4-sonnet-thinking",
- Model::ClaudeOpus4 => "claude-4-opus",
- Model::ClaudeOpus4_1 => "claude-4-opus-1",
- Model::ClaudeOpus4Thinking => "claude-4-opus-thinking",
- Model::ClaudeOpus4_1Thinking => "claude-4-opus-1-thinking",
+ Model::ClaudeSonnet4 => "claude-sonnet-4",
+ Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking",
+ Model::ClaudeOpus4 => "claude-opus-4",
+ Model::ClaudeOpus4_1 => "claude-opus-4-1",
+ Model::ClaudeOpus4Thinking => "claude-opus-4-thinking",
+ Model::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking",
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
Model::Claude3Opus => "claude-3-opus",
@@ -359,14 +359,12 @@ impl Model {
pub fn max_output_tokens(&self) -> u64 {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
- Self::Claude3_7Sonnet
- | Self::Claude3_7SonnetThinking
- | Self::ClaudeSonnet4
- | Self::ClaudeSonnet4Thinking
- | Self::ClaudeOpus4
- | Model::ClaudeOpus4Thinking
+ Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
+ Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
+ Self::ClaudeOpus4
+ | Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
- | Model::ClaudeOpus4_1Thinking => 128_000,
+ | Self::ClaudeOpus4_1Thinking => 32_000,
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
Self::Custom {
max_output_tokens, ..
@@ -784,10 +782,10 @@ mod tests {
);
// Test thinking models have different friendly IDs but same request IDs
- assert_eq!(Model::ClaudeSonnet4.id(), "claude-4-sonnet");
+ assert_eq!(Model::ClaudeSonnet4.id(), "claude-sonnet-4");
assert_eq!(
Model::ClaudeSonnet4Thinking.id(),
- "claude-4-sonnet-thinking"
+ "claude-sonnet-4-thinking"
);
assert_eq!(
Model::ClaudeSonnet4.request_id(),
@@ -82,11 +82,12 @@ impl Render for Breadcrumbs {
}
text_style.color = Color::Muted.color(cx);
- if index == 0 && !TabBarSettings::get_global(cx).show && active_item.is_dirty(cx) {
- if let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
- {
- return styled_element;
- }
+ if index == 0
+ && !TabBarSettings::get_global(cx).show
+ && active_item.is_dirty(cx)
+ && let Some(styled_element) = apply_dirty_filename_style(&segment, &text_style, cx)
+ {
+ return styled_element;
}
StyledText::new(segment.text.replace('\n', "⏎"))
@@ -231,7 +232,7 @@ fn apply_dirty_filename_style(
let highlight = vec![(filename_position..text.len(), highlight_style)];
Some(
StyledText::new(text)
- .with_default_highlights(&text_style, highlight)
+ .with_default_highlights(text_style, highlight)
.into_any(),
)
}
@@ -162,6 +162,22 @@ impl BufferDiffSnapshot {
}
}
+ fn unchanged(
+ buffer: &text::BufferSnapshot,
+ base_text: language::BufferSnapshot,
+ ) -> BufferDiffSnapshot {
+ debug_assert_eq!(buffer.text(), base_text.text());
+ BufferDiffSnapshot {
+ inner: BufferDiffInner {
+ base_text,
+ hunks: SumTree::new(buffer),
+ pending_hunks: SumTree::new(buffer),
+ base_text_exists: false,
+ },
+ secondary_diff: None,
+ }
+ }
+
fn new_with_base_text(
buffer: text::BufferSnapshot,
base_text: Option<Arc<String>>,
@@ -175,12 +191,8 @@ impl BufferDiffSnapshot {
if let Some(text) = &base_text {
let base_text_rope = Rope::from(text.as_str());
base_text_pair = Some((text.clone(), base_text_rope.clone()));
- let snapshot = language::Buffer::build_snapshot(
- base_text_rope,
- language.clone(),
- language_registry.clone(),
- cx,
- );
+ let snapshot =
+ language::Buffer::build_snapshot(base_text_rope, language, language_registry, cx);
base_text_snapshot = cx.background_spawn(snapshot);
base_text_exists = true;
} else {
@@ -217,7 +229,10 @@ impl BufferDiffSnapshot {
cx: &App,
) -> impl Future<Output = Self> + use<> {
let base_text_exists = base_text.is_some();
- let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone()));
+ let base_text_pair = base_text.map(|text| {
+ debug_assert_eq!(&*text, &base_text_snapshot.text());
+ (text, base_text_snapshot.as_rope().clone())
+ });
cx.background_executor()
.spawn_labeled(*CALCULATE_DIFF_TASK, async move {
Self {
@@ -572,14 +587,14 @@ impl BufferDiffInner {
pending_range.end.column = 0;
}
- if pending_range == (start_point..end_point) {
- if !buffer.has_edits_since_in_range(
+ if pending_range == (start_point..end_point)
+ && !buffer.has_edits_since_in_range(
&pending_hunk.buffer_version,
start_anchor..end_anchor,
- ) {
- has_pending = true;
- secondary_status = pending_hunk.new_status;
- }
+ )
+ {
+ has_pending = true;
+ secondary_status = pending_hunk.new_status;
}
}
@@ -877,6 +892,18 @@ impl BufferDiff {
}
}
+ pub fn new_unchanged(
+ buffer: &text::BufferSnapshot,
+ base_text: language::BufferSnapshot,
+ ) -> Self {
+ debug_assert_eq!(buffer.text(), base_text.text());
+ BufferDiff {
+ buffer_id: buffer.remote_id(),
+ inner: BufferDiffSnapshot::unchanged(buffer, base_text).inner,
+ secondary_diff: None,
+ }
+ }
+
#[cfg(any(test, feature = "test-support"))]
pub fn new_with_base_text(
base_text: &str,
@@ -928,7 +955,7 @@ impl BufferDiff {
let new_index_text = self.inner.stage_or_unstage_hunks_impl(
&self.secondary_diff.as_ref()?.read(cx).inner,
stage,
- &hunks,
+ hunks,
buffer,
file_exists,
);
@@ -952,12 +979,12 @@ impl BufferDiff {
cx: &App,
) -> Option<Range<Anchor>> {
let start = self
- .hunks_intersecting_range(range.clone(), &buffer, cx)
+ .hunks_intersecting_range(range.clone(), buffer, cx)
.next()?
.buffer_range
.start;
let end = self
- .hunks_intersecting_range_rev(range.clone(), &buffer)
+ .hunks_intersecting_range_rev(range, buffer)
.next()?
.buffer_range
.end;
@@ -1031,21 +1058,20 @@ impl BufferDiff {
&& state.base_text.syntax_update_count()
== new_state.base_text.syntax_update_count() =>
{
- (false, new_state.compare(&state, buffer))
+ (false, new_state.compare(state, buffer))
}
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
};
- if let Some(secondary_changed_range) = secondary_diff_change {
- if let Some(secondary_hunk_range) =
- self.range_to_hunk_range(secondary_changed_range, &buffer, cx)
- {
- if let Some(range) = &mut changed_range {
- range.start = secondary_hunk_range.start.min(&range.start, &buffer);
- range.end = secondary_hunk_range.end.max(&range.end, &buffer);
- } else {
- changed_range = Some(secondary_hunk_range);
- }
+ if let Some(secondary_changed_range) = secondary_diff_change
+ && let Some(secondary_hunk_range) =
+ self.range_to_hunk_range(secondary_changed_range, buffer, cx)
+ {
+ if let Some(range) = &mut changed_range {
+ range.start = secondary_hunk_range.start.min(&range.start, buffer);
+ range.end = secondary_hunk_range.end.max(&range.end, buffer);
+ } else {
+ changed_range = Some(secondary_hunk_range);
}
}
@@ -1057,8 +1083,8 @@ impl BufferDiff {
if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last())
{
if let Some(range) = &mut changed_range {
- range.start = range.start.min(&first.buffer_range.start, &buffer);
- range.end = range.end.max(&last.buffer_range.end, &buffer);
+ range.start = range.start.min(&first.buffer_range.start, buffer);
+ range.end = range.end.max(&last.buffer_range.end, buffer);
} else {
changed_range = Some(first.buffer_range.start..last.buffer_range.end);
}
@@ -1442,7 +1468,7 @@ mod tests {
.unindent();
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
- let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text.clone(), cx);
+ let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
let mut uncommitted_diff =
BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
@@ -1797,7 +1823,7 @@ mod tests {
uncommitted_diff.update(cx, |diff, cx| {
let hunks = diff
- .hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
+ .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
.collect::<Vec<_>>();
for hunk in &hunks {
assert_ne!(
@@ -1812,7 +1838,7 @@ mod tests {
.to_string();
let hunks = diff
- .hunks_intersecting_range(hunk_range.clone(), &buffer, &cx)
+ .hunks_intersecting_range(hunk_range.clone(), &buffer, cx)
.collect::<Vec<_>>();
for hunk in &hunks {
assert_eq!(
@@ -1870,7 +1896,7 @@ mod tests {
.to_string();
assert_eq!(new_index_text, buffer_text);
- let hunk = diff.hunks(&buffer, &cx).next().unwrap();
+ let hunk = diff.hunks(&buffer, cx).next().unwrap();
assert_eq!(
hunk.secondary_status,
DiffHunkSecondaryStatus::SecondaryHunkRemovalPending
@@ -1882,7 +1908,7 @@ mod tests {
.to_string();
assert_eq!(index_text, head_text);
- let hunk = diff.hunks(&buffer, &cx).next().unwrap();
+ let hunk = diff.hunks(&buffer, cx).next().unwrap();
// optimistically unstaged (fine, could also be HasSecondaryHunk)
assert_eq!(
hunk.secondary_status,
@@ -2018,10 +2044,10 @@ mod tests {
#[gpui::test(iterations = 100)]
async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
fn gen_line(rng: &mut StdRng) -> String {
- if rng.gen_bool(0.2) {
+ if rng.random_bool(0.2) {
"\n".to_owned()
} else {
- let c = rng.gen_range('A'..='Z');
+ let c = rng.random_range('A'..='Z');
format!("{c}{c}{c}\n")
}
}
@@ -2029,8 +2055,8 @@ mod tests {
fn gen_working_copy(rng: &mut StdRng, head: &str) -> String {
let mut old_lines = {
let mut old_lines = Vec::new();
- let mut old_lines_iter = head.lines();
- while let Some(line) = old_lines_iter.next() {
+ let old_lines_iter = head.lines();
+ for line in old_lines_iter {
assert!(!line.ends_with("\n"));
old_lines.push(line.to_owned());
}
@@ -2040,7 +2066,7 @@ mod tests {
old_lines.into_iter()
};
let mut result = String::new();
- let unchanged_count = rng.gen_range(0..=old_lines.len());
+ let unchanged_count = rng.random_range(0..=old_lines.len());
result +=
&old_lines
.by_ref()
@@ -2050,14 +2076,14 @@ mod tests {
s
});
while old_lines.len() > 0 {
- let deleted_count = rng.gen_range(0..=old_lines.len());
+ let deleted_count = rng.random_range(0..=old_lines.len());
let _advance = old_lines
.by_ref()
.take(deleted_count)
.map(|line| line.len() + 1)
.sum::<usize>();
let minimum_added = if deleted_count == 0 { 1 } else { 0 };
- let added_count = rng.gen_range(minimum_added..=5);
+ let added_count = rng.random_range(minimum_added..=5);
let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
result += &addition;
@@ -2066,7 +2092,8 @@ mod tests {
if blank_lines == old_lines.len() {
break;
};
- let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
+ let unchanged_count =
+ rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
result += &old_lines.by_ref().take(unchanged_count).fold(
String::new(),
|mut s, line| {
@@ -2123,7 +2150,7 @@ mod tests {
)
});
let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
- let mut index_text = if rng.r#gen() {
+ let mut index_text = if rng.random() {
Rope::from(head_text.as_str())
} else {
working_copy.as_rope().clone()
@@ -2134,12 +2161,12 @@ mod tests {
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
.collect::<Vec<_>>()
});
- if hunks.len() == 0 {
+ if hunks.is_empty() {
return;
}
for _ in 0..operations {
- let i = rng.gen_range(0..hunks.len());
+ let i = rng.random_range(0..hunks.len());
let hunk = &mut hunks[i];
let hunk_to_change = hunk.clone();
let stage = match hunk.secondary_status {
@@ -29,6 +29,7 @@ client.workspace = true
collections.workspace = true
fs.workspace = true
futures.workspace = true
+feature_flags.workspace = true
gpui = { workspace = true, features = ["screen-capture"] }
language.workspace = true
log.workspace = true
@@ -116,7 +116,7 @@ impl ActiveCall {
envelope: TypedEnvelope<proto::IncomingCall>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
- let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?;
+ let user_store = this.read_with(&cx, |this, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
@@ -147,7 +147,7 @@ impl ActiveCall {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
- .map_or(false, |call| call.room_id == envelope.payload.room_id)
+ .is_some_and(|call| call.room_id == envelope.payload.room_id)
{
incoming_call.take();
}
@@ -64,7 +64,7 @@ pub struct RemoteParticipant {
impl RemoteParticipant {
pub fn has_video_tracks(&self) -> bool {
- return !self.video_tracks.is_empty();
+ !self.video_tracks.is_empty()
}
pub fn can_write(&self) -> bool {
@@ -9,11 +9,12 @@ use client::{
proto::{self, PeerId},
};
use collections::{BTreeMap, HashMap, HashSet};
+use feature_flags::FeatureFlagAppExt;
use fs::Fs;
-use futures::{FutureExt, StreamExt};
+use futures::StreamExt;
use gpui::{
- App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, ScreenCaptureSource,
- ScreenCaptureStream, Task, WeakEntity,
+ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FutureExt as _,
+ ScreenCaptureSource, ScreenCaptureStream, Task, Timeout, WeakEntity,
};
use gpui_tokio::Tokio;
use language::LanguageRegistry;
@@ -370,57 +371,53 @@ impl Room {
})?;
// Wait for client to re-establish a connection to the server.
- {
- let mut reconnection_timeout =
- cx.background_executor().timer(RECONNECT_TIMEOUT).fuse();
- let client_reconnection = async {
- let mut remaining_attempts = 3;
- while remaining_attempts > 0 {
- if client_status.borrow().is_connected() {
- log::info!("client reconnected, attempting to rejoin room");
-
- let Some(this) = this.upgrade() else { break };
- match this.update(cx, |this, cx| this.rejoin(cx)) {
- Ok(task) => {
- if task.await.log_err().is_some() {
- return true;
- } else {
- remaining_attempts -= 1;
- }
+ let executor = cx.background_executor().clone();
+ let client_reconnection = async {
+ let mut remaining_attempts = 3;
+ while remaining_attempts > 0 {
+ if client_status.borrow().is_connected() {
+ log::info!("client reconnected, attempting to rejoin room");
+
+ let Some(this) = this.upgrade() else { break };
+ match this.update(cx, |this, cx| this.rejoin(cx)) {
+ Ok(task) => {
+ if task.await.log_err().is_some() {
+ return true;
+ } else {
+ remaining_attempts -= 1;
}
- Err(_app_dropped) => return false,
}
- } else if client_status.borrow().is_signed_out() {
- return false;
+ Err(_app_dropped) => return false,
}
-
- log::info!(
- "waiting for client status change, remaining attempts {}",
- remaining_attempts
- );
- client_status.next().await;
+ } else if client_status.borrow().is_signed_out() {
+ return false;
}
- false
+
+ log::info!(
+ "waiting for client status change, remaining attempts {}",
+ remaining_attempts
+ );
+ client_status.next().await;
}
- .fuse();
- futures::pin_mut!(client_reconnection);
-
- futures::select_biased! {
- reconnected = client_reconnection => {
- if reconnected {
- log::info!("successfully reconnected to room");
- // If we successfully joined the room, go back around the loop
- // waiting for future connection status changes.
- continue;
- }
- }
- _ = reconnection_timeout => {
- log::info!("room reconnection timeout expired");
- }
+ false
+ };
+
+ match client_reconnection
+ .with_timeout(RECONNECT_TIMEOUT, &executor)
+ .await
+ {
+ Ok(true) => {
+ log::info!("successfully reconnected to room");
+ // If we successfully joined the room, go back around the loop
+ // waiting for future connection status changes.
+ continue;
+ }
+ Ok(false) => break,
+ Err(Timeout) => {
+ log::info!("room reconnection timeout expired");
+ break;
}
}
-
- break;
}
}
@@ -831,24 +828,23 @@ impl Room {
);
Audio::play_sound(Sound::Joined, cx);
- if let Some(livekit_participants) = &livekit_participants {
- if let Some(livekit_participant) = livekit_participants
+ if let Some(livekit_participants) = &livekit_participants
+ && let Some(livekit_participant) = livekit_participants
.get(&ParticipantIdentity(user.id.to_string()))
+ {
+ for publication in
+ livekit_participant.track_publications().into_values()
{
- for publication in
- livekit_participant.track_publications().into_values()
- {
- if let Some(track) = publication.track() {
- this.livekit_room_updated(
- RoomEvent::TrackSubscribed {
- track,
- publication,
- participant: livekit_participant.clone(),
- },
- cx,
- )
- .warn_on_err();
- }
+ if let Some(track) = publication.track() {
+ this.livekit_room_updated(
+ RoomEvent::TrackSubscribed {
+ track,
+ publication,
+ participant: livekit_participant.clone(),
+ },
+ cx,
+ )
+ .warn_on_err();
}
}
}
@@ -944,10 +940,8 @@ impl Room {
self.client.user_id()
)
})?;
- if self.live_kit.as_ref().map_or(true, |kit| kit.deafened) {
- if publication.is_audio() {
- publication.set_enabled(false, cx);
- }
+ if self.live_kit.as_ref().is_none_or(|kit| kit.deafened) && publication.is_audio() {
+ publication.set_enabled(false, cx);
}
match track {
livekit_client::RemoteTrack::Audio(track) => {
@@ -1009,10 +1003,10 @@ impl Room {
for (sid, participant) in &mut self.remote_participants {
participant.speaking = speaker_ids.binary_search(sid).is_ok();
}
- if let Some(id) = self.client.user_id() {
- if let Some(room) = &mut self.live_kit {
- room.speaking = speaker_ids.binary_search(&id).is_ok();
- }
+ if let Some(id) = self.client.user_id()
+ && let Some(room) = &mut self.live_kit
+ {
+ room.speaking = speaker_ids.binary_search(&id).is_ok();
}
}
@@ -1046,18 +1040,16 @@ impl Room {
if let LocalTrack::Published {
track_publication, ..
} = &room.microphone_track
+ && track_publication.sid() == publication.sid()
{
- if track_publication.sid() == publication.sid() {
- room.microphone_track = LocalTrack::None;
- }
+ room.microphone_track = LocalTrack::None;
}
if let LocalTrack::Published {
track_publication, ..
} = &room.screen_track
+ && track_publication.sid() == publication.sid()
{
- if track_publication.sid() == publication.sid() {
- room.screen_track = LocalTrack::None;
- }
+ room.screen_track = LocalTrack::None;
}
}
}
@@ -1170,7 +1162,7 @@ impl Room {
let request = self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: project.read(cx).worktree_metadata_protos(cx),
- is_ssh_project: project.read(cx).is_via_ssh(),
+ is_ssh_project: project.read(cx).is_via_remote_server(),
});
cx.spawn(async move |this, cx| {
@@ -1182,7 +1174,7 @@ impl Room {
this.update(cx, |this, cx| {
this.shared_projects.insert(project.downgrade());
let active_project = this.local_participant.active_project.as_ref();
- if active_project.map_or(false, |location| *location == project) {
+ if active_project.is_some_and(|location| *location == project) {
this.set_location(Some(&project), cx)
} else {
Task::ready(Ok(()))
@@ -1255,9 +1247,9 @@ impl Room {
}
pub fn is_sharing_screen(&self) -> bool {
- self.live_kit.as_ref().map_or(false, |live_kit| {
- !matches!(live_kit.screen_track, LocalTrack::None)
- })
+ self.live_kit
+ .as_ref()
+ .is_some_and(|live_kit| !matches!(live_kit.screen_track, LocalTrack::None))
}
pub fn shared_screen_id(&self) -> Option<u64> {
@@ -1270,13 +1262,13 @@ impl Room {
}
pub fn is_sharing_mic(&self) -> bool {
- self.live_kit.as_ref().map_or(false, |live_kit| {
- !matches!(live_kit.microphone_track, LocalTrack::None)
- })
+ self.live_kit
+ .as_ref()
+ .is_some_and(|live_kit| !matches!(live_kit.microphone_track, LocalTrack::None))
}
pub fn is_muted(&self) -> bool {
- self.live_kit.as_ref().map_or(false, |live_kit| {
+ self.live_kit.as_ref().is_some_and(|live_kit| {
matches!(live_kit.microphone_track, LocalTrack::None)
|| live_kit.muted_by_user
|| live_kit.deafened
@@ -1286,13 +1278,13 @@ impl Room {
pub fn muted_by_user(&self) -> bool {
self.live_kit
.as_ref()
- .map_or(false, |live_kit| live_kit.muted_by_user)
+ .is_some_and(|live_kit| live_kit.muted_by_user)
}
pub fn is_speaking(&self) -> bool {
self.live_kit
.as_ref()
- .map_or(false, |live_kit| live_kit.speaking)
+ .is_some_and(|live_kit| live_kit.speaking)
}
pub fn is_deafened(&self) -> Option<bool> {
@@ -1331,8 +1323,18 @@ impl Room {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
+ let is_staff = cx.is_staff();
+ let user_name = self
+ .user_store
+ .read(cx)
+ .current_user()
+ .and_then(|user| user.name.clone())
+ .unwrap_or_else(|| "unknown".to_string());
+
cx.spawn(async move |this, cx| {
- let publication = room.publish_local_microphone_track(cx).await;
+ let publication = room
+ .publish_local_microphone_track(user_name, is_staff, cx)
+ .await;
this.update(cx, |this, cx| {
let live_kit = this
.live_kit
@@ -1488,10 +1490,8 @@ impl Room {
self.set_deafened(deafened, cx);
- if should_change_mute {
- if let Some(task) = self.set_mute(deafened, cx) {
- task.detach_and_log_err(cx);
- }
+ if should_change_mute && let Some(task) = self.set_mute(deafened, cx) {
+ task.detach_and_log_err(cx);
}
}
}
@@ -2,7 +2,7 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
#[derive(Deserialize, Debug)]
pub struct CallSettings {
@@ -11,7 +11,8 @@ pub struct CallSettings {
}
/// Configuration of voice calls in Zed.
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "calls")]
pub struct CallSettingsContent {
/// Whether the microphone should be muted when joining a channel or a call.
///
@@ -25,8 +26,6 @@ pub struct CallSettingsContent {
}
impl Settings for CallSettings {
- const KEY: Option<&'static str> = Some("calls");
-
type FileContent = CallSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -25,11 +25,9 @@ gpui.workspace = true
language.workspace = true
log.workspace = true
postage.workspace = true
-rand.workspace = true
release_channel.workspace = true
rpc.workspace = true
settings.workspace = true
-sum_tree.workspace = true
text.workspace = true
time.workspace = true
util.workspace = true
@@ -1,5 +1,4 @@
mod channel_buffer;
-mod channel_chat;
mod channel_store;
use client::{Client, UserStore};
@@ -7,10 +6,6 @@ use gpui::{App, Entity};
use std::sync::Arc;
pub use channel_buffer::{ACKNOWLEDGE_DEBOUNCE_INTERVAL, ChannelBuffer, ChannelBufferEvent};
-pub use channel_chat::{
- ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, MessageParams,
- mentions_to_proto,
-};
pub use channel_store::{Channel, ChannelEvent, ChannelMembership, ChannelStore};
#[cfg(test)]
@@ -19,5 +14,4 @@ mod channel_store_tests;
pub fn init(client: &Arc<Client>, user_store: Entity<UserStore>, cx: &mut App) {
channel_store::init(client, user_store, cx);
channel_buffer::init(&client.clone().into());
- channel_chat::init(&client.clone().into());
}
@@ -82,7 +82,7 @@ impl ChannelBuffer {
collaborators: Default::default(),
acknowledge_task: None,
channel_id: channel.id,
- subscription: Some(subscription.set_entity(&cx.entity(), &mut cx.to_async())),
+ subscription: Some(subscription.set_entity(&cx.entity(), &cx.to_async())),
user_store,
channel_store,
};
@@ -110,7 +110,7 @@ impl ChannelBuffer {
let Ok(subscription) = self.client.subscribe_to_entity(self.channel_id.0) else {
return;
};
- self.subscription = Some(subscription.set_entity(&cx.entity(), &mut cx.to_async()));
+ self.subscription = Some(subscription.set_entity(&cx.entity(), &cx.to_async()));
cx.emit(ChannelBufferEvent::Connected);
}
}
@@ -135,7 +135,7 @@ impl ChannelBuffer {
}
}
- for (_, old_collaborator) in &self.collaborators {
+ for old_collaborator in self.collaborators.values() {
if !new_collaborators.contains_key(&old_collaborator.peer_id) {
self.buffer.update(cx, |buffer, cx| {
buffer.remove_peer(old_collaborator.replica_id, cx)
@@ -191,12 +191,11 @@ impl ChannelBuffer {
operation,
is_local: true,
} => {
- if *ZED_ALWAYS_ACTIVE {
- if let language::Operation::UpdateSelections { selections, .. } = operation {
- if selections.is_empty() {
- return;
- }
- }
+ if *ZED_ALWAYS_ACTIVE
+ && let language::Operation::UpdateSelections { selections, .. } = operation
+ && selections.is_empty()
+ {
+ return;
}
let operation = language::proto::serialize_operation(operation);
self.client
@@ -1,862 +0,0 @@
-use crate::{Channel, ChannelStore};
-use anyhow::{Context as _, Result};
-use client::{
- ChannelId, Client, Subscription, TypedEnvelope, UserId, proto,
- user::{User, UserStore},
-};
-use collections::HashSet;
-use futures::lock::Mutex;
-use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity};
-use rand::prelude::*;
-use rpc::AnyProtoClient;
-use std::{
- ops::{ControlFlow, Range},
- sync::Arc,
-};
-use sum_tree::{Bias, Dimensions, SumTree};
-use time::OffsetDateTime;
-use util::{ResultExt as _, TryFutureExt, post_inc};
-
-pub struct ChannelChat {
- pub channel_id: ChannelId,
- messages: SumTree<ChannelMessage>,
- acknowledged_message_ids: HashSet<u64>,
- channel_store: Entity<ChannelStore>,
- loaded_all_messages: bool,
- last_acknowledged_id: Option<u64>,
- next_pending_message_id: usize,
- first_loaded_message_id: Option<u64>,
- user_store: Entity<UserStore>,
- rpc: Arc<Client>,
- outgoing_messages_lock: Arc<Mutex<()>>,
- rng: StdRng,
- _subscription: Subscription,
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub struct MessageParams {
- pub text: String,
- pub mentions: Vec<(Range<usize>, UserId)>,
- pub reply_to_message_id: Option<u64>,
-}
-
-#[derive(Clone, Debug)]
-pub struct ChannelMessage {
- pub id: ChannelMessageId,
- pub body: String,
- pub timestamp: OffsetDateTime,
- pub sender: Arc<User>,
- pub nonce: u128,
- pub mentions: Vec<(Range<usize>, UserId)>,
- pub reply_to_message_id: Option<u64>,
- pub edited_at: Option<OffsetDateTime>,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub enum ChannelMessageId {
- Saved(u64),
- Pending(usize),
-}
-
-impl From<ChannelMessageId> for Option<u64> {
- fn from(val: ChannelMessageId) -> Self {
- match val {
- ChannelMessageId::Saved(id) => Some(id),
- ChannelMessageId::Pending(_) => None,
- }
- }
-}
-
-#[derive(Clone, Debug, Default)]
-pub struct ChannelMessageSummary {
- max_id: ChannelMessageId,
- count: usize,
-}
-
-#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
-struct Count(usize);
-
-#[derive(Clone, Debug, PartialEq)]
-pub enum ChannelChatEvent {
- MessagesUpdated {
- old_range: Range<usize>,
- new_count: usize,
- },
- UpdateMessage {
- message_id: ChannelMessageId,
- message_ix: usize,
- },
- NewMessage {
- channel_id: ChannelId,
- message_id: u64,
- },
-}
-
-impl EventEmitter<ChannelChatEvent> for ChannelChat {}
-pub fn init(client: &AnyProtoClient) {
- client.add_entity_message_handler(ChannelChat::handle_message_sent);
- client.add_entity_message_handler(ChannelChat::handle_message_removed);
- client.add_entity_message_handler(ChannelChat::handle_message_updated);
-}
-
-impl ChannelChat {
- pub async fn new(
- channel: Arc<Channel>,
- channel_store: Entity<ChannelStore>,
- user_store: Entity<UserStore>,
- client: Arc<Client>,
- cx: &mut AsyncApp,
- ) -> Result<Entity<Self>> {
- let channel_id = channel.id;
- let subscription = client.subscribe_to_entity(channel_id.0).unwrap();
-
- let response = client
- .request(proto::JoinChannelChat {
- channel_id: channel_id.0,
- })
- .await?;
-
- let handle = cx.new(|cx| {
- cx.on_release(Self::release).detach();
- Self {
- channel_id: channel.id,
- user_store: user_store.clone(),
- channel_store,
- rpc: client.clone(),
- outgoing_messages_lock: Default::default(),
- messages: Default::default(),
- acknowledged_message_ids: Default::default(),
- loaded_all_messages: false,
- next_pending_message_id: 0,
- last_acknowledged_id: None,
- rng: StdRng::from_entropy(),
- first_loaded_message_id: None,
- _subscription: subscription.set_entity(&cx.entity(), &cx.to_async()),
- }
- })?;
- Self::handle_loaded_messages(
- handle.downgrade(),
- user_store,
- client,
- response.messages,
- response.done,
- cx,
- )
- .await?;
- Ok(handle)
- }
-
- fn release(&mut self, _: &mut App) {
- self.rpc
- .send(proto::LeaveChannelChat {
- channel_id: self.channel_id.0,
- })
- .log_err();
- }
-
- pub fn channel(&self, cx: &App) -> Option<Arc<Channel>> {
- self.channel_store
- .read(cx)
- .channel_for_id(self.channel_id)
- .cloned()
- }
-
- pub fn client(&self) -> &Arc<Client> {
- &self.rpc
- }
-
- pub fn send_message(
- &mut self,
- message: MessageParams,
- cx: &mut Context<Self>,
- ) -> Result<Task<Result<u64>>> {
- anyhow::ensure!(
- !message.text.trim().is_empty(),
- "message body can't be empty"
- );
-
- let current_user = self
- .user_store
- .read(cx)
- .current_user()
- .context("current_user is not present")?;
-
- let channel_id = self.channel_id;
- let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
- let nonce = self.rng.r#gen();
- self.insert_messages(
- SumTree::from_item(
- ChannelMessage {
- id: pending_id,
- body: message.text.clone(),
- sender: current_user,
- timestamp: OffsetDateTime::now_utc(),
- mentions: message.mentions.clone(),
- nonce,
- reply_to_message_id: message.reply_to_message_id,
- edited_at: None,
- },
- &(),
- ),
- cx,
- );
- let user_store = self.user_store.clone();
- let rpc = self.rpc.clone();
- let outgoing_messages_lock = self.outgoing_messages_lock.clone();
-
- // todo - handle messages that fail to send (e.g. >1024 chars)
- Ok(cx.spawn(async move |this, cx| {
- let outgoing_message_guard = outgoing_messages_lock.lock().await;
- let request = rpc.request(proto::SendChannelMessage {
- channel_id: channel_id.0,
- body: message.text,
- nonce: Some(nonce.into()),
- mentions: mentions_to_proto(&message.mentions),
- reply_to_message_id: message.reply_to_message_id,
- });
- let response = request.await?;
- drop(outgoing_message_guard);
- let response = response.message.context("invalid message")?;
- let id = response.id;
- let message = ChannelMessage::from_proto(response, &user_store, cx).await?;
- this.update(cx, |this, cx| {
- this.insert_messages(SumTree::from_item(message, &()), cx);
- if this.first_loaded_message_id.is_none() {
- this.first_loaded_message_id = Some(id);
- }
- })?;
- Ok(id)
- }))
- }
-
- pub fn remove_message(&mut self, id: u64, cx: &mut Context<Self>) -> Task<Result<()>> {
- let response = self.rpc.request(proto::RemoveChannelMessage {
- channel_id: self.channel_id.0,
- message_id: id,
- });
- cx.spawn(async move |this, cx| {
- response.await?;
- this.update(cx, |this, cx| {
- this.message_removed(id, cx);
- })?;
- Ok(())
- })
- }
-
- pub fn update_message(
- &mut self,
- id: u64,
- message: MessageParams,
- cx: &mut Context<Self>,
- ) -> Result<Task<Result<()>>> {
- self.message_update(
- ChannelMessageId::Saved(id),
- message.text.clone(),
- message.mentions.clone(),
- Some(OffsetDateTime::now_utc()),
- cx,
- );
-
- let nonce: u128 = self.rng.r#gen();
-
- let request = self.rpc.request(proto::UpdateChannelMessage {
- channel_id: self.channel_id.0,
- message_id: id,
- body: message.text,
- nonce: Some(nonce.into()),
- mentions: mentions_to_proto(&message.mentions),
- });
- Ok(cx.spawn(async move |_, _| {
- request.await?;
- Ok(())
- }))
- }
-
- pub fn load_more_messages(&mut self, cx: &mut Context<Self>) -> Option<Task<Option<()>>> {
- if self.loaded_all_messages {
- return None;
- }
-
- let rpc = self.rpc.clone();
- let user_store = self.user_store.clone();
- let channel_id = self.channel_id;
- let before_message_id = self.first_loaded_message_id()?;
- Some(cx.spawn(async move |this, cx| {
- async move {
- let response = rpc
- .request(proto::GetChannelMessages {
- channel_id: channel_id.0,
- before_message_id,
- })
- .await?;
- Self::handle_loaded_messages(
- this,
- user_store,
- rpc,
- response.messages,
- response.done,
- cx,
- )
- .await?;
-
- anyhow::Ok(())
- }
- .log_err()
- .await
- }))
- }
-
- pub fn first_loaded_message_id(&mut self) -> Option<u64> {
- self.first_loaded_message_id
- }
-
- /// Load a message by its id, if it's already stored locally.
- pub fn find_loaded_message(&self, id: u64) -> Option<&ChannelMessage> {
- self.messages.iter().find(|message| match message.id {
- ChannelMessageId::Saved(message_id) => message_id == id,
- ChannelMessageId::Pending(_) => false,
- })
- }
-
- /// Load all of the chat messages since a certain message id.
- ///
- /// For now, we always maintain a suffix of the channel's messages.
- pub async fn load_history_since_message(
- chat: Entity<Self>,
- message_id: u64,
- mut cx: AsyncApp,
- ) -> Option<usize> {
- loop {
- let step = chat
- .update(&mut cx, |chat, cx| {
- if let Some(first_id) = chat.first_loaded_message_id() {
- if first_id <= message_id {
- let mut cursor = chat
- .messages
- .cursor::<Dimensions<ChannelMessageId, Count>>(&());
- let message_id = ChannelMessageId::Saved(message_id);
- cursor.seek(&message_id, Bias::Left);
- return ControlFlow::Break(
- if cursor
- .item()
- .map_or(false, |message| message.id == message_id)
- {
- Some(cursor.start().1.0)
- } else {
- None
- },
- );
- }
- }
- ControlFlow::Continue(chat.load_more_messages(cx))
- })
- .log_err()?;
- match step {
- ControlFlow::Break(ix) => return ix,
- ControlFlow::Continue(task) => task?.await?,
- }
- }
- }
-
- pub fn acknowledge_last_message(&mut self, cx: &mut Context<Self>) {
- if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id {
- if self
- .last_acknowledged_id
- .map_or(true, |acknowledged_id| acknowledged_id < latest_message_id)
- {
- self.rpc
- .send(proto::AckChannelMessage {
- channel_id: self.channel_id.0,
- message_id: latest_message_id,
- })
- .ok();
- self.last_acknowledged_id = Some(latest_message_id);
- self.channel_store.update(cx, |store, cx| {
- store.acknowledge_message_id(self.channel_id, latest_message_id, cx);
- });
- }
- }
- }
-
- async fn handle_loaded_messages(
- this: WeakEntity<Self>,
- user_store: Entity<UserStore>,
- rpc: Arc<Client>,
- proto_messages: Vec<proto::ChannelMessage>,
- loaded_all_messages: bool,
- cx: &mut AsyncApp,
- ) -> Result<()> {
- let loaded_messages = messages_from_proto(proto_messages, &user_store, cx).await?;
-
- let first_loaded_message_id = loaded_messages.first().map(|m| m.id);
- let loaded_message_ids = this.read_with(cx, |this, _| {
- let mut loaded_message_ids: HashSet<u64> = HashSet::default();
- for message in loaded_messages.iter() {
- if let Some(saved_message_id) = message.id.into() {
- loaded_message_ids.insert(saved_message_id);
- }
- }
- for message in this.messages.iter() {
- if let Some(saved_message_id) = message.id.into() {
- loaded_message_ids.insert(saved_message_id);
- }
- }
- loaded_message_ids
- })?;
-
- let missing_ancestors = loaded_messages
- .iter()
- .filter_map(|message| {
- if let Some(ancestor_id) = message.reply_to_message_id {
- if !loaded_message_ids.contains(&ancestor_id) {
- return Some(ancestor_id);
- }
- }
- None
- })
- .collect::<Vec<_>>();
-
- let loaded_ancestors = if missing_ancestors.is_empty() {
- None
- } else {
- let response = rpc
- .request(proto::GetChannelMessagesById {
- message_ids: missing_ancestors,
- })
- .await?;
- Some(messages_from_proto(response.messages, &user_store, cx).await?)
- };
- this.update(cx, |this, cx| {
- this.first_loaded_message_id = first_loaded_message_id.and_then(|msg_id| msg_id.into());
- this.loaded_all_messages = loaded_all_messages;
- this.insert_messages(loaded_messages, cx);
- if let Some(loaded_ancestors) = loaded_ancestors {
- this.insert_messages(loaded_ancestors, cx);
- }
- })?;
-
- Ok(())
- }
-
- pub fn rejoin(&mut self, cx: &mut Context<Self>) {
- let user_store = self.user_store.clone();
- let rpc = self.rpc.clone();
- let channel_id = self.channel_id;
- cx.spawn(async move |this, cx| {
- async move {
- let response = rpc
- .request(proto::JoinChannelChat {
- channel_id: channel_id.0,
- })
- .await?;
- Self::handle_loaded_messages(
- this.clone(),
- user_store.clone(),
- rpc.clone(),
- response.messages,
- response.done,
- cx,
- )
- .await?;
-
- let pending_messages = this.read_with(cx, |this, _| {
- this.pending_messages().cloned().collect::<Vec<_>>()
- })?;
-
- for pending_message in pending_messages {
- let request = rpc.request(proto::SendChannelMessage {
- channel_id: channel_id.0,
- body: pending_message.body,
- mentions: mentions_to_proto(&pending_message.mentions),
- nonce: Some(pending_message.nonce.into()),
- reply_to_message_id: pending_message.reply_to_message_id,
- });
- let response = request.await?;
- let message = ChannelMessage::from_proto(
- response.message.context("invalid message")?,
- &user_store,
- cx,
- )
- .await?;
- this.update(cx, |this, cx| {
- this.insert_messages(SumTree::from_item(message, &()), cx);
- })?;
- }
-
- anyhow::Ok(())
- }
- .log_err()
- .await
- })
- .detach();
- }
-
- pub fn message_count(&self) -> usize {
- self.messages.summary().count
- }
-
- pub fn messages(&self) -> &SumTree<ChannelMessage> {
- &self.messages
- }
-
- pub fn message(&self, ix: usize) -> &ChannelMessage {
- let mut cursor = self.messages.cursor::<Count>(&());
- cursor.seek(&Count(ix), Bias::Right);
- cursor.item().unwrap()
- }
-
- pub fn acknowledge_message(&mut self, id: u64) {
- if self.acknowledged_message_ids.insert(id) {
- self.rpc
- .send(proto::AckChannelMessage {
- channel_id: self.channel_id.0,
- message_id: id,
- })
- .ok();
- }
- }
-
- pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
- let mut cursor = self.messages.cursor::<Count>(&());
- cursor.seek(&Count(range.start), Bias::Right);
- cursor.take(range.len())
- }
-
- pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
- let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
- cursor.seek(&ChannelMessageId::Pending(0), Bias::Left);
- cursor
- }
-
- async fn handle_message_sent(
- this: Entity<Self>,
- message: TypedEnvelope<proto::ChannelMessageSent>,
- mut cx: AsyncApp,
- ) -> Result<()> {
- let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?;
- let message = message.payload.message.context("empty message")?;
- let message_id = message.id;
-
- let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
- this.update(&mut cx, |this, cx| {
- this.insert_messages(SumTree::from_item(message, &()), cx);
- cx.emit(ChannelChatEvent::NewMessage {
- channel_id: this.channel_id,
- message_id,
- })
- })?;
-
- Ok(())
- }
-
- async fn handle_message_removed(
- this: Entity<Self>,
- message: TypedEnvelope<proto::RemoveChannelMessage>,
- mut cx: AsyncApp,
- ) -> Result<()> {
- this.update(&mut cx, |this, cx| {
- this.message_removed(message.payload.message_id, cx)
- })?;
- Ok(())
- }
-
- async fn handle_message_updated(
- this: Entity<Self>,
- message: TypedEnvelope<proto::ChannelMessageUpdate>,
- mut cx: AsyncApp,
- ) -> Result<()> {
- let user_store = this.read_with(&mut cx, |this, _| this.user_store.clone())?;
- let message = message.payload.message.context("empty message")?;
-
- let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
-
- this.update(&mut cx, |this, cx| {
- this.message_update(
- message.id,
- message.body,
- message.mentions,
- message.edited_at,
- cx,
- )
- })?;
- Ok(())
- }
-
- fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut Context<Self>) {
- if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
- let nonces = messages
- .cursor::<()>(&())
- .map(|m| m.nonce)
- .collect::<HashSet<_>>();
-
- let mut old_cursor = self
- .messages
- .cursor::<Dimensions<ChannelMessageId, Count>>(&());
- let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left);
- let start_ix = old_cursor.start().1.0;
- let removed_messages = old_cursor.slice(&last_message.id, Bias::Right);
- let removed_count = removed_messages.summary().count;
- let new_count = messages.summary().count;
- let end_ix = start_ix + removed_count;
-
- new_messages.append(messages, &());
-
- let mut ranges = Vec::<Range<usize>>::new();
- if new_messages.last().unwrap().is_pending() {
- new_messages.append(old_cursor.suffix(), &());
- } else {
- new_messages.append(
- old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left),
- &(),
- );
-
- while let Some(message) = old_cursor.item() {
- let message_ix = old_cursor.start().1.0;
- if nonces.contains(&message.nonce) {
- if ranges.last().map_or(false, |r| r.end == message_ix) {
- ranges.last_mut().unwrap().end += 1;
- } else {
- ranges.push(message_ix..message_ix + 1);
- }
- } else {
- new_messages.push(message.clone(), &());
- }
- old_cursor.next();
- }
- }
-
- drop(old_cursor);
- self.messages = new_messages;
-
- for range in ranges.into_iter().rev() {
- cx.emit(ChannelChatEvent::MessagesUpdated {
- old_range: range,
- new_count: 0,
- });
- }
- cx.emit(ChannelChatEvent::MessagesUpdated {
- old_range: start_ix..end_ix,
- new_count,
- });
-
- cx.notify();
- }
- }
-
- fn message_removed(&mut self, id: u64, cx: &mut Context<Self>) {
- let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
- let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left);
- if let Some(item) = cursor.item() {
- if item.id == ChannelMessageId::Saved(id) {
- let deleted_message_ix = messages.summary().count;
- cursor.next();
- messages.append(cursor.suffix(), &());
- drop(cursor);
- self.messages = messages;
-
- // If the message that was deleted was the last acknowledged message,
- // replace the acknowledged message with an earlier one.
- self.channel_store.update(cx, |store, _| {
- let summary = self.messages.summary();
- if summary.count == 0 {
- store.set_acknowledged_message_id(self.channel_id, None);
- } else if deleted_message_ix == summary.count {
- if let ChannelMessageId::Saved(id) = summary.max_id {
- store.set_acknowledged_message_id(self.channel_id, Some(id));
- }
- }
- });
-
- cx.emit(ChannelChatEvent::MessagesUpdated {
- old_range: deleted_message_ix..deleted_message_ix + 1,
- new_count: 0,
- });
- }
- }
- }
-
- fn message_update(
- &mut self,
- id: ChannelMessageId,
- body: String,
- mentions: Vec<(Range<usize>, u64)>,
- edited_at: Option<OffsetDateTime>,
- cx: &mut Context<Self>,
- ) {
- let mut cursor = self.messages.cursor::<ChannelMessageId>(&());
- let mut messages = cursor.slice(&id, Bias::Left);
- let ix = messages.summary().count;
-
- if let Some(mut message_to_update) = cursor.item().cloned() {
- message_to_update.body = body;
- message_to_update.mentions = mentions;
- message_to_update.edited_at = edited_at;
- messages.push(message_to_update, &());
- cursor.next();
- }
-
- messages.append(cursor.suffix(), &());
- drop(cursor);
- self.messages = messages;
-
- cx.emit(ChannelChatEvent::UpdateMessage {
- message_ix: ix,
- message_id: id,
- });
-
- cx.notify();
- }
-}
-
-async fn messages_from_proto(
- proto_messages: Vec<proto::ChannelMessage>,
- user_store: &Entity<UserStore>,
- cx: &mut AsyncApp,
-) -> Result<SumTree<ChannelMessage>> {
- let messages = ChannelMessage::from_proto_vec(proto_messages, user_store, cx).await?;
- let mut result = SumTree::default();
- result.extend(messages, &());
- Ok(result)
-}
-
-impl ChannelMessage {
- pub async fn from_proto(
- message: proto::ChannelMessage,
- user_store: &Entity<UserStore>,
- cx: &mut AsyncApp,
- ) -> Result<Self> {
- let sender = user_store
- .update(cx, |user_store, cx| {
- user_store.get_user(message.sender_id, cx)
- })?
- .await?;
-
- let edited_at = message.edited_at.and_then(|t| -> Option<OffsetDateTime> {
- if let Ok(a) = OffsetDateTime::from_unix_timestamp(t as i64) {
- return Some(a);
- }
-
- None
- });
-
- Ok(ChannelMessage {
- id: ChannelMessageId::Saved(message.id),
- body: message.body,
- mentions: message
- .mentions
- .into_iter()
- .filter_map(|mention| {
- let range = mention.range?;
- Some((range.start as usize..range.end as usize, mention.user_id))
- })
- .collect(),
- timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
- sender,
- nonce: message.nonce.context("nonce is required")?.into(),
- reply_to_message_id: message.reply_to_message_id,
- edited_at,
- })
- }
-
- pub fn is_pending(&self) -> bool {
- matches!(self.id, ChannelMessageId::Pending(_))
- }
-
- pub async fn from_proto_vec(
- proto_messages: Vec<proto::ChannelMessage>,
- user_store: &Entity<UserStore>,
- cx: &mut AsyncApp,
- ) -> Result<Vec<Self>> {
- let unique_user_ids = proto_messages
- .iter()
- .map(|m| m.sender_id)
- .collect::<HashSet<_>>()
- .into_iter()
- .collect();
- user_store
- .update(cx, |user_store, cx| {
- user_store.get_users(unique_user_ids, cx)
- })?
- .await?;
-
- let mut messages = Vec::with_capacity(proto_messages.len());
- for message in proto_messages {
- messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
- }
- Ok(messages)
- }
-}
-
-pub fn mentions_to_proto(mentions: &[(Range<usize>, UserId)]) -> Vec<proto::ChatMention> {
- mentions
- .iter()
- .map(|(range, user_id)| proto::ChatMention {
- range: Some(proto::Range {
- start: range.start as u64,
- end: range.end as u64,
- }),
- user_id: *user_id,
- })
- .collect()
-}
-
-impl sum_tree::Item for ChannelMessage {
- type Summary = ChannelMessageSummary;
-
- fn summary(&self, _cx: &()) -> Self::Summary {
- ChannelMessageSummary {
- max_id: self.id,
- count: 1,
- }
- }
-}
-
-impl Default for ChannelMessageId {
- fn default() -> Self {
- Self::Saved(0)
- }
-}
-
-impl sum_tree::Summary for ChannelMessageSummary {
- type Context = ();
-
- fn zero(_cx: &Self::Context) -> Self {
- Default::default()
- }
-
- fn add_summary(&mut self, summary: &Self, _: &()) {
- self.max_id = summary.max_id;
- self.count += summary.count;
- }
-}
-
-impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId {
- fn zero(_cx: &()) -> Self {
- Default::default()
- }
-
- fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
- debug_assert!(summary.max_id > *self);
- *self = summary.max_id;
- }
-}
-
-impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
- fn zero(_cx: &()) -> Self {
- Default::default()
- }
-
- fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
- self.0 += summary.count;
- }
-}
-
-impl<'a> From<&'a str> for MessageParams {
- fn from(value: &'a str) -> Self {
- Self {
- text: value.into(),
- mentions: Vec::new(),
- reply_to_message_id: None,
- }
- }
-}
@@ -1,6 +1,6 @@
mod channel_index;
-use crate::{ChannelMessage, channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
+use crate::channel_buffer::ChannelBuffer;
use anyhow::{Context as _, Result, anyhow};
use channel_index::ChannelIndex;
use client::{ChannelId, Client, ClientSettings, Subscription, User, UserId, UserStore};
@@ -41,7 +41,6 @@ pub struct ChannelStore {
outgoing_invites: HashSet<(ChannelId, UserId)>,
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
opened_buffers: HashMap<ChannelId, OpenEntityHandle<ChannelBuffer>>,
- opened_chats: HashMap<ChannelId, OpenEntityHandle<ChannelChat>>,
client: Arc<Client>,
did_subscribe: bool,
channels_loaded: (watch::Sender<bool>, watch::Receiver<bool>),
@@ -63,10 +62,8 @@ pub struct Channel {
#[derive(Default, Debug)]
pub struct ChannelState {
- latest_chat_message: Option<u64>,
latest_notes_version: NotesVersion,
observed_notes_version: NotesVersion,
- observed_chat_message: Option<u64>,
role: Option<ChannelRole>,
}
@@ -196,7 +193,6 @@ impl ChannelStore {
channel_participants: Default::default(),
outgoing_invites: Default::default(),
opened_buffers: Default::default(),
- opened_chats: Default::default(),
update_channels_tx,
client,
user_store,
@@ -262,13 +258,12 @@ impl ChannelStore {
}
}
status = status_receiver.next().fuse() => {
- if let Some(status) = status {
- if status.is_connected() {
+ if let Some(status) = status
+ && status.is_connected() {
this.update(cx, |this, _cx| {
this.initialize();
}).ok();
}
- }
continue;
}
_ = timer => {
@@ -336,10 +331,10 @@ impl ChannelStore {
}
pub fn has_open_channel_buffer(&self, channel_id: ChannelId, _cx: &App) -> bool {
- if let Some(buffer) = self.opened_buffers.get(&channel_id) {
- if let OpenEntityHandle::Open(buffer) = buffer {
- return buffer.upgrade().is_some();
- }
+ if let Some(buffer) = self.opened_buffers.get(&channel_id)
+ && let OpenEntityHandle::Open(buffer) = buffer
+ {
+ return buffer.upgrade().is_some();
}
false
}
@@ -363,90 +358,12 @@ impl ChannelStore {
)
}
- pub fn fetch_channel_messages(
- &self,
- message_ids: Vec<u64>,
- cx: &mut Context<Self>,
- ) -> Task<Result<Vec<ChannelMessage>>> {
- let request = if message_ids.is_empty() {
- None
- } else {
- Some(
- self.client
- .request(proto::GetChannelMessagesById { message_ids }),
- )
- };
- cx.spawn(async move |this, cx| {
- if let Some(request) = request {
- let response = request.await?;
- let this = this.upgrade().context("channel store dropped")?;
- let user_store = this.read_with(cx, |this, _| this.user_store.clone())?;
- ChannelMessage::from_proto_vec(response.messages, &user_store, cx).await
- } else {
- Ok(Vec::new())
- }
- })
- }
-
pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> bool {
self.channel_states
.get(&channel_id)
.is_some_and(|state| state.has_channel_buffer_changed())
}
- pub fn has_new_messages(&self, channel_id: ChannelId) -> bool {
- self.channel_states
- .get(&channel_id)
- .is_some_and(|state| state.has_new_messages())
- }
-
- pub fn set_acknowledged_message_id(&mut self, channel_id: ChannelId, message_id: Option<u64>) {
- if let Some(state) = self.channel_states.get_mut(&channel_id) {
- state.latest_chat_message = message_id;
- }
- }
-
- pub fn last_acknowledge_message_id(&self, channel_id: ChannelId) -> Option<u64> {
- self.channel_states.get(&channel_id).and_then(|state| {
- if let Some(last_message_id) = state.latest_chat_message {
- if state
- .last_acknowledged_message_id()
- .is_some_and(|id| id < last_message_id)
- {
- return state.last_acknowledged_message_id();
- }
- }
-
- None
- })
- }
-
- pub fn acknowledge_message_id(
- &mut self,
- channel_id: ChannelId,
- message_id: u64,
- cx: &mut Context<Self>,
- ) {
- self.channel_states
- .entry(channel_id)
- .or_default()
- .acknowledge_message_id(message_id);
- cx.notify();
- }
-
- pub fn update_latest_message_id(
- &mut self,
- channel_id: ChannelId,
- message_id: u64,
- cx: &mut Context<Self>,
- ) {
- self.channel_states
- .entry(channel_id)
- .or_default()
- .update_latest_message_id(message_id);
- cx.notify();
- }
-
pub fn acknowledge_notes_version(
&mut self,
channel_id: ChannelId,
@@ -475,23 +392,6 @@ impl ChannelStore {
cx.notify()
}
- pub fn open_channel_chat(
- &mut self,
- channel_id: ChannelId,
- cx: &mut Context<Self>,
- ) -> Task<Result<Entity<ChannelChat>>> {
- let client = self.client.clone();
- let user_store = self.user_store.clone();
- let this = cx.entity();
- self.open_channel_resource(
- channel_id,
- "chat",
- |this| &mut this.opened_chats,
- async move |channel, cx| ChannelChat::new(channel, this, user_store, client, cx).await,
- cx,
- )
- }
-
/// Asynchronously open a given resource associated with a channel.
///
/// Make sure that the resource is only opened once, even if this method
@@ -570,16 +470,14 @@ impl ChannelStore {
self.channel_index
.by_id()
.get(&channel_id)
- .map_or(false, |channel| channel.is_root_channel())
+ .is_some_and(|channel| channel.is_root_channel())
}
pub fn is_public_channel(&self, channel_id: ChannelId) -> bool {
self.channel_index
.by_id()
.get(&channel_id)
- .map_or(false, |channel| {
- channel.visibility == ChannelVisibility::Public
- })
+ .is_some_and(|channel| channel.visibility == ChannelVisibility::Public)
}
pub fn channel_capability(&self, channel_id: ChannelId) -> Capability {
@@ -910,9 +808,9 @@ impl ChannelStore {
async fn handle_update_channels(
this: Entity<Self>,
message: TypedEnvelope<proto::UpdateChannels>,
- mut cx: AsyncApp,
+ cx: AsyncApp,
) -> Result<()> {
- this.read_with(&mut cx, |this, _| {
+ this.read_with(&cx, |this, _| {
this.update_channels_tx
.unbounded_send(message.payload)
.unwrap();
@@ -935,13 +833,6 @@ impl ChannelStore {
cx,
);
}
- for message_id in message.payload.observed_channel_message_id {
- this.acknowledge_message_id(
- ChannelId(message_id.channel_id),
- message_id.message_id,
- cx,
- );
- }
for membership in message.payload.channel_memberships {
if let Some(role) = ChannelRole::from_i32(membership.role) {
this.channel_states
@@ -961,28 +852,18 @@ impl ChannelStore {
self.outgoing_invites.clear();
self.disconnect_channel_buffers_task.take();
- for chat in self.opened_chats.values() {
- if let OpenEntityHandle::Open(chat) = chat {
- if let Some(chat) = chat.upgrade() {
- chat.update(cx, |chat, cx| {
- chat.rejoin(cx);
- });
- }
- }
- }
-
let mut buffer_versions = Vec::new();
for buffer in self.opened_buffers.values() {
- if let OpenEntityHandle::Open(buffer) = buffer {
- if let Some(buffer) = buffer.upgrade() {
- let channel_buffer = buffer.read(cx);
- let buffer = channel_buffer.buffer().read(cx);
- buffer_versions.push(proto::ChannelBufferVersion {
- channel_id: channel_buffer.channel_id.0,
- epoch: channel_buffer.epoch(),
- version: language::proto::serialize_version(&buffer.version()),
- });
- }
+ if let OpenEntityHandle::Open(buffer) = buffer
+ && let Some(buffer) = buffer.upgrade()
+ {
+ let channel_buffer = buffer.read(cx);
+ let buffer = channel_buffer.buffer().read(cx);
+ buffer_versions.push(proto::ChannelBufferVersion {
+ channel_id: channel_buffer.channel_id.0,
+ epoch: channel_buffer.epoch(),
+ version: language::proto::serialize_version(&buffer.version()),
+ });
}
}
@@ -1077,11 +958,11 @@ impl ChannelStore {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
- for (_, buffer) in &this.opened_buffers {
- if let OpenEntityHandle::Open(buffer) = &buffer {
- if let Some(buffer) = buffer.upgrade() {
- buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
- }
+ for buffer in this.opened_buffers.values() {
+ if let OpenEntityHandle::Open(buffer) = &buffer
+ && let Some(buffer) = buffer.upgrade()
+ {
+ buffer.update(cx, |buffer, cx| buffer.disconnect(cx));
}
}
})
@@ -1098,7 +979,6 @@ impl ChannelStore {
self.channel_participants.clear();
self.outgoing_invites.clear();
self.opened_buffers.clear();
- self.opened_chats.clear();
self.disconnect_channel_buffers_task = None;
self.channel_states.clear();
}
@@ -1135,7 +1015,6 @@ impl ChannelStore {
let channels_changed = !payload.channels.is_empty()
|| !payload.delete_channels.is_empty()
- || !payload.latest_channel_message_ids.is_empty()
|| !payload.latest_channel_buffer_versions.is_empty();
if channels_changed {
@@ -1157,10 +1036,9 @@ impl ChannelStore {
}
if let Some(OpenEntityHandle::Open(buffer)) =
self.opened_buffers.remove(&channel_id)
+ && let Some(buffer) = buffer.upgrade()
{
- if let Some(buffer) = buffer.upgrade() {
- buffer.update(cx, ChannelBuffer::disconnect);
- }
+ buffer.update(cx, ChannelBuffer::disconnect);
}
}
}
@@ -1170,12 +1048,11 @@ impl ChannelStore {
let id = ChannelId(channel.id);
let channel_changed = index.insert(channel);
- if channel_changed {
- if let Some(OpenEntityHandle::Open(buffer)) = self.opened_buffers.get(&id) {
- if let Some(buffer) = buffer.upgrade() {
- buffer.update(cx, ChannelBuffer::channel_changed);
- }
- }
+ if channel_changed
+ && let Some(OpenEntityHandle::Open(buffer)) = self.opened_buffers.get(&id)
+ && let Some(buffer) = buffer.upgrade()
+ {
+ buffer.update(cx, ChannelBuffer::channel_changed);
}
}
@@ -1187,13 +1064,6 @@ impl ChannelStore {
.update_latest_notes_version(latest_buffer_version.epoch, &version)
}
- for latest_channel_message in payload.latest_channel_message_ids {
- self.channel_states
- .entry(ChannelId(latest_channel_message.channel_id))
- .or_default()
- .update_latest_message_id(latest_channel_message.message_id);
- }
-
self.channels_loaded.0.try_send(true).log_err();
}
@@ -1257,29 +1127,6 @@ impl ChannelState {
.changed_since(&self.observed_notes_version.version))
}
- fn has_new_messages(&self) -> bool {
- let latest_message_id = self.latest_chat_message;
- let observed_message_id = self.observed_chat_message;
-
- latest_message_id.is_some_and(|latest_message_id| {
- latest_message_id > observed_message_id.unwrap_or_default()
- })
- }
-
- fn last_acknowledged_message_id(&self) -> Option<u64> {
- self.observed_chat_message
- }
-
- fn acknowledge_message_id(&mut self, message_id: u64) {
- let observed = self.observed_chat_message.get_or_insert(message_id);
- *observed = (*observed).max(message_id);
- }
-
- fn update_latest_message_id(&mut self, message_id: u64) {
- self.latest_chat_message =
- Some(message_id.max(self.latest_chat_message.unwrap_or_default()));
- }
-
fn acknowledge_notes_version(&mut self, epoch: u64, version: &clock::Global) {
if self.observed_notes_version.epoch == epoch {
self.observed_notes_version.version.join(version);
@@ -1,9 +1,7 @@
-use crate::channel_chat::ChannelChatEvent;
-
use super::*;
-use client::{Client, UserStore, test::FakeServer};
+use client::{Client, UserStore};
use clock::FakeSystemClock;
-use gpui::{App, AppContext as _, Entity, SemanticVersion, TestAppContext};
+use gpui::{App, AppContext as _, Entity, SemanticVersion};
use http_client::FakeHttpClient;
use rpc::proto::{self};
use settings::SettingsStore;
@@ -235,201 +233,6 @@ fn test_dangling_channel_paths(cx: &mut App) {
assert_channels(&channel_store, &[(0, "a".to_string())], cx);
}
-#[gpui::test]
-async fn test_channel_messages(cx: &mut TestAppContext) {
- let user_id = 5;
- let channel_id = 5;
- let channel_store = cx.update(init_test);
- let client = channel_store.read_with(cx, |s, _| s.client());
- let server = FakeServer::for_client(user_id, &client, cx).await;
-
- // Get the available channels.
- server.send(proto::UpdateChannels {
- channels: vec![proto::Channel {
- id: channel_id,
- name: "the-channel".to_string(),
- visibility: proto::ChannelVisibility::Members as i32,
- parent_path: vec![],
- channel_order: 1,
- }],
- ..Default::default()
- });
- cx.executor().run_until_parked();
- cx.update(|cx| {
- assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx);
- });
-
- // Join a channel and populate its existing messages.
- let channel = channel_store.update(cx, |store, cx| {
- let channel_id = store.ordered_channels().next().unwrap().1.id;
- store.open_channel_chat(channel_id, cx)
- });
- let join_channel = server.receive::<proto::JoinChannelChat>().await.unwrap();
- server.respond(
- join_channel.receipt(),
- proto::JoinChannelChatResponse {
- messages: vec![
- proto::ChannelMessage {
- id: 10,
- body: "a".into(),
- timestamp: 1000,
- sender_id: 5,
- mentions: vec![],
- nonce: Some(1.into()),
- reply_to_message_id: None,
- edited_at: None,
- },
- proto::ChannelMessage {
- id: 11,
- body: "b".into(),
- timestamp: 1001,
- sender_id: 6,
- mentions: vec![],
- nonce: Some(2.into()),
- reply_to_message_id: None,
- edited_at: None,
- },
- ],
- done: false,
- },
- );
-
- cx.executor().start_waiting();
-
- // Client requests all users for the received messages
- let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
- get_users.payload.user_ids.sort();
- assert_eq!(get_users.payload.user_ids, vec![6]);
- server.respond(
- get_users.receipt(),
- proto::UsersResponse {
- users: vec![proto::User {
- id: 6,
- github_login: "maxbrunsfeld".into(),
- avatar_url: "http://avatar.com/maxbrunsfeld".into(),
- name: None,
- }],
- },
- );
-
- let channel = channel.await.unwrap();
- channel.update(cx, |channel, _| {
- assert_eq!(
- channel
- .messages_in_range(0..2)
- .map(|message| (message.sender.github_login.clone(), message.body.clone()))
- .collect::<Vec<_>>(),
- &[
- ("user-5".into(), "a".into()),
- ("maxbrunsfeld".into(), "b".into())
- ]
- );
- });
-
- // Receive a new message.
- server.send(proto::ChannelMessageSent {
- channel_id,
- message: Some(proto::ChannelMessage {
- id: 12,
- body: "c".into(),
- timestamp: 1002,
- sender_id: 7,
- mentions: vec![],
- nonce: Some(3.into()),
- reply_to_message_id: None,
- edited_at: None,
- }),
- });
-
- // Client requests user for message since they haven't seen them yet
- let get_users = server.receive::<proto::GetUsers>().await.unwrap();
- assert_eq!(get_users.payload.user_ids, vec![7]);
- server.respond(
- get_users.receipt(),
- proto::UsersResponse {
- users: vec![proto::User {
- id: 7,
- github_login: "as-cii".into(),
- avatar_url: "http://avatar.com/as-cii".into(),
- name: None,
- }],
- },
- );
-
- assert_eq!(
- channel.next_event(cx).await,
- ChannelChatEvent::MessagesUpdated {
- old_range: 2..2,
- new_count: 1,
- }
- );
- channel.update(cx, |channel, _| {
- assert_eq!(
- channel
- .messages_in_range(2..3)
- .map(|message| (message.sender.github_login.clone(), message.body.clone()))
- .collect::<Vec<_>>(),
- &[("as-cii".into(), "c".into())]
- )
- });
-
- // Scroll up to view older messages.
- channel.update(cx, |channel, cx| {
- channel.load_more_messages(cx).unwrap().detach();
- });
- let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
- assert_eq!(get_messages.payload.channel_id, 5);
- assert_eq!(get_messages.payload.before_message_id, 10);
- server.respond(
- get_messages.receipt(),
- proto::GetChannelMessagesResponse {
- done: true,
- messages: vec![
- proto::ChannelMessage {
- id: 8,
- body: "y".into(),
- timestamp: 998,
- sender_id: 5,
- nonce: Some(4.into()),
- mentions: vec![],
- reply_to_message_id: None,
- edited_at: None,
- },
- proto::ChannelMessage {
- id: 9,
- body: "z".into(),
- timestamp: 999,
- sender_id: 6,
- nonce: Some(5.into()),
- mentions: vec![],
- reply_to_message_id: None,
- edited_at: None,
- },
- ],
- },
- );
-
- assert_eq!(
- channel.next_event(cx).await,
- ChannelChatEvent::MessagesUpdated {
- old_range: 0..0,
- new_count: 2,
- }
- );
- channel.update(cx, |channel, _| {
- assert_eq!(
- channel
- .messages_in_range(0..2)
- .map(|message| (message.sender.github_login.clone(), message.body.clone()))
- .collect::<Vec<_>>(),
- &[
- ("user-5".into(), "y".into()),
- ("maxbrunsfeld".into(), "z".into())
- ]
- );
- });
-}
-
fn init_test(cx: &mut App) -> Entity<ChannelStore> {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
@@ -438,7 +241,7 @@ fn init_test(cx: &mut App) -> Entity<ChannelStore> {
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_404_response();
- let client = Client::new(clock, http.clone(), cx);
+ let client = Client::new(clock, http, cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
client::init(&client, cx);
@@ -14,6 +14,7 @@ pub enum CliRequest {
paths: Vec<String>,
urls: Vec<String>,
diff_paths: Vec<[String; 2]>,
+ wsl: Option<String>,
wait: bool,
open_new_workspace: Option<bool>,
env: Option<HashMap<String, String>>,
@@ -6,7 +6,6 @@
use anyhow::{Context as _, Result};
use clap::Parser;
use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
-use collections::HashMap;
use parking_lot::Mutex;
use std::{
env, fs, io,
@@ -85,6 +84,18 @@ struct Args {
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
+ /// The username and WSL distribution to use when opening paths. If not specified,
+ /// Zed will attempt to open the paths directly.
+ ///
+ /// The username is optional, and if not specified, the default user for the distribution
+ /// will be used.
+ ///
+ /// Example: `me@Ubuntu` or `Ubuntu`.
+ ///
+ /// WARN: You should not fill in this field by hand.
+ #[cfg(target_os = "windows")]
+ #[arg(long, value_name = "USER@DISTRO")]
+ wsl: Option<String>,
/// Not supported in Zed CLI, only supported on Zed binary
/// Will attempt to give the correct command to run
#[arg(long)]
@@ -129,14 +140,41 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
}
-fn main() -> Result<()> {
- #[cfg(all(not(debug_assertions), target_os = "windows"))]
- unsafe {
- use ::windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
+fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
+ let mut command = util::command::new_std_command("wsl.exe");
- let _ = AttachConsole(ATTACH_PARENT_PROCESS);
+ let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
+ if user.is_empty() {
+ anyhow::bail!("user is empty in wsl argument");
+ }
+ (Some(user), distro)
+ } else {
+ (None, wsl)
+ };
+
+ if let Some(user) = user {
+ command.arg("--user").arg(user);
}
+ let output = command
+ .arg("--distribution")
+ .arg(distro_name)
+ .arg("wslpath")
+ .arg("-m")
+ .arg(source)
+ .output()?;
+
+ let result = String::from_utf8_lossy(&output.stdout);
+ let prefix = format!("//wsl.localhost/{}", distro_name);
+
+ Ok(result
+ .trim()
+ .strip_prefix(&prefix)
+ .unwrap_or(&result)
+ .to_string())
+}
+
+fn main() -> Result<()> {
#[cfg(unix)]
util::prevent_root_execution();
@@ -223,6 +261,8 @@ fn main() -> Result<()> {
let env = {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
+ use collections::HashMap;
+
// On Linux, the desktop entry uses `cli` to spawn `zed`.
// We need to handle env vars correctly since std::env::vars() may not contain
// project-specific vars (e.g. those set by direnv).
@@ -235,8 +275,19 @@ fn main() -> Result<()> {
}
}
- #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
- Some(std::env::vars().collect::<HashMap<_, _>>())
+ #[cfg(target_os = "windows")]
+ {
+ // On Windows, by default, a child process inherits a copy of the environment block of the parent process.
+ // So we don't need to pass env vars explicitly.
+ None
+ }
+
+ #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
+ {
+ use collections::HashMap;
+
+ Some(std::env::vars().collect::<HashMap<_, _>>())
+ }
};
let exit_status = Arc::new(Mutex::new(None));
@@ -253,6 +304,11 @@ fn main() -> Result<()> {
]);
}
+ #[cfg(target_os = "windows")]
+ let wsl = args.wsl.as_ref();
+ #[cfg(not(target_os = "windows"))]
+ let wsl = None;
+
for path in args.paths_with_position.iter() {
if path.starts_with("zed://")
|| path.starts_with("http://")
@@ -271,8 +327,10 @@ fn main() -> Result<()> {
paths.push(tmp_file.path().to_string_lossy().to_string());
let (tmp_file, _) = tmp_file.keep()?;
anonymous_fd_tmp_files.push((file, tmp_file));
+ } else if let Some(wsl) = wsl {
+ urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?));
} else {
- paths.push(parse_path_with_position(path)?)
+ paths.push(parse_path_with_position(path)?);
}
}
@@ -288,10 +346,16 @@ fn main() -> Result<()> {
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
let (tx, rx) = (handshake.requests, handshake.responses);
+ #[cfg(target_os = "windows")]
+ let wsl = args.wsl;
+ #[cfg(not(target_os = "windows"))]
+ let wsl = None;
+
tx.send(CliRequest::Open {
paths,
urls,
diff_paths,
+ wsl,
wait: args.wait,
open_new_workspace,
env,
@@ -363,7 +427,7 @@ fn anonymous_fd(path: &str) -> Option<fs::File> {
let fd: fd::RawFd = fd_str.parse().ok()?;
let file = unsafe { fs::File::from_raw_fd(fd) };
- return Some(file);
+ Some(file)
}
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
{
@@ -381,13 +445,13 @@ fn anonymous_fd(path: &str) -> Option<fs::File> {
}
let fd: fd::RawFd = fd_str.parse().ok()?;
let file = unsafe { fs::File::from_raw_fd(fd) };
- return Some(file);
+ Some(file)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
{
_ = path;
// not implemented for bsd, windows. Could be, but isn't yet
- return None;
+ None
}
}
@@ -494,11 +558,11 @@ mod linux {
Ok(Fork::Parent(_)) => Ok(()),
Ok(Fork::Child) => {
unsafe { std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "") };
- if let Err(_) = fork::setsid() {
+ if fork::setsid().is_err() {
eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
process::exit(1);
}
- if let Err(_) = fork::close_fd() {
+ if fork::close_fd().is_err() {
eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
}
let error =
@@ -518,11 +582,11 @@ mod linux {
) -> Result<(), std::io::Error> {
for _ in 0..100 {
thread::sleep(Duration::from_millis(10));
- if sock.connect_addr(&sock_addr).is_ok() {
+ if sock.connect_addr(sock_addr).is_ok() {
return Ok(());
}
}
- sock.connect_addr(&sock_addr)
+ sock.connect_addr(sock_addr)
}
}
}
@@ -534,8 +598,8 @@ mod flatpak {
use std::process::Command;
use std::{env, process};
- const EXTRA_LIB_ENV_NAME: &'static str = "ZED_FLATPAK_LIB_PATH";
- const NO_ESCAPE_ENV_NAME: &'static str = "ZED_FLATPAK_NO_ESCAPE";
+ const EXTRA_LIB_ENV_NAME: &str = "ZED_FLATPAK_LIB_PATH";
+ const NO_ESCAPE_ENV_NAME: &str = "ZED_FLATPAK_NO_ESCAPE";
/// Adds bundled libraries to LD_LIBRARY_PATH if running under flatpak
pub fn ld_extra_libs() {
@@ -586,14 +650,11 @@ mod flatpak {
pub fn set_bin_if_no_escape(mut args: super::Args) -> super::Args {
if env::var(NO_ESCAPE_ENV_NAME).is_ok()
- && env::var("FLATPAK_ID").map_or(false, |id| id.starts_with("dev.zed.Zed"))
+ && env::var("FLATPAK_ID").is_ok_and(|id| id.starts_with("dev.zed.Zed"))
+ && args.zed.is_none()
{
- if args.zed.is_none() {
- args.zed = Some("/app/libexec/zed-editor".into());
- unsafe {
- env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed")
- };
- }
+ args.zed = Some("/app/libexec/zed-editor".into());
+ unsafe { env::set_var("ZED_UPDATE_EXPLANATION", "Please use flatpak to update zed") };
}
args
}
@@ -647,15 +708,15 @@ mod windows {
Storage::FileSystem::{
CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
},
- System::Threading::CreateMutexW,
+ System::Threading::{CREATE_NEW_PROCESS_GROUP, CreateMutexW},
},
core::HSTRING,
};
use crate::{Detect, InstalledApp};
- use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
+ use std::{io, os::windows::process::CommandExt};
fn check_single_instance() -> bool {
let mutex = unsafe {
@@ -694,6 +755,7 @@ mod windows {
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
if check_single_instance() {
std::process::Command::new(self.0.clone())
+ .creation_flags(CREATE_NEW_PROCESS_GROUP.0)
.arg(ipc_url)
.spawn()?;
} else {
@@ -929,7 +991,7 @@ mod mac_os {
fn path(&self) -> PathBuf {
match self {
- Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed").clone(),
+ Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
Bundle::LocalPath { executable, .. } => executable.clone(),
}
}
@@ -44,6 +44,7 @@ rpc = { workspace = true, features = ["gpui"] }
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
+serde_urlencoded.workspace = true
settings.workspace = true
sha2.workspace = true
smol.workspace = true
@@ -74,7 +75,7 @@ util = { workspace = true, features = ["test-support"] }
windows.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
-cocoa.workspace = true
+objc2-foundation.workspace = true
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
tokio-native-tls = "0.3"
@@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use std::{
any::TypeId,
convert::TryFrom,
@@ -66,6 +66,8 @@ pub static IMPERSONATE_LOGIN: LazyLock<Option<String>> = LazyLock::new(|| {
.and_then(|s| if s.is_empty() { None } else { Some(s) })
});
+pub static USE_WEB_LOGIN: LazyLock<bool> = LazyLock::new(|| std::env::var("ZED_WEB_LOGIN").is_ok());
+
pub static ADMIN_API_TOKEN: LazyLock<Option<String>> = LazyLock::new(|| {
std::env::var("ZED_ADMIN_API_TOKEN")
.ok()
@@ -76,7 +78,7 @@ pub static ZED_APP_PATH: LazyLock<Option<PathBuf>> =
LazyLock::new(|| std::env::var("ZED_APP_PATH").ok().map(PathBuf::from));
pub static ZED_ALWAYS_ACTIVE: LazyLock<bool> =
- LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty()));
+ LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").is_ok_and(|e| !e.is_empty()));
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(30);
@@ -94,7 +96,8 @@ actions!(
]
);
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
pub struct ClientSettingsContent {
server_url: Option<String>,
}
@@ -105,8 +108,6 @@ pub struct ClientSettings {
}
impl Settings for ClientSettings {
- const KEY: Option<&'static str> = None;
-
type FileContent = ClientSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -120,7 +121,8 @@ impl Settings for ClientSettings {
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
-#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
pub struct ProxySettingsContent {
proxy: Option<String>,
}
@@ -131,8 +133,6 @@ pub struct ProxySettings {
}
impl Settings for ProxySettings {
- const KEY: Option<&'static str> = None;
-
type FileContent = ProxySettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -162,7 +162,7 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
let client = client.clone();
move |_: &SignIn, cx| {
if let Some(client) = client.upgrade() {
- cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, &cx).await)
+ cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, cx).await)
.detach_and_log_err(cx);
}
}
@@ -173,7 +173,7 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
move |_: &SignOut, cx| {
if let Some(client) = client.upgrade() {
cx.spawn(async move |cx| {
- client.sign_out(&cx).await;
+ client.sign_out(cx).await;
})
.detach();
}
@@ -181,11 +181,11 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
});
cx.on_action({
- let client = client.clone();
+ let client = client;
move |_: &Reconnect, cx| {
if let Some(client) = client.upgrade() {
cx.spawn(async move |cx| {
- client.reconnect(&cx);
+ client.reconnect(cx);
})
.detach();
}
@@ -285,6 +285,7 @@ pub enum Status {
},
ConnectionLost,
Reauthenticating,
+ Reauthenticated,
Reconnecting,
ReconnectionError {
next_reconnection: Instant,
@@ -296,6 +297,21 @@ impl Status {
matches!(self, Self::Connected { .. })
}
+ pub fn was_connected(&self) -> bool {
+ matches!(
+ self,
+ Self::ConnectionLost
+ | Self::Reauthenticating
+ | Self::Reauthenticated
+ | Self::Reconnecting
+ )
+ }
+
+ /// Returns whether the client is currently connected or was connected at some point.
+ pub fn is_or_was_connected(&self) -> bool {
+ self.is_connected() || self.was_connected()
+ }
+
pub fn is_signing_in(&self) -> bool {
matches!(
self,
@@ -509,7 +525,8 @@ pub struct TelemetrySettings {
}
/// Control what info is collected by Zed.
-#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "telemetry")]
pub struct TelemetrySettingsContent {
/// Send debug info like crash reports.
///
@@ -522,8 +539,6 @@ pub struct TelemetrySettingsContent {
}
impl settings::Settings for TelemetrySettings {
- const KEY: Option<&'static str> = Some("telemetry");
-
type FileContent = TelemetrySettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -673,11 +688,11 @@ impl Client {
#[cfg(any(test, feature = "test-support"))]
let mut rng = StdRng::seed_from_u64(0);
#[cfg(not(any(test, feature = "test-support")))]
- let mut rng = StdRng::from_entropy();
+ let mut rng = StdRng::from_os_rng();
let mut delay = INITIAL_RECONNECTION_DELAY;
loop {
- match client.connect(true, &cx).await {
+ match client.connect(true, cx).await {
ConnectionResult::Timeout => {
log::error!("client connect attempt timed out")
}
@@ -701,10 +716,11 @@ impl Client {
Status::ReconnectionError {
next_reconnection: Instant::now() + delay,
},
- &cx,
+ cx,
+ );
+ let jitter = Duration::from_millis(
+ rng.random_range(0..delay.as_millis() as u64),
);
- let jitter =
- Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64));
cx.background_executor().timer(delay + jitter).await;
delay = cmp::min(delay * 2, MAX_RECONNECTION_DELAY);
} else {
@@ -791,7 +807,7 @@ impl Client {
Arc::new(move |subscriber, envelope, client, cx| {
let subscriber = subscriber.downcast::<E>().unwrap();
let envelope = envelope.into_any().downcast::<TypedEnvelope<M>>().unwrap();
- handler(subscriber, *envelope, client.clone(), cx).boxed_local()
+ handler(subscriber, *envelope, client, cx).boxed_local()
}),
);
if prev_handler.is_some() {
@@ -855,31 +871,34 @@ impl Client {
try_provider: bool,
cx: &AsyncApp,
) -> Result<Credentials> {
- if self.status().borrow().is_signed_out() {
+ let is_reauthenticating = if self.status().borrow().is_signed_out() {
self.set_status(Status::Authenticating, cx);
+ false
} else {
self.set_status(Status::Reauthenticating, cx);
- }
+ true
+ };
let mut credentials = None;
let old_credentials = self.state.read().credentials.clone();
- if let Some(old_credentials) = old_credentials {
- if self.validate_credentials(&old_credentials, cx).await? {
- credentials = Some(old_credentials);
- }
+ if let Some(old_credentials) = old_credentials
+ && self.validate_credentials(&old_credentials, cx).await?
+ {
+ credentials = Some(old_credentials);
}
- if credentials.is_none() && try_provider {
- if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await {
- if self.validate_credentials(&stored_credentials, cx).await? {
- credentials = Some(stored_credentials);
- } else {
- self.credentials_provider
- .delete_credentials(cx)
- .await
- .log_err();
- }
+ if credentials.is_none()
+ && try_provider
+ && let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await
+ {
+ if self.validate_credentials(&stored_credentials, cx).await? {
+ credentials = Some(stored_credentials);
+ } else {
+ self.credentials_provider
+ .delete_credentials(cx)
+ .await
+ .log_err();
}
}
@@ -916,7 +935,14 @@ impl Client {
self.cloud_client
.set_credentials(credentials.user_id as u32, credentials.access_token.clone());
self.state.write().credentials = Some(credentials.clone());
- self.set_status(Status::Authenticated, cx);
+ self.set_status(
+ if is_reauthenticating {
+ Status::Reauthenticated
+ } else {
+ Status::Authenticated
+ },
+ cx,
+ );
Ok(credentials)
}
@@ -973,6 +999,11 @@ impl Client {
try_provider: bool,
cx: &AsyncApp,
) -> Result<()> {
+ // Don't try to sign in again if we're already connected to Collab, as it will temporarily disconnect us.
+ if self.status().borrow().is_connected() {
+ return Ok(());
+ }
+
let (is_staff_tx, is_staff_rx) = oneshot::channel::<bool>();
let mut is_staff_tx = Some(is_staff_tx);
cx.update(|cx| {
@@ -1023,11 +1054,12 @@ impl Client {
Status::SignedOut | Status::Authenticated => true,
Status::ConnectionError
| Status::ConnectionLost
- | Status::Authenticating { .. }
+ | Status::Authenticating
| Status::AuthenticationError
- | Status::Reauthenticating { .. }
+ | Status::Reauthenticating
+ | Status::Reauthenticated
| Status::ReconnectionError { .. } => false,
- Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => {
+ Status::Connected { .. } | Status::Connecting | Status::Reconnecting => {
return ConnectionResult::Result(Ok(()));
}
Status::UpgradeRequired => {
@@ -1151,7 +1183,7 @@ impl Client {
let this = self.clone();
async move |cx| {
while let Some(message) = incoming.next().await {
- this.handle_message(message, &cx);
+ this.handle_message(message, cx);
// Don't starve the main thread when receiving lots of messages at once.
smol::future::yield_now().await;
}
@@ -1169,12 +1201,12 @@ impl Client {
peer_id,
})
{
- this.set_status(Status::SignedOut, &cx);
+ this.set_status(Status::SignedOut, cx);
}
}
Err(err) => {
log::error!("connection error: {:?}", err);
- this.set_status(Status::ConnectionLost, &cx);
+ this.set_status(Status::ConnectionLost, cx);
}
}
})
@@ -1284,19 +1316,21 @@ impl Client {
"http" => Http,
_ => Err(anyhow!("invalid rpc url: {}", rpc_url))?,
};
- let rpc_host = rpc_url
- .host_str()
- .zip(rpc_url.port_or_known_default())
- .context("missing host in rpc url")?;
-
- let stream = {
- let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
- let _guard = handle.enter();
- match proxy {
- Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?,
- None => Box::new(TcpStream::connect(rpc_host).await?),
+
+ let stream = gpui_tokio::Tokio::spawn_result(cx, {
+ let rpc_url = rpc_url.clone();
+ async move {
+ let rpc_host = rpc_url
+ .host_str()
+ .zip(rpc_url.port_or_known_default())
+ .context("missing host in rpc url")?;
+ Ok(match proxy {
+ Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?,
+ None => Box::new(TcpStream::connect(rpc_host).await?),
+ })
}
- };
+ })?
+ .await?;
log::info!("connected to rpc endpoint {}", rpc_url);
@@ -1384,11 +1418,13 @@ impl Client {
if let Some((login, token)) =
IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref())
{
- eprintln!("authenticate as admin {login}, {token}");
+ if !*USE_WEB_LOGIN {
+ eprintln!("authenticate as admin {login}, {token}");
- return this
- .authenticate_as_admin(http, login.clone(), token.clone())
- .await;
+ return this
+ .authenticate_as_admin(http, login.clone(), token.clone())
+ .await;
+ }
}
// Start an HTTP server to receive the redirect from Zed's sign-in page.
@@ -1410,6 +1446,12 @@ impl Client {
open_url_tx.send(url).log_err();
+ #[derive(Deserialize)]
+ struct CallbackParams {
+ pub user_id: String,
+ pub access_token: String,
+ }
+
// Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
// access token from the query params.
//
@@ -1420,17 +1462,13 @@ impl Client {
for _ in 0..100 {
if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
let path = req.url();
- let mut user_id = None;
- let mut access_token = None;
let url = Url::parse(&format!("http://example.com{}", path))
.context("failed to parse login notification url")?;
- for (key, value) in url.query_pairs() {
- if key == "access_token" {
- access_token = Some(value.to_string());
- } else if key == "user_id" {
- user_id = Some(value.to_string());
- }
- }
+ let callback_params: CallbackParams =
+ serde_urlencoded::from_str(url.query().unwrap_or_default())
+ .context(
+ "failed to parse sign-in callback query parameters",
+ )?;
let post_auth_url =
http.build_url("/native_app_signin_succeeded");
@@ -1445,8 +1483,8 @@ impl Client {
)
.context("failed to respond to login http request")?;
return Ok((
- user_id.context("missing user_id parameter")?,
- access_token.context("missing access_token parameter")?,
+ callback_params.user_id,
+ callback_params.access_token,
));
}
}
@@ -1656,21 +1694,10 @@ impl Client {
);
cx.spawn(async move |_| match future.await {
Ok(()) => {
- log::debug!(
- "rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
- client_id,
- original_sender_id,
- type_name
- );
+ log::debug!("rpc message handled. client_id:{client_id}, sender_id:{original_sender_id:?}, type:{type_name}");
}
Err(error) => {
- log::error!(
- "error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
- client_id,
- original_sender_id,
- type_name,
- error
- );
+ log::error!("error handling message. client_id:{client_id}, sender_id:{original_sender_id:?}, type:{type_name}, error:{error:#}");
}
})
.detach();
@@ -1894,10 +1921,7 @@ mod tests {
assert!(matches!(status.next().await, Some(Status::Connecting)));
executor.advance_clock(CONNECTION_TIMEOUT);
- assert!(matches!(
- status.next().await,
- Some(Status::ConnectionError { .. })
- ));
+ assert!(matches!(status.next().await, Some(Status::ConnectionError)));
auth_and_connect.await.into_response().unwrap_err();
// Allow the connection to be established.
@@ -1921,10 +1945,7 @@ mod tests {
})
});
executor.advance_clock(2 * INITIAL_RECONNECTION_DELAY);
- assert!(matches!(
- status.next().await,
- Some(Status::Reconnecting { .. })
- ));
+ assert!(matches!(status.next().await, Some(Status::Reconnecting)));
executor.advance_clock(CONNECTION_TIMEOUT);
assert!(matches!(
@@ -2040,10 +2061,7 @@ mod tests {
assert_eq!(*auth_count.lock(), 1);
assert_eq!(*dropped_auth_count.lock(), 0);
- let _authenticate = cx.spawn({
- let client = client.clone();
- |cx| async move { client.connect(false, &cx).await }
- });
+ let _authenticate = cx.spawn(|cx| async move { client.connect(false, &cx).await });
executor.run_until_parked();
assert_eq!(*auth_count.lock(), 2);
assert_eq!(*dropped_auth_count.lock(), 1);
@@ -2065,8 +2083,8 @@ mod tests {
let (done_tx1, done_rx1) = smol::channel::unbounded();
let (done_tx2, done_rx2) = smol::channel::unbounded();
AnyProtoClient::from(client.clone()).add_entity_message_handler(
- move |entity: Entity<TestEntity>, _: TypedEnvelope<proto::JoinProject>, mut cx| {
- match entity.read_with(&mut cx, |entity, _| entity.id).unwrap() {
+ move |entity: Entity<TestEntity>, _: TypedEnvelope<proto::JoinProject>, cx| {
+ match entity.read_with(&cx, |entity, _| entity.id).unwrap() {
1 => done_tx1.try_send(()).unwrap(),
2 => done_tx2.try_send(()).unwrap(),
_ => unreachable!(),
@@ -2090,17 +2108,17 @@ mod tests {
let _subscription1 = client
.subscribe_to_entity(1)
.unwrap()
- .set_entity(&entity1, &mut cx.to_async());
+ .set_entity(&entity1, &cx.to_async());
let _subscription2 = client
.subscribe_to_entity(2)
.unwrap()
- .set_entity(&entity2, &mut cx.to_async());
+ .set_entity(&entity2, &cx.to_async());
// Ensure dropping a subscription for the same entity type still allows receiving of
// messages for other entity IDs of the same type.
let subscription3 = client
.subscribe_to_entity(3)
.unwrap()
- .set_entity(&entity3, &mut cx.to_async());
+ .set_entity(&entity3, &cx.to_async());
drop(subscription3);
server.send(proto::JoinProject {
@@ -76,7 +76,7 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
pub static MINIDUMP_ENDPOINT: LazyLock<Option<String>> = LazyLock::new(|| {
option_env!("ZED_MINIDUMP_ENDPOINT")
- .map(|s| s.to_owned())
+ .map(str::to_string)
.or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok())
});
@@ -84,6 +84,10 @@ static DOTNET_PROJECT_FILES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap()
});
+#[cfg(target_os = "macos")]
+static MACOS_VERSION_REGEX: LazyLock<Regex> =
+ LazyLock::new(|| Regex::new(r"(\s*\(Build [^)]*[0-9]\))").unwrap());
+
pub fn os_name() -> String {
#[cfg(target_os = "macos")]
{
@@ -108,19 +112,16 @@ pub fn os_name() -> String {
pub fn os_version() -> String {
#[cfg(target_os = "macos")]
{
- use cocoa::base::nil;
- use cocoa::foundation::NSProcessInfo;
-
- unsafe {
- let process_info = cocoa::foundation::NSProcessInfo::processInfo(nil);
- let version = process_info.operatingSystemVersion();
- gpui::SemanticVersion::new(
- version.majorVersion as usize,
- version.minorVersion as usize,
- version.patchVersion as usize,
- )
+ use objc2_foundation::NSProcessInfo;
+ let process_info = NSProcessInfo::processInfo();
+ let version_nsstring = unsafe { process_info.operatingSystemVersionString() };
+ // "Version 15.6.1 (Build 24G90)" -> "15.6.1 (Build 24G90)"
+ let version_string = version_nsstring.to_string().replace("Version ", "");
+ // "15.6.1 (Build 24G90)" -> "15.6.1"
+ // "26.0.0 (Build 25A5349a)" -> unchanged (Beta or Rapid Security Response; ends with letter)
+ MACOS_VERSION_REGEX
+ .replace_all(&version_string, "")
.to_string()
- }
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
@@ -739,7 +740,7 @@ mod tests {
);
// Third scan of worktree does not double report, as we already reported
- test_project_discovery_helper(telemetry.clone(), vec!["package.json"], None, worktree_id);
+ test_project_discovery_helper(telemetry, vec!["package.json"], None, worktree_id);
}
#[gpui::test]
@@ -751,7 +752,7 @@ mod tests {
let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
test_project_discovery_helper(
- telemetry.clone(),
+ telemetry,
vec!["package.json", "pnpm-lock.yaml"],
Some(vec!["node", "pnpm"]),
1,
@@ -767,7 +768,7 @@ mod tests {
let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
test_project_discovery_helper(
- telemetry.clone(),
+ telemetry,
vec!["package.json", "yarn.lock"],
Some(vec!["node", "yarn"]),
1,
@@ -786,7 +787,7 @@ mod tests {
// project type for the same worktree multiple times
test_project_discovery_helper(
- telemetry.clone().clone(),
+ telemetry.clone(),
vec!["global.json"],
Some(vec!["dotnet"]),
1,
@@ -1,16 +1,12 @@
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
use anyhow::{Context as _, Result, anyhow};
-use chrono::Duration;
use cloud_api_client::{AuthenticatedUser, GetAuthenticatedUserResponse, PlanInfo};
-use cloud_llm_client::{CurrentUsage, Plan, UsageData, UsageLimit};
+use cloud_llm_client::{CurrentUsage, PlanV1, UsageData, UsageLimit};
use futures::{StreamExt, stream::BoxStream};
use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext};
use http_client::{AsyncBody, Method, Request, http};
use parking_lot::Mutex;
-use rpc::{
- ConnectionId, Peer, Receipt, TypedEnvelope,
- proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
-};
+use rpc::{ConnectionId, Peer, Receipt, TypedEnvelope, proto};
use std::sync::Arc;
pub struct FakeServer {
@@ -187,50 +183,27 @@ impl FakeServer {
pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
self.executor.start_waiting();
- loop {
- let message = self
- .state
- .lock()
- .incoming
- .as_mut()
- .expect("not connected")
- .next()
- .await
- .context("other half hung up")?;
- self.executor.finish_waiting();
- let type_name = message.payload_type_name();
- let message = message.into_any();
-
- if message.is::<TypedEnvelope<M>>() {
- return Ok(*message.downcast().unwrap());
- }
-
- let accepted_tos_at = chrono::Utc::now()
- .checked_sub_signed(Duration::hours(5))
- .expect("failed to build accepted_tos_at")
- .timestamp() as u64;
-
- if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
- self.respond(
- message
- .downcast::<TypedEnvelope<GetPrivateUserInfo>>()
- .unwrap()
- .receipt(),
- GetPrivateUserInfoResponse {
- metrics_id: "the-metrics-id".into(),
- staff: false,
- flags: Default::default(),
- accepted_tos_at: Some(accepted_tos_at),
- },
- );
- continue;
- }
+ let message = self
+ .state
+ .lock()
+ .incoming
+ .as_mut()
+ .expect("not connected")
+ .next()
+ .await
+ .context("other half hung up")?;
+ self.executor.finish_waiting();
+ let type_name = message.payload_type_name();
+ let message = message.into_any();
- panic!(
- "fake server received unexpected message type: {:?}",
- type_name
- );
+ if message.is::<TypedEnvelope<M>>() {
+ return Ok(*message.downcast().unwrap());
}
+
+ panic!(
+ "fake server received unexpected message type: {:?}",
+ type_name
+ );
}
pub fn respond<T: proto::RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) {
@@ -296,7 +269,8 @@ pub fn make_get_authenticated_user_response(
},
feature_flags: vec![],
plan: PlanInfo {
- plan: Plan::ZedPro,
+ plan: PlanV1::ZedPro,
+ plan_v2: None,
subscription_period: None,
usage: CurrentUsage {
model_requests: UsageData {
@@ -1,11 +1,12 @@
use super::{Client, Status, TypedEnvelope, proto};
-use anyhow::{Context as _, Result, anyhow};
+use anyhow::{Context as _, Result};
use chrono::{DateTime, Utc};
use cloud_api_client::websocket_protocol::MessageToClient;
use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo};
use cloud_llm_client::{
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
- MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
+ MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, Plan,
+ UsageLimit,
};
use collections::{HashMap, HashSet, hash_map::Entry};
use derive_more::Deref;
@@ -41,16 +42,11 @@ impl std::fmt::Display for ChannelId {
pub struct ProjectId(pub u64);
impl ProjectId {
- pub fn to_proto(&self) -> u64 {
+ pub fn to_proto(self) -> u64 {
self.0
}
}
-#[derive(
- Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
-)]
-pub struct DevServerProjectId(pub u64);
-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParticipantIndex(pub u32);
@@ -116,7 +112,6 @@ pub struct UserStore {
edit_prediction_usage: Option<EditPredictionUsage>,
plan_info: Option<PlanInfo>,
current_user: watch::Receiver<Option<Arc<User>>>,
- accepted_tos_at: Option<Option<cloud_api_client::Timestamp>>,
contacts: Vec<Arc<Contact>>,
incoming_contact_requests: Vec<Arc<User>>,
outgoing_contact_requests: Vec<Arc<User>>,
@@ -177,7 +172,6 @@ impl UserStore {
let (mut current_user_tx, current_user_rx) = watch::channel();
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
let rpc_subscriptions = vec![
- client.add_message_handler(cx.weak_entity(), Self::handle_update_plan),
client.add_message_handler(cx.weak_entity(), Self::handle_update_contacts),
client.add_message_handler(cx.weak_entity(), Self::handle_update_invite_info),
client.add_message_handler(cx.weak_entity(), Self::handle_show_contacts),
@@ -195,7 +189,6 @@ impl UserStore {
plan_info: None,
model_request_usage: None,
edit_prediction_usage: None,
- accepted_tos_at: None,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
participant_indices: Default::default(),
@@ -224,7 +217,9 @@ impl UserStore {
return Ok(());
};
match status {
- Status::Authenticated | Status::Connected { .. } => {
+ Status::Authenticated
+ | Status::Reauthenticated
+ | Status::Connected { .. } => {
if let Some(user_id) = client.user_id() {
let response = client
.cloud_client()
@@ -272,7 +267,6 @@ impl UserStore {
Status::SignedOut => {
current_user_tx.send(None).await.ok();
this.update(cx, |this, cx| {
- this.accepted_tos_at = None;
cx.emit(Event::PrivateUserInfoUpdated);
cx.notify();
this.clear_contacts()
@@ -333,9 +327,9 @@ impl UserStore {
async fn handle_update_contacts(
this: Entity<Self>,
message: TypedEnvelope<proto::UpdateContacts>,
- mut cx: AsyncApp,
+ cx: AsyncApp,
) -> Result<()> {
- this.read_with(&mut cx, |this, _| {
+ this.read_with(&cx, |this, _| {
this.update_contacts_tx
.unbounded_send(UpdateContacts::Update(message.payload))
.unwrap();
@@ -343,26 +337,6 @@ impl UserStore {
Ok(())
}
- async fn handle_update_plan(
- this: Entity<Self>,
- _message: TypedEnvelope<proto::UpdateUserPlan>,
- mut cx: AsyncApp,
- ) -> Result<()> {
- let client = this
- .read_with(&cx, |this, _| this.client.upgrade())?
- .context("client was dropped")?;
-
- let response = client
- .cloud_client()
- .get_authenticated_user()
- .await
- .context("failed to fetch authenticated user")?;
-
- this.update(&mut cx, |this, cx| {
- this.update_authenticated_user(response, cx);
- })
- }
-
fn update_contacts(&mut self, message: UpdateContacts, cx: &Context<Self>) -> Task<Result<()>> {
match message {
UpdateContacts::Wait(barrier) => {
@@ -719,20 +693,22 @@ impl UserStore {
self.current_user.borrow().clone()
}
- pub fn plan(&self) -> Option<cloud_llm_client::Plan> {
+ pub fn plan(&self) -> Option<Plan> {
#[cfg(debug_assertions)]
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
+ use cloud_llm_client::PlanV1;
+
return match plan.as_str() {
- "free" => Some(cloud_llm_client::Plan::ZedFree),
- "trial" => Some(cloud_llm_client::Plan::ZedProTrial),
- "pro" => Some(cloud_llm_client::Plan::ZedPro),
+ "free" => Some(Plan::V1(PlanV1::ZedFree)),
+ "trial" => Some(Plan::V1(PlanV1::ZedProTrial)),
+ "pro" => Some(Plan::V1(PlanV1::ZedPro)),
_ => {
panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'");
}
};
}
- self.plan_info.as_ref().map(|info| info.plan)
+ self.plan_info.as_ref().map(|info| info.plan())
}
pub fn subscription_period(&self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
@@ -812,19 +788,6 @@ impl UserStore {
.set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff);
}
- let accepted_tos_at = {
- #[cfg(debug_assertions)]
- if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() {
- None
- } else {
- response.user.accepted_tos_at
- }
-
- #[cfg(not(debug_assertions))]
- response.user.accepted_tos_at
- };
-
- self.accepted_tos_at = Some(accepted_tos_at);
self.model_request_usage = Some(ModelRequestUsage(RequestUsage {
limit: response.plan.usage.model_requests.limit,
amount: response.plan.usage.model_requests.used as i32,
@@ -867,32 +830,6 @@ impl UserStore {
self.current_user.clone()
}
- pub fn has_accepted_terms_of_service(&self) -> bool {
- self.accepted_tos_at
- .map_or(false, |accepted_tos_at| accepted_tos_at.is_some())
- }
-
- pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> {
- if self.current_user().is_none() {
- return Task::ready(Err(anyhow!("no current user")));
- };
-
- let client = self.client.clone();
- cx.spawn(async move |this, cx| -> anyhow::Result<()> {
- let client = client.upgrade().context("client not found")?;
- let response = client
- .cloud_client()
- .accept_terms_of_service()
- .await
- .context("error accepting tos")?;
- this.update(cx, |this, cx| {
- this.accepted_tos_at = Some(response.user.accepted_tos_at);
- cx.emit(Event::PrivateUserInfoUpdated);
- })?;
- Ok(())
- })
- }
-
fn load_users(
&self,
request: impl RequestMessage<Response = UsersResponse>,
@@ -915,10 +852,10 @@ impl UserStore {
let mut ret = Vec::with_capacity(users.len());
for user in users {
let user = User::new(user);
- if let Some(old) = self.users.insert(user.id, user.clone()) {
- if old.github_login != user.github_login {
- self.by_github_login.remove(&old.github_login);
- }
+ if let Some(old) = self.users.insert(user.id, user.clone())
+ && old.github_login != user.github_login
+ {
+ self.by_github_login.remove(&old.github_login);
}
self.by_github_login
.insert(user.github_login.clone(), user.id);
@@ -1019,19 +956,6 @@ impl RequestUsage {
}
}
- pub fn from_proto(amount: u32, limit: proto::UsageLimit) -> Option<Self> {
- let limit = match limit.variant? {
- proto::usage_limit::Variant::Limited(limited) => {
- UsageLimit::Limited(limited.limit as i32)
- }
- proto::usage_limit::Variant::Unlimited(_) => UsageLimit::Unlimited,
- };
- Some(RequestUsage {
- limit,
- amount: amount as i32,
- })
- }
-
fn from_headers(
limit_name: &str,
amount_name: &str,
@@ -43,3 +43,11 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
server_url = server_url(cx)
)
}
+
+/// Returns the URL to Zed AI's external agents documentation.
+pub fn external_agents_docs(cx: &App) -> String {
+ format!(
+ "{server_url}/docs/ai/external-agents",
+ server_url = server_url(cx)
+ )
+}
@@ -102,13 +102,7 @@ impl CloudApiClient {
let credentials = credentials.as_ref().context("no credentials provided")?;
let authorization_header = format!("{} {}", credentials.user_id, credentials.access_token);
- Ok(cx.spawn(async move |cx| {
- let handle = cx
- .update(|cx| Tokio::handle(cx))
- .ok()
- .context("failed to get Tokio handle")?;
- let _guard = handle.enter();
-
+ Ok(Tokio::spawn_result(cx, async move {
let ws = WebSocket::connect(connect_url)
.with_request(
request::Builder::new()
@@ -121,34 +115,6 @@ impl CloudApiClient {
}))
}
- pub async fn accept_terms_of_service(&self) -> Result<AcceptTermsOfServiceResponse> {
- let request = self.build_request(
- Request::builder().method(Method::POST).uri(
- self.http_client
- .build_zed_cloud_url("/client/terms_of_service/accept", &[])?
- .as_ref(),
- ),
- AsyncBody::default(),
- )?;
-
- let mut response = self.http_client.send(request).await?;
-
- if !response.status().is_success() {
- let mut body = String::new();
- response.body_mut().read_to_string(&mut body).await?;
-
- anyhow::bail!(
- "Failed to accept terms of service.\nStatus: {:?}\nBody: {body}",
- response.status()
- )
- }
-
- let mut body = String::new();
- response.body_mut().read_to_string(&mut body).await?;
-
- Ok(serde_json::from_str(&body)?)
- }
-
pub async fn create_llm_token(
&self,
system_id: Option<String>,
@@ -205,12 +171,12 @@ impl CloudApiClient {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
if response.status() == StatusCode::UNAUTHORIZED {
- return Ok(false);
+ Ok(false)
} else {
- return Err(anyhow!(
+ Err(anyhow!(
"Failed to get authenticated user.\nStatus: {:?}\nBody: {body}",
response.status()
- ));
+ ))
}
}
}
@@ -1,6 +1,7 @@
mod timestamp;
pub mod websocket_protocol;
+use cloud_llm_client::Plan;
use serde::{Deserialize, Serialize};
pub use crate::timestamp::Timestamp;
@@ -27,7 +28,9 @@ pub struct AuthenticatedUser {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct PlanInfo {
- pub plan: cloud_llm_client::Plan,
+ pub plan: cloud_llm_client::PlanV1,
+ #[serde(default)]
+ pub plan_v2: Option<cloud_llm_client::PlanV2>,
pub subscription_period: Option<SubscriptionPeriod>,
pub usage: cloud_llm_client::CurrentUsage,
pub trial_started_at: Option<Timestamp>,
@@ -36,6 +39,12 @@ pub struct PlanInfo {
pub has_overdue_invoices: bool,
}
+impl PlanInfo {
+ pub fn plan(&self) -> Plan {
+ self.plan_v2.map(Plan::V2).unwrap_or(Plan::V1(self.plan))
+ }
+}
+
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub struct SubscriptionPeriod {
pub started_at: Timestamp,
@@ -74,9 +74,21 @@ impl FromStr for UsageLimit {
}
}
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum Plan {
+ V1(PlanV1),
+ V2(PlanV2),
+}
+
+impl Plan {
+ pub fn is_v2(&self) -> bool {
+ matches!(self, Self::V2(_))
+ }
+}
+
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
-pub enum Plan {
+pub enum PlanV1 {
#[default]
#[serde(alias = "Free")]
ZedFree,
@@ -86,40 +98,36 @@ pub enum Plan {
ZedProTrial,
}
-impl Plan {
- pub fn as_str(&self) -> &'static str {
- match self {
- Plan::ZedFree => "zed_free",
- Plan::ZedPro => "zed_pro",
- Plan::ZedProTrial => "zed_pro_trial",
- }
- }
+impl FromStr for PlanV1 {
+ type Err = anyhow::Error;
- pub fn model_requests_limit(&self) -> UsageLimit {
- match self {
- Plan::ZedPro => UsageLimit::Limited(500),
- Plan::ZedProTrial => UsageLimit::Limited(150),
- Plan::ZedFree => UsageLimit::Limited(50),
+ fn from_str(value: &str) -> Result<Self, Self::Err> {
+ match value {
+ "zed_free" => Ok(Self::ZedFree),
+ "zed_pro" => Ok(Self::ZedPro),
+ "zed_pro_trial" => Ok(Self::ZedProTrial),
+ plan => Err(anyhow::anyhow!("invalid plan: {plan:?}")),
}
}
+}
- pub fn edit_predictions_limit(&self) -> UsageLimit {
- match self {
- Plan::ZedPro => UsageLimit::Unlimited,
- Plan::ZedProTrial => UsageLimit::Unlimited,
- Plan::ZedFree => UsageLimit::Limited(2_000),
- }
- }
+#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum PlanV2 {
+ #[default]
+ ZedFree,
+ ZedPro,
+ ZedProTrial,
}
-impl FromStr for Plan {
+impl FromStr for PlanV2 {
type Err = anyhow::Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
- "zed_free" => Ok(Plan::ZedFree),
- "zed_pro" => Ok(Plan::ZedPro),
- "zed_pro_trial" => Ok(Plan::ZedProTrial),
+ "zed_free" => Ok(Self::ZedFree),
+ "zed_pro" => Ok(Self::ZedPro),
+ "zed_pro_trial" => Ok(Self::ZedProTrial),
plan => Err(anyhow::anyhow!("invalid plan: {plan:?}")),
}
}
@@ -320,7 +328,7 @@ pub struct ListModelsResponse {
#[derive(Debug, Serialize, Deserialize)]
pub struct GetSubscriptionResponse {
- pub plan: Plan,
+ pub plan: PlanV1,
pub usage: Option<CurrentUsage>,
}
@@ -344,27 +352,39 @@ mod tests {
use super::*;
#[test]
- fn test_plan_deserialize_snake_case() {
- let plan = serde_json::from_value::<Plan>(json!("zed_free")).unwrap();
- assert_eq!(plan, Plan::ZedFree);
+ fn test_plan_v1_deserialize_snake_case() {
+ let plan = serde_json::from_value::<PlanV1>(json!("zed_free")).unwrap();
+ assert_eq!(plan, PlanV1::ZedFree);
+
+ let plan = serde_json::from_value::<PlanV1>(json!("zed_pro")).unwrap();
+ assert_eq!(plan, PlanV1::ZedPro);
+
+ let plan = serde_json::from_value::<PlanV1>(json!("zed_pro_trial")).unwrap();
+ assert_eq!(plan, PlanV1::ZedProTrial);
+ }
+
+ #[test]
+ fn test_plan_v1_deserialize_aliases() {
+ let plan = serde_json::from_value::<PlanV1>(json!("Free")).unwrap();
+ assert_eq!(plan, PlanV1::ZedFree);
- let plan = serde_json::from_value::<Plan>(json!("zed_pro")).unwrap();
- assert_eq!(plan, Plan::ZedPro);
+ let plan = serde_json::from_value::<PlanV1>(json!("ZedPro")).unwrap();
+ assert_eq!(plan, PlanV1::ZedPro);
- let plan = serde_json::from_value::<Plan>(json!("zed_pro_trial")).unwrap();
- assert_eq!(plan, Plan::ZedProTrial);
+ let plan = serde_json::from_value::<PlanV1>(json!("ZedProTrial")).unwrap();
+ assert_eq!(plan, PlanV1::ZedProTrial);
}
#[test]
- fn test_plan_deserialize_aliases() {
- let plan = serde_json::from_value::<Plan>(json!("Free")).unwrap();
- assert_eq!(plan, Plan::ZedFree);
+ fn test_plan_v2_deserialize_snake_case() {
+ let plan = serde_json::from_value::<PlanV2>(json!("zed_free")).unwrap();
+ assert_eq!(plan, PlanV2::ZedFree);
- let plan = serde_json::from_value::<Plan>(json!("ZedPro")).unwrap();
- assert_eq!(plan, Plan::ZedPro);
+ let plan = serde_json::from_value::<PlanV2>(json!("zed_pro")).unwrap();
+ assert_eq!(plan, PlanV2::ZedPro);
- let plan = serde_json::from_value::<Plan>(json!("ZedProTrial")).unwrap();
- assert_eq!(plan, Plan::ZedProTrial);
+ let plan = serde_json::from_value::<PlanV2>(json!("zed_pro_trial")).unwrap();
+ assert_eq!(plan, PlanV2::ZedProTrial);
}
#[test]
@@ -19,7 +19,6 @@ test-support = ["sqlite"]
[dependencies]
anyhow.workspace = true
-async-stripe.workspace = true
async-trait.workspace = true
async-tungstenite.workspace = true
aws-config = { version = "1.1.5" }
@@ -30,16 +29,13 @@ axum-extra = { version = "0.4", features = ["erased-json"] }
base64.workspace = true
chrono.workspace = true
clock.workspace = true
-cloud_llm_client.workspace = true
collections.workspace = true
dashmap.workspace = true
-derive_more.workspace = true
envy = "0.4.2"
futures.workspace = true
gpui.workspace = true
hex.workspace = true
http_client.workspace = true
-jsonwebtoken.workspace = true
livekit_api.workspace = true
log.workspace = true
nanoid.workspace = true
@@ -65,7 +61,6 @@ subtle.workspace = true
supermaven_api.workspace = true
telemetry_events.workspace = true
text.workspace = true
-thiserror.workspace = true
time.workspace = true
tokio = { workspace = true, features = ["full"] }
toml.workspace = true
@@ -136,6 +131,3 @@ util.workspace = true
workspace = { workspace = true, features = ["test-support"] }
worktree = { workspace = true, features = ["test-support"] }
zlog.workspace = true
-
-[package.metadata.cargo-machete]
-ignored = ["async-stripe"]
@@ -219,12 +219,6 @@ spec:
secretKeyRef:
name: slack
key: panics_webhook
- - name: STRIPE_API_KEY
- valueFrom:
- secretKeyRef:
- name: stripe
- key: api_key
- optional: true
- name: COMPLETE_WITH_LANGUAGE_MODEL_RATE_LIMIT_PER_HOUR
value: "1000"
- name: SUPERMAVEN_ADMIN_API_KEY
@@ -116,6 +116,7 @@ CREATE TABLE "project_repositories" (
"scan_id" INTEGER NOT NULL,
"is_deleted" BOOL NOT NULL,
"current_merge_conflicts" VARCHAR,
+ "merge_message" VARCHAR,
"branch_summary" VARCHAR,
"head_commit_details" VARCHAR,
PRIMARY KEY (project_id, id)
@@ -174,6 +175,7 @@ CREATE TABLE "language_servers" (
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"name" VARCHAR NOT NULL,
"capabilities" TEXT NOT NULL,
+ "worktree_id" BIGINT,
PRIMARY KEY (project_id, id)
);
@@ -474,67 +476,6 @@ CREATE UNIQUE INDEX "index_extensions_external_id" ON "extensions" ("external_id
CREATE INDEX "index_extensions_total_download_count" ON "extensions" ("total_download_count");
-CREATE TABLE rate_buckets (
- user_id INT NOT NULL,
- rate_limit_name VARCHAR(255) NOT NULL,
- token_count INT NOT NULL,
- last_refill TIMESTAMP WITHOUT TIME ZONE NOT NULL,
- PRIMARY KEY (user_id, rate_limit_name),
- FOREIGN KEY (user_id) REFERENCES users (id)
-);
-
-CREATE INDEX idx_user_id_rate_limit ON rate_buckets (user_id, rate_limit_name);
-
-CREATE TABLE IF NOT EXISTS billing_preferences (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- user_id INTEGER NOT NULL REFERENCES users (id),
- max_monthly_llm_usage_spending_in_cents INTEGER NOT NULL,
- model_request_overages_enabled bool NOT NULL DEFAULT FALSE,
- model_request_overages_spend_limit_in_cents integer NOT NULL DEFAULT 0
-);
-
-CREATE UNIQUE INDEX "uix_billing_preferences_on_user_id" ON billing_preferences (user_id);
-
-CREATE TABLE IF NOT EXISTS billing_customers (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- user_id INTEGER NOT NULL REFERENCES users (id),
- has_overdue_invoices BOOLEAN NOT NULL DEFAULT FALSE,
- stripe_customer_id TEXT NOT NULL,
- trial_started_at TIMESTAMP
-);
-
-CREATE UNIQUE INDEX "uix_billing_customers_on_user_id" ON billing_customers (user_id);
-
-CREATE UNIQUE INDEX "uix_billing_customers_on_stripe_customer_id" ON billing_customers (stripe_customer_id);
-
-CREATE TABLE IF NOT EXISTS billing_subscriptions (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
- billing_customer_id INTEGER NOT NULL REFERENCES billing_customers (id),
- stripe_subscription_id TEXT NOT NULL,
- stripe_subscription_status TEXT NOT NULL,
- stripe_cancel_at TIMESTAMP,
- stripe_cancellation_reason TEXT,
- kind TEXT,
- stripe_current_period_start BIGINT,
- stripe_current_period_end BIGINT
-);
-
-CREATE INDEX "ix_billing_subscriptions_on_billing_customer_id" ON billing_subscriptions (billing_customer_id);
-
-CREATE UNIQUE INDEX "uix_billing_subscriptions_on_stripe_subscription_id" ON billing_subscriptions (stripe_subscription_id);
-
-CREATE TABLE IF NOT EXISTS processed_stripe_events (
- stripe_event_id TEXT PRIMARY KEY,
- stripe_event_type TEXT NOT NULL,
- stripe_event_created_timestamp INTEGER NOT NULL,
- processed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
-);
-
-CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp);
-
CREATE TABLE IF NOT EXISTS "breakpoints" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
@@ -0,0 +1,2 @@
+alter table users
+alter column admin set not null;
@@ -0,0 +1,2 @@
+alter table billing_customers
+ add column orb_customer_id text;
@@ -0,0 +1 @@
+drop table rate_buckets;
@@ -0,0 +1 @@
+ALTER TABLE "project_repositories" ADD COLUMN "merge_message" VARCHAR;
@@ -0,0 +1,2 @@
+alter table billing_subscriptions
+ add column orb_subscription_id text;
@@ -0,0 +1,3 @@
+alter table billing_subscriptions
+ alter column stripe_subscription_id drop not null,
+ alter column stripe_subscription_status drop not null;
@@ -0,0 +1,4 @@
+alter table billing_subscriptions
+ add column orb_subscription_status text,
+ add column orb_current_billing_period_start_date timestamp without time zone,
+ add column orb_current_billing_period_end_date timestamp without time zone;
@@ -0,0 +1,2 @@
+ALTER TABLE language_servers
+ ADD COLUMN worktree_id BIGINT;
@@ -1,19 +1,11 @@
-pub mod billing;
pub mod contributors;
pub mod events;
pub mod extensions;
pub mod ips_file;
pub mod slack;
-use crate::db::Database;
-use crate::{
- AppState, Error, Result, auth,
- db::{User, UserId},
- rpc,
-};
-use ::rpc::proto;
+use crate::{AppState, Error, Result, auth, db::UserId, rpc};
use anyhow::Context as _;
-use axum::extract;
use axum::{
Extension, Json, Router,
body::Body,
@@ -25,7 +17,6 @@ use axum::{
routing::{get, post},
};
use axum_extra::response::ErasedJson;
-use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, OnceLock};
use tower::ServiceBuilder;
@@ -100,10 +91,7 @@ impl std::fmt::Display for SystemIdHeader {
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
Router::new()
- .route("/users/look_up", get(look_up_user))
.route("/users/:id/access_tokens", post(create_access_token))
- .route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens))
- .route("/users/:id/update_plan", post(update_plan))
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
.merge(contributors::router())
.layer(
@@ -144,99 +132,6 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
Ok::<_, Error>(next.run(req).await)
}
-#[derive(Debug, Deserialize)]
-struct LookUpUserParams {
- identifier: String,
-}
-
-#[derive(Debug, Serialize)]
-struct LookUpUserResponse {
- user: Option<User>,
-}
-
-async fn look_up_user(
- Query(params): Query<LookUpUserParams>,
- Extension(app): Extension<Arc<AppState>>,
-) -> Result<Json<LookUpUserResponse>> {
- let user = resolve_identifier_to_user(&app.db, ¶ms.identifier).await?;
- let user = if let Some(user) = user {
- match user {
- UserOrId::User(user) => Some(user),
- UserOrId::Id(id) => app.db.get_user_by_id(id).await?,
- }
- } else {
- None
- };
-
- Ok(Json(LookUpUserResponse { user }))
-}
-
-enum UserOrId {
- User(User),
- Id(UserId),
-}
-
-async fn resolve_identifier_to_user(
- db: &Arc<Database>,
- identifier: &str,
-) -> Result<Option<UserOrId>> {
- if let Some(identifier) = identifier.parse::<i32>().ok() {
- let user = db.get_user_by_id(UserId(identifier)).await?;
-
- return Ok(user.map(UserOrId::User));
- }
-
- if identifier.starts_with("cus_") {
- let billing_customer = db
- .get_billing_customer_by_stripe_customer_id(&identifier)
- .await?;
-
- return Ok(billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id)));
- }
-
- if identifier.starts_with("sub_") {
- let billing_subscription = db
- .get_billing_subscription_by_stripe_subscription_id(&identifier)
- .await?;
-
- if let Some(billing_subscription) = billing_subscription {
- let billing_customer = db
- .get_billing_customer_by_id(billing_subscription.billing_customer_id)
- .await?;
-
- return Ok(
- billing_customer.map(|billing_customer| UserOrId::Id(billing_customer.user_id))
- );
- } else {
- return Ok(None);
- }
- }
-
- if identifier.contains('@') {
- let user = db.get_user_by_email(identifier).await?;
-
- return Ok(user.map(UserOrId::User));
- }
-
- if let Some(user) = db.get_user_by_github_login(identifier).await? {
- return Ok(Some(UserOrId::User(user)));
- }
-
- Ok(None)
-}
-
-#[derive(Deserialize, Debug)]
-struct CreateUserParams {
- github_user_id: i32,
- github_login: String,
- email_address: String,
- email_confirmation_code: Option<String>,
- #[serde(default)]
- admin: bool,
- #[serde(default)]
- invite_count: i32,
-}
-
async fn get_rpc_server_snapshot(
Extension(rpc_server): Extension<Arc<rpc::Server>>,
) -> Result<ErasedJson> {
@@ -295,90 +190,3 @@ async fn create_access_token(
encrypted_access_token,
}))
}
-
-#[derive(Serialize)]
-struct RefreshLlmTokensResponse {}
-
-async fn refresh_llm_tokens(
- Path(user_id): Path<UserId>,
- Extension(rpc_server): Extension<Arc<rpc::Server>>,
-) -> Result<Json<RefreshLlmTokensResponse>> {
- rpc_server.refresh_llm_tokens_for_user(user_id).await;
-
- Ok(Json(RefreshLlmTokensResponse {}))
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-struct UpdatePlanBody {
- pub plan: cloud_llm_client::Plan,
- pub subscription_period: SubscriptionPeriod,
- pub usage: cloud_llm_client::CurrentUsage,
- pub trial_started_at: Option<DateTime<Utc>>,
- pub is_usage_based_billing_enabled: bool,
- pub is_account_too_young: bool,
- pub has_overdue_invoices: bool,
-}
-
-#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
-struct SubscriptionPeriod {
- pub started_at: DateTime<Utc>,
- pub ended_at: DateTime<Utc>,
-}
-
-#[derive(Serialize)]
-struct UpdatePlanResponse {}
-
-async fn update_plan(
- Path(user_id): Path<UserId>,
- Extension(rpc_server): Extension<Arc<rpc::Server>>,
- extract::Json(body): extract::Json<UpdatePlanBody>,
-) -> Result<Json<UpdatePlanResponse>> {
- let plan = match body.plan {
- cloud_llm_client::Plan::ZedFree => proto::Plan::Free,
- cloud_llm_client::Plan::ZedPro => proto::Plan::ZedPro,
- cloud_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial,
- };
-
- let update_user_plan = proto::UpdateUserPlan {
- plan: plan.into(),
- trial_started_at: body
- .trial_started_at
- .map(|trial_started_at| trial_started_at.timestamp() as u64),
- is_usage_based_billing_enabled: Some(body.is_usage_based_billing_enabled),
- usage: Some(proto::SubscriptionUsage {
- model_requests_usage_amount: body.usage.model_requests.used,
- model_requests_usage_limit: Some(usage_limit_to_proto(body.usage.model_requests.limit)),
- edit_predictions_usage_amount: body.usage.edit_predictions.used,
- edit_predictions_usage_limit: Some(usage_limit_to_proto(
- body.usage.edit_predictions.limit,
- )),
- }),
- subscription_period: Some(proto::SubscriptionPeriod {
- started_at: body.subscription_period.started_at.timestamp() as u64,
- ended_at: body.subscription_period.ended_at.timestamp() as u64,
- }),
- account_too_young: Some(body.is_account_too_young),
- has_overdue_invoices: Some(body.has_overdue_invoices),
- };
-
- rpc_server
- .update_plan_for_user(user_id, update_user_plan)
- .await?;
-
- Ok(Json(UpdatePlanResponse {}))
-}
-
-fn usage_limit_to_proto(limit: cloud_llm_client::UsageLimit) -> proto::UsageLimit {
- proto::UsageLimit {
- variant: Some(match limit {
- cloud_llm_client::UsageLimit::Limited(limit) => {
- proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
- limit: limit as u32,
- })
- }
- cloud_llm_client::UsageLimit::Unlimited => {
- proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
- }
- }),
- }
-}
@@ -1,59 +0,0 @@
-use std::sync::Arc;
-use stripe::SubscriptionStatus;
-
-use crate::AppState;
-use crate::db::billing_subscription::StripeSubscriptionStatus;
-use crate::db::{CreateBillingCustomerParams, billing_customer};
-use crate::stripe_client::{StripeClient, StripeCustomerId};
-
-impl From<SubscriptionStatus> for StripeSubscriptionStatus {
- fn from(value: SubscriptionStatus) -> Self {
- match value {
- SubscriptionStatus::Incomplete => Self::Incomplete,
- SubscriptionStatus::IncompleteExpired => Self::IncompleteExpired,
- SubscriptionStatus::Trialing => Self::Trialing,
- SubscriptionStatus::Active => Self::Active,
- SubscriptionStatus::PastDue => Self::PastDue,
- SubscriptionStatus::Canceled => Self::Canceled,
- SubscriptionStatus::Unpaid => Self::Unpaid,
- SubscriptionStatus::Paused => Self::Paused,
- }
- }
-}
-
-/// Finds or creates a billing customer using the provided customer.
-pub async fn find_or_create_billing_customer(
- app: &Arc<AppState>,
- stripe_client: &dyn StripeClient,
- customer_id: &StripeCustomerId,
-) -> anyhow::Result<Option<billing_customer::Model>> {
- // If we already have a billing customer record associated with the Stripe customer,
- // there's nothing more we need to do.
- if let Some(billing_customer) = app
- .db
- .get_billing_customer_by_stripe_customer_id(customer_id.0.as_ref())
- .await?
- {
- return Ok(Some(billing_customer));
- }
-
- let customer = stripe_client.get_customer(customer_id).await?;
-
- let Some(email) = customer.email else {
- return Ok(None);
- };
-
- let Some(user) = app.db.get_user_by_email(&email).await? else {
- return Ok(None);
- };
-
- let billing_customer = app
- .db
- .create_billing_customer(&CreateBillingCustomerParams {
- user_id: user.id,
- stripe_customer_id: customer.id.to_string(),
- })
- .await?;
-
- Ok(Some(billing_customer))
-}
@@ -149,35 +149,35 @@ pub async fn post_crash(
"crash report"
);
- if let Some(kinesis_client) = app.kinesis_client.clone() {
- if let Some(stream) = app.config.kinesis_stream.clone() {
- let properties = json!({
- "app_version": report.header.app_version,
- "os_version": report.header.os_version,
- "os_name": "macOS",
- "bundle_id": report.header.bundle_id,
- "incident_id": report.header.incident_id,
- "installation_id": installation_id,
- "description": description,
- "backtrace": summary,
- });
- let row = SnowflakeRow::new(
- "Crash Reported",
- None,
- false,
- Some(installation_id),
- properties,
- );
- let data = serde_json::to_vec(&row)?;
- kinesis_client
- .put_record()
- .stream_name(stream)
- .partition_key(row.insert_id.unwrap_or_default())
- .data(data.into())
- .send()
- .await
- .log_err();
- }
+ if let Some(kinesis_client) = app.kinesis_client.clone()
+ && let Some(stream) = app.config.kinesis_stream.clone()
+ {
+ let properties = json!({
+ "app_version": report.header.app_version,
+ "os_version": report.header.os_version,
+ "os_name": "macOS",
+ "bundle_id": report.header.bundle_id,
+ "incident_id": report.header.incident_id,
+ "installation_id": installation_id,
+ "description": description,
+ "backtrace": summary,
+ });
+ let row = SnowflakeRow::new(
+ "Crash Reported",
+ None,
+ false,
+ Some(installation_id),
+ properties,
+ );
+ let data = serde_json::to_vec(&row)?;
+ kinesis_client
+ .put_record()
+ .stream_name(stream)
+ .partition_key(row.insert_id.unwrap_or_default())
+ .data(data.into())
+ .send()
+ .await
+ .log_err();
}
if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() {
@@ -280,7 +280,7 @@ pub async fn post_hang(
service = "client",
version = %report.app_version.unwrap_or_default().to_string(),
os_name = %report.os_name,
- os_version = report.os_version.unwrap_or_default().to_string(),
+ os_version = report.os_version.unwrap_or_default(),
incident_id = %incident_id,
installation_id = %report.installation_id.unwrap_or_default(),
backtrace = %backtrace,
@@ -359,34 +359,34 @@ pub async fn post_panic(
"panic report"
);
- if let Some(kinesis_client) = app.kinesis_client.clone() {
- if let Some(stream) = app.config.kinesis_stream.clone() {
- let properties = json!({
- "app_version": panic.app_version,
- "os_name": panic.os_name,
- "os_version": panic.os_version,
- "incident_id": incident_id,
- "installation_id": panic.installation_id,
- "description": panic.payload,
- "backtrace": backtrace,
- });
- let row = SnowflakeRow::new(
- "Panic Reported",
- None,
- false,
- panic.installation_id.clone(),
- properties,
- );
- let data = serde_json::to_vec(&row)?;
- kinesis_client
- .put_record()
- .stream_name(stream)
- .partition_key(row.insert_id.unwrap_or_default())
- .data(data.into())
- .send()
- .await
- .log_err();
- }
+ if let Some(kinesis_client) = app.kinesis_client.clone()
+ && let Some(stream) = app.config.kinesis_stream.clone()
+ {
+ let properties = json!({
+ "app_version": panic.app_version,
+ "os_name": panic.os_name,
+ "os_version": panic.os_version,
+ "incident_id": incident_id,
+ "installation_id": panic.installation_id,
+ "description": panic.payload,
+ "backtrace": backtrace,
+ });
+ let row = SnowflakeRow::new(
+ "Panic Reported",
+ None,
+ false,
+ panic.installation_id.clone(),
+ properties,
+ );
+ let data = serde_json::to_vec(&row)?;
+ kinesis_client
+ .put_record()
+ .stream_name(stream)
+ .partition_key(row.insert_id.unwrap_or_default())
+ .data(data.into())
+ .send()
+ .await
+ .log_err();
}
if !report_to_slack(&panic) {
@@ -518,31 +518,31 @@ pub async fn post_events(
let first_event_at = chrono::Utc::now()
- chrono::Duration::milliseconds(last_event.milliseconds_since_first_event);
- if let Some(kinesis_client) = app.kinesis_client.clone() {
- if let Some(stream) = app.config.kinesis_stream.clone() {
- let mut request = kinesis_client.put_records().stream_name(stream);
- let mut has_records = false;
- for row in for_snowflake(
- request_body.clone(),
- first_event_at,
- country_code.clone(),
- checksum_matched,
- ) {
- if let Some(data) = serde_json::to_vec(&row).log_err() {
- request = request.records(
- aws_sdk_kinesis::types::PutRecordsRequestEntry::builder()
- .partition_key(request_body.system_id.clone().unwrap_or_default())
- .data(data.into())
- .build()
- .unwrap(),
- );
- has_records = true;
- }
- }
- if has_records {
- request.send().await.log_err();
+ if let Some(kinesis_client) = app.kinesis_client.clone()
+ && let Some(stream) = app.config.kinesis_stream.clone()
+ {
+ let mut request = kinesis_client.put_records().stream_name(stream);
+ let mut has_records = false;
+ for row in for_snowflake(
+ request_body.clone(),
+ first_event_at,
+ country_code.clone(),
+ checksum_matched,
+ ) {
+ if let Some(data) = serde_json::to_vec(&row).log_err() {
+ request = request.records(
+ aws_sdk_kinesis::types::PutRecordsRequestEntry::builder()
+ .partition_key(request_body.system_id.clone().unwrap_or_default())
+ .data(data.into())
+ .build()
+ .unwrap(),
+ );
+ has_records = true;
}
}
+ if has_records {
+ request.send().await.log_err();
+ }
};
Ok(())
@@ -564,170 +564,10 @@ fn for_snowflake(
country_code: Option<String>,
checksum_matched: bool,
) -> impl Iterator<Item = SnowflakeRow> {
- body.events.into_iter().filter_map(move |event| {
+ body.events.into_iter().map(move |event| {
let timestamp =
first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
- // We will need to double check, but I believe all of the events that
- // are being transformed here are now migrated over to use the
- // telemetry::event! macro, as of this commit so this code can go away
- // when we feel enough users have upgraded past this point.
let (event_type, mut event_properties) = match &event.event {
- Event::Editor(e) => (
- match e.operation.as_str() {
- "open" => "Editor Opened".to_string(),
- "save" => "Editor Saved".to_string(),
- _ => format!("Unknown Editor Event: {}", e.operation),
- },
- serde_json::to_value(e).unwrap(),
- ),
- Event::EditPrediction(e) => (
- format!(
- "Edit Prediction {}",
- if e.suggestion_accepted {
- "Accepted"
- } else {
- "Discarded"
- }
- ),
- serde_json::to_value(e).unwrap(),
- ),
- Event::EditPredictionRating(e) => (
- "Edit Prediction Rated".to_string(),
- serde_json::to_value(e).unwrap(),
- ),
- Event::Call(e) => {
- let event_type = match e.operation.trim() {
- "unshare project" => "Project Unshared".to_string(),
- "open channel notes" => "Channel Notes Opened".to_string(),
- "share project" => "Project Shared".to_string(),
- "join channel" => "Channel Joined".to_string(),
- "hang up" => "Call Ended".to_string(),
- "accept incoming" => "Incoming Call Accepted".to_string(),
- "invite" => "Participant Invited".to_string(),
- "disable microphone" => "Microphone Disabled".to_string(),
- "enable microphone" => "Microphone Enabled".to_string(),
- "enable screen share" => "Screen Share Enabled".to_string(),
- "disable screen share" => "Screen Share Disabled".to_string(),
- "decline incoming" => "Incoming Call Declined".to_string(),
- _ => format!("Unknown Call Event: {}", e.operation),
- };
-
- (event_type, serde_json::to_value(e).unwrap())
- }
- Event::Assistant(e) => (
- match e.phase {
- telemetry_events::AssistantPhase::Response => "Assistant Responded".to_string(),
- telemetry_events::AssistantPhase::Invoked => "Assistant Invoked".to_string(),
- telemetry_events::AssistantPhase::Accepted => {
- "Assistant Response Accepted".to_string()
- }
- telemetry_events::AssistantPhase::Rejected => {
- "Assistant Response Rejected".to_string()
- }
- },
- serde_json::to_value(e).unwrap(),
- ),
- Event::Cpu(_) | Event::Memory(_) => return None,
- Event::App(e) => {
- let mut properties = json!({});
- let event_type = match e.operation.trim() {
- // App
- "open" => "App Opened".to_string(),
- "first open" => "App First Opened".to_string(),
- "first open for release channel" => {
- "App First Opened For Release Channel".to_string()
- }
- "close" => "App Closed".to_string(),
-
- // Project
- "open project" => "Project Opened".to_string(),
- "open node project" => {
- properties["project_type"] = json!("node");
- "Project Opened".to_string()
- }
- "open pnpm project" => {
- properties["project_type"] = json!("pnpm");
- "Project Opened".to_string()
- }
- "open yarn project" => {
- properties["project_type"] = json!("yarn");
- "Project Opened".to_string()
- }
-
- // SSH
- "create ssh server" => "SSH Server Created".to_string(),
- "create ssh project" => "SSH Project Created".to_string(),
- "open ssh project" => "SSH Project Opened".to_string(),
-
- // Welcome Page
- "welcome page: change keymap" => "Welcome Keymap Changed".to_string(),
- "welcome page: change theme" => "Welcome Theme Changed".to_string(),
- "welcome page: close" => "Welcome Page Closed".to_string(),
- "welcome page: edit settings" => "Welcome Settings Edited".to_string(),
- "welcome page: install cli" => "Welcome CLI Installed".to_string(),
- "welcome page: open" => "Welcome Page Opened".to_string(),
- "welcome page: open extensions" => "Welcome Extensions Page Opened".to_string(),
- "welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(),
- "welcome page: toggle diagnostic telemetry" => {
- "Welcome Diagnostic Telemetry Toggled".to_string()
- }
- "welcome page: toggle metric telemetry" => {
- "Welcome Metric Telemetry Toggled".to_string()
- }
- "welcome page: toggle vim" => "Welcome Vim Mode Toggled".to_string(),
- "welcome page: view docs" => "Welcome Documentation Viewed".to_string(),
-
- // Extensions
- "extensions page: open" => "Extensions Page Opened".to_string(),
- "extensions: install extension" => "Extension Installed".to_string(),
- "extensions: uninstall extension" => "Extension Uninstalled".to_string(),
-
- // Misc
- "markdown preview: open" => "Markdown Preview Opened".to_string(),
- "project diagnostics: open" => "Project Diagnostics Opened".to_string(),
- "project search: open" => "Project Search Opened".to_string(),
- "repl sessions: open" => "REPL Session Started".to_string(),
-
- // Feature Upsell
- "feature upsell: toggle vim" => {
- properties["source"] = json!("Feature Upsell");
- "Vim Mode Toggled".to_string()
- }
- _ => e
- .operation
- .strip_prefix("feature upsell: viewed docs (")
- .and_then(|s| s.strip_suffix(')'))
- .map_or_else(
- || format!("Unknown App Event: {}", e.operation),
- |docs_url| {
- properties["url"] = json!(docs_url);
- properties["source"] = json!("Feature Upsell");
- "Documentation Viewed".to_string()
- },
- ),
- };
- (event_type, properties)
- }
- Event::Setting(e) => (
- "Settings Changed".to_string(),
- serde_json::to_value(e).unwrap(),
- ),
- Event::Extension(e) => (
- "Extension Loaded".to_string(),
- serde_json::to_value(e).unwrap(),
- ),
- Event::Edit(e) => (
- "Editor Edited".to_string(),
- serde_json::to_value(e).unwrap(),
- ),
- Event::Action(e) => (
- "Action Invoked".to_string(),
- serde_json::to_value(e).unwrap(),
- ),
- Event::Repl(e) => (
- "Kernel Status Changed".to_string(),
- serde_json::to_value(e).unwrap(),
- ),
Event::Flexible(e) => (
e.event_type.clone(),
serde_json::to_value(&e.event_properties).unwrap(),
@@ -759,7 +599,7 @@ fn for_snowflake(
})
});
- Some(SnowflakeRow {
+ SnowflakeRow {
time: timestamp,
user_id: body.metrics_id.clone(),
device_id: body.system_id.clone(),
@@ -767,7 +607,7 @@ fn for_snowflake(
event_properties,
user_properties,
insert_id: Some(Uuid::new_v4().to_string()),
- })
+ }
})
}
@@ -337,8 +337,7 @@ async fn fetch_extensions_from_blob_store(
if known_versions
.binary_search_by_key(&published_version, |known_version| known_version)
.is_err()
- {
- if let Some(extension) = fetch_extension_manifest(
+ && let Some(extension) = fetch_extension_manifest(
blob_store_client,
blob_store_bucket,
extension_id,
@@ -346,12 +345,11 @@ async fn fetch_extensions_from_blob_store(
)
.await
.log_err()
- {
- new_versions
- .entry(extension_id)
- .or_default()
- .push(extension);
- }
+ {
+ new_versions
+ .entry(extension_id)
+ .or_default()
+ .push(extension);
}
}
}
@@ -79,27 +79,27 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
verify_access_token(access_token, user_id, &state.db).await
};
- if let Ok(validate_result) = validate_result {
- if validate_result.is_valid {
- let user = state
+ if let Ok(validate_result) = validate_result
+ && validate_result.is_valid
+ {
+ let user = state
+ .db
+ .get_user_by_id(user_id)
+ .await?
+ .with_context(|| format!("user {user_id} not found"))?;
+
+ if let Some(impersonator_id) = validate_result.impersonator_id {
+ let admin = state
.db
- .get_user_by_id(user_id)
+ .get_user_by_id(impersonator_id)
.await?
- .with_context(|| format!("user {user_id} not found"))?;
-
- if let Some(impersonator_id) = validate_result.impersonator_id {
- let admin = state
- .db
- .get_user_by_id(impersonator_id)
- .await?
- .with_context(|| format!("user {impersonator_id} not found"))?;
- req.extensions_mut()
- .insert(Principal::Impersonated { user, admin });
- } else {
- req.extensions_mut().insert(Principal::User(user));
- };
- return Ok::<_, Error>(next.run(req).await);
- }
+ .with_context(|| format!("user {impersonator_id} not found"))?;
+ req.extensions_mut()
+ .insert(Principal::Impersonated { user, admin });
+ } else {
+ req.extensions_mut().insert(Principal::User(user));
+ };
+ return Ok::<_, Error>(next.run(req).await);
}
Err(Error::http(
@@ -227,7 +227,7 @@ pub async fn verify_access_token(
#[cfg(test)]
mod test {
- use rand::thread_rng;
+ use rand::prelude::*;
use scrypt::password_hash::{PasswordHasher, SaltString};
use sea_orm::EntityTrait;
@@ -236,7 +236,7 @@ mod test {
#[gpui::test]
async fn test_verify_access_token(cx: &mut gpui::TestAppContext) {
- let test_db = crate::db::TestDb::sqlite(cx.executor().clone());
+ let test_db = crate::db::TestDb::sqlite(cx.executor());
let db = test_db.db();
let user = db
@@ -358,9 +358,42 @@ mod test {
None,
None,
params,
- &SaltString::generate(thread_rng()),
+ &SaltString::generate(PasswordHashRngCompat::new()),
)
.map_err(anyhow::Error::new)?
.to_string())
}
+
+ // TODO: remove once we password_hash v0.6 is released.
+ struct PasswordHashRngCompat(rand::rngs::ThreadRng);
+
+ impl PasswordHashRngCompat {
+ fn new() -> Self {
+ Self(rand::rng())
+ }
+ }
+
+ impl scrypt::password_hash::rand_core::RngCore for PasswordHashRngCompat {
+ fn next_u32(&mut self) -> u32 {
+ self.0.next_u32()
+ }
+
+ fn next_u64(&mut self) -> u64 {
+ self.0.next_u64()
+ }
+
+ fn fill_bytes(&mut self, dest: &mut [u8]) {
+ self.0.fill_bytes(dest);
+ }
+
+ fn try_fill_bytes(
+ &mut self,
+ dest: &mut [u8],
+ ) -> Result<(), scrypt::password_hash::rand_core::Error> {
+ self.fill_bytes(dest);
+ Ok(())
+ }
+ }
+
+ impl scrypt::password_hash::rand_core::CryptoRng for PasswordHashRngCompat {}
}
@@ -26,7 +26,6 @@ use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use std::ops::RangeInclusive;
use std::{
- fmt::Write as _,
future::Future,
marker::PhantomData,
ops::{Deref, DerefMut},
@@ -41,12 +40,7 @@ use worktree_settings_file::LocalSettingsKind;
pub use tests::TestDb;
pub use ids::*;
-pub use queries::billing_customers::{CreateBillingCustomerParams, UpdateBillingCustomerParams};
-pub use queries::billing_subscriptions::{
- CreateBillingSubscriptionParams, UpdateBillingSubscriptionParams,
-};
pub use queries::contributors::ContributorSelector;
-pub use queries::processed_stripe_events::CreateProcessedStripeEventParams;
pub use sea_orm::ConnectOptions;
pub use tables::user::Model as User;
pub use tables::*;
@@ -261,7 +255,7 @@ impl Database {
let test_options = self.test_options.as_ref().unwrap();
test_options.executor.simulate_random_delay().await;
let fail_probability = *test_options.query_failure_probability.lock();
- if test_options.executor.rng().gen_bool(fail_probability) {
+ if test_options.executor.rng().random_bool(fail_probability) {
return Err(anyhow!("simulated query failure"))?;
}
@@ -491,9 +485,7 @@ pub struct ChannelsForUser {
pub invited_channels: Vec<Channel>,
pub observed_buffer_versions: Vec<proto::ChannelBufferVersion>,
- pub observed_channel_messages: Vec<proto::ChannelMessageId>,
pub latest_buffer_versions: Vec<proto::ChannelBufferVersion>,
- pub latest_channel_messages: Vec<proto::ChannelMessageId>,
}
#[derive(Debug)]
@@ -690,7 +682,7 @@ impl LocalSettingsKind {
}
}
- pub fn to_proto(&self) -> proto::LocalSettingsKind {
+ pub fn to_proto(self) -> proto::LocalSettingsKind {
match self {
Self::Settings => proto::LocalSettingsKind::Settings,
Self::Tasks => proto::LocalSettingsKind::Tasks,
@@ -70,9 +70,6 @@ macro_rules! id_type {
}
id_type!(AccessTokenId);
-id_type!(BillingCustomerId);
-id_type!(BillingSubscriptionId);
-id_type!(BillingPreferencesId);
id_type!(BufferId);
id_type!(ChannelBufferCollaboratorId);
id_type!(ChannelChatParticipantId);
@@ -1,18 +1,13 @@
use super::*;
pub mod access_tokens;
-pub mod billing_customers;
-pub mod billing_preferences;
-pub mod billing_subscriptions;
pub mod buffers;
pub mod channels;
pub mod contacts;
pub mod contributors;
pub mod embeddings;
pub mod extensions;
-pub mod messages;
pub mod notifications;
-pub mod processed_stripe_events;
pub mod projects;
pub mod rooms;
pub mod servers;
@@ -1,100 +0,0 @@
-use super::*;
-
-#[derive(Debug)]
-pub struct CreateBillingCustomerParams {
- pub user_id: UserId,
- pub stripe_customer_id: String,
-}
-
-#[derive(Debug, Default)]
-pub struct UpdateBillingCustomerParams {
- pub user_id: ActiveValue<UserId>,
- pub stripe_customer_id: ActiveValue<String>,
- pub has_overdue_invoices: ActiveValue<bool>,
- pub trial_started_at: ActiveValue<Option<DateTime>>,
-}
-
-impl Database {
- /// Creates a new billing customer.
- pub async fn create_billing_customer(
- &self,
- params: &CreateBillingCustomerParams,
- ) -> Result<billing_customer::Model> {
- self.transaction(|tx| async move {
- let customer = billing_customer::Entity::insert(billing_customer::ActiveModel {
- user_id: ActiveValue::set(params.user_id),
- stripe_customer_id: ActiveValue::set(params.stripe_customer_id.clone()),
- ..Default::default()
- })
- .exec_with_returning(&*tx)
- .await?;
-
- Ok(customer)
- })
- .await
- }
-
- /// Updates the specified billing customer.
- pub async fn update_billing_customer(
- &self,
- id: BillingCustomerId,
- params: &UpdateBillingCustomerParams,
- ) -> Result<()> {
- self.transaction(|tx| async move {
- billing_customer::Entity::update(billing_customer::ActiveModel {
- id: ActiveValue::set(id),
- user_id: params.user_id.clone(),
- stripe_customer_id: params.stripe_customer_id.clone(),
- has_overdue_invoices: params.has_overdue_invoices.clone(),
- trial_started_at: params.trial_started_at.clone(),
- created_at: ActiveValue::not_set(),
- })
- .exec(&*tx)
- .await?;
-
- Ok(())
- })
- .await
- }
-
- pub async fn get_billing_customer_by_id(
- &self,
- id: BillingCustomerId,
- ) -> Result<Option<billing_customer::Model>> {
- self.transaction(|tx| async move {
- Ok(billing_customer::Entity::find()
- .filter(billing_customer::Column::Id.eq(id))
- .one(&*tx)
- .await?)
- })
- .await
- }
-
- /// Returns the billing customer for the user with the specified ID.
- pub async fn get_billing_customer_by_user_id(
- &self,
- user_id: UserId,
- ) -> Result<Option<billing_customer::Model>> {
- self.transaction(|tx| async move {
- Ok(billing_customer::Entity::find()
- .filter(billing_customer::Column::UserId.eq(user_id))
- .one(&*tx)
- .await?)
- })
- .await
- }
-
- /// Returns the billing customer for the user with the specified Stripe customer ID.
- pub async fn get_billing_customer_by_stripe_customer_id(
- &self,
- stripe_customer_id: &str,
- ) -> Result<Option<billing_customer::Model>> {
- self.transaction(|tx| async move {
- Ok(billing_customer::Entity::find()
- .filter(billing_customer::Column::StripeCustomerId.eq(stripe_customer_id))
- .one(&*tx)
- .await?)
- })
- .await
- }
-}
@@ -1,17 +0,0 @@
-use super::*;
-
-impl Database {
- /// Returns the billing preferences for the given user, if they exist.
- pub async fn get_billing_preferences(
- &self,
- user_id: UserId,
- ) -> Result<Option<billing_preference::Model>> {
- self.transaction(|tx| async move {
- Ok(billing_preference::Entity::find()
- .filter(billing_preference::Column::UserId.eq(user_id))
- .one(&*tx)
- .await?)
- })
- .await
- }
-}
@@ -1,158 +0,0 @@
-use anyhow::Context as _;
-
-use crate::db::billing_subscription::{
- StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
-};
-
-use super::*;
-
-#[derive(Debug)]
-pub struct CreateBillingSubscriptionParams {
- pub billing_customer_id: BillingCustomerId,
- pub kind: Option<SubscriptionKind>,
- pub stripe_subscription_id: String,
- pub stripe_subscription_status: StripeSubscriptionStatus,
- pub stripe_cancellation_reason: Option<StripeCancellationReason>,
- pub stripe_current_period_start: Option<i64>,
- pub stripe_current_period_end: Option<i64>,
-}
-
-#[derive(Debug, Default)]
-pub struct UpdateBillingSubscriptionParams {
- pub billing_customer_id: ActiveValue<BillingCustomerId>,
- pub kind: ActiveValue<Option<SubscriptionKind>>,
- pub stripe_subscription_id: ActiveValue<String>,
- pub stripe_subscription_status: ActiveValue<StripeSubscriptionStatus>,
- pub stripe_cancel_at: ActiveValue<Option<DateTime>>,
- pub stripe_cancellation_reason: ActiveValue<Option<StripeCancellationReason>>,
- pub stripe_current_period_start: ActiveValue<Option<i64>>,
- pub stripe_current_period_end: ActiveValue<Option<i64>>,
-}
-
-impl Database {
- /// Creates a new billing subscription.
- pub async fn create_billing_subscription(
- &self,
- params: &CreateBillingSubscriptionParams,
- ) -> Result<billing_subscription::Model> {
- self.transaction(|tx| async move {
- let id = billing_subscription::Entity::insert(billing_subscription::ActiveModel {
- billing_customer_id: ActiveValue::set(params.billing_customer_id),
- kind: ActiveValue::set(params.kind),
- stripe_subscription_id: ActiveValue::set(params.stripe_subscription_id.clone()),
- stripe_subscription_status: ActiveValue::set(params.stripe_subscription_status),
- stripe_cancellation_reason: ActiveValue::set(params.stripe_cancellation_reason),
- stripe_current_period_start: ActiveValue::set(params.stripe_current_period_start),
- stripe_current_period_end: ActiveValue::set(params.stripe_current_period_end),
- ..Default::default()
- })
- .exec(&*tx)
- .await?
- .last_insert_id;
-
- Ok(billing_subscription::Entity::find_by_id(id)
- .one(&*tx)
- .await?
- .context("failed to retrieve inserted billing subscription")?)
- })
- .await
- }
-
- /// Updates the specified billing subscription.
- pub async fn update_billing_subscription(
- &self,
- id: BillingSubscriptionId,
- params: &UpdateBillingSubscriptionParams,
- ) -> Result<()> {
- self.transaction(|tx| async move {
- billing_subscription::Entity::update(billing_subscription::ActiveModel {
- id: ActiveValue::set(id),
- billing_customer_id: params.billing_customer_id.clone(),
- kind: params.kind.clone(),
- stripe_subscription_id: params.stripe_subscription_id.clone(),
- stripe_subscription_status: params.stripe_subscription_status.clone(),
- stripe_cancel_at: params.stripe_cancel_at.clone(),
- stripe_cancellation_reason: params.stripe_cancellation_reason.clone(),
- stripe_current_period_start: params.stripe_current_period_start.clone(),
- stripe_current_period_end: params.stripe_current_period_end.clone(),
- created_at: ActiveValue::not_set(),
- })
- .exec(&*tx)
- .await?;
-
- Ok(())
- })
- .await
- }
-
- /// Returns the billing subscription with the specified Stripe subscription ID.
- pub async fn get_billing_subscription_by_stripe_subscription_id(
- &self,
- stripe_subscription_id: &str,
- ) -> Result<Option<billing_subscription::Model>> {
- self.transaction(|tx| async move {
- Ok(billing_subscription::Entity::find()
- .filter(
- billing_subscription::Column::StripeSubscriptionId.eq(stripe_subscription_id),
- )
- .one(&*tx)
- .await?)
- })
- .await
- }
-
- pub async fn get_active_billing_subscription(
- &self,
- user_id: UserId,
- ) -> Result<Option<billing_subscription::Model>> {
- self.transaction(|tx| async move {
- Ok(billing_subscription::Entity::find()
- .inner_join(billing_customer::Entity)
- .filter(billing_customer::Column::UserId.eq(user_id))
- .filter(
- Condition::all()
- .add(
- Condition::any()
- .add(
- billing_subscription::Column::StripeSubscriptionStatus
- .eq(StripeSubscriptionStatus::Active),
- )
- .add(
- billing_subscription::Column::StripeSubscriptionStatus
- .eq(StripeSubscriptionStatus::Trialing),
- ),
- )
- .add(billing_subscription::Column::Kind.is_not_null()),
- )
- .one(&*tx)
- .await?)
- })
- .await
- }
-
- /// Returns whether the user has an active billing subscription.
- pub async fn has_active_billing_subscription(&self, user_id: UserId) -> Result<bool> {
- Ok(self.count_active_billing_subscriptions(user_id).await? > 0)
- }
-
- /// Returns the count of the active billing subscriptions for the user with the specified ID.
- pub async fn count_active_billing_subscriptions(&self, user_id: UserId) -> Result<usize> {
- self.transaction(|tx| async move {
- let count = billing_subscription::Entity::find()
- .inner_join(billing_customer::Entity)
- .filter(
- billing_customer::Column::UserId.eq(user_id).and(
- billing_subscription::Column::StripeSubscriptionStatus
- .eq(StripeSubscriptionStatus::Active)
- .or(billing_subscription::Column::StripeSubscriptionStatus
- .eq(StripeSubscriptionStatus::Trialing)),
- ),
- )
- .count(&*tx)
- .await?;
-
- Ok(count as usize)
- })
- .await
- }
-}
@@ -618,25 +618,17 @@ impl Database {
}
drop(rows);
- let latest_channel_messages = self.latest_channel_messages(&channel_ids, tx).await?;
-
let observed_buffer_versions = self
.observed_channel_buffer_changes(&channel_ids_by_buffer_id, user_id, tx)
.await?;
- let observed_channel_messages = self
- .observed_channel_messages(&channel_ids, user_id, tx)
- .await?;
-
Ok(ChannelsForUser {
channel_memberships,
channels,
invited_channels,
channel_participants,
latest_buffer_versions,
- latest_channel_messages,
observed_buffer_versions,
- observed_channel_messages,
})
}
@@ -87,10 +87,10 @@ impl Database {
continue;
};
- if let Some((_, max_extension_version)) = &max_versions.get(&version.extension_id) {
- if max_extension_version > &extension_version {
- continue;
- }
+ if let Some((_, max_extension_version)) = &max_versions.get(&version.extension_id)
+ && max_extension_version > &extension_version
+ {
+ continue;
}
if let Some(constraints) = constraints {
@@ -331,10 +331,10 @@ impl Database {
.exec_without_returning(&*tx)
.await?;
- if let Ok(db_version) = semver::Version::parse(&extension.latest_version) {
- if db_version >= latest_version.version {
- continue;
- }
+ if let Ok(db_version) = semver::Version::parse(&extension.latest_version)
+ && db_version >= latest_version.version
+ {
+ continue;
}
let mut extension = extension.into_active_model();
@@ -1,725 +0,0 @@
-use super::*;
-use anyhow::Context as _;
-use rpc::Notification;
-use sea_orm::{SelectColumns, TryInsertResult};
-use time::OffsetDateTime;
-use util::ResultExt;
-
-impl Database {
- /// Inserts a record representing a user joining the chat for a given channel.
- pub async fn join_channel_chat(
- &self,
- channel_id: ChannelId,
- connection_id: ConnectionId,
- user_id: UserId,
- ) -> Result<()> {
- self.transaction(|tx| async move {
- let channel = self.get_channel_internal(channel_id, &tx).await?;
- self.check_user_is_channel_participant(&channel, user_id, &tx)
- .await?;
- channel_chat_participant::ActiveModel {
- id: ActiveValue::NotSet,
- channel_id: ActiveValue::Set(channel_id),
- user_id: ActiveValue::Set(user_id),
- connection_id: ActiveValue::Set(connection_id.id as i32),
- connection_server_id: ActiveValue::Set(ServerId(connection_id.owner_id as i32)),
- }
- .insert(&*tx)
- .await?;
- Ok(())
- })
- .await
- }
-
- /// Removes `channel_chat_participant` records associated with the given connection ID.
- pub async fn channel_chat_connection_lost(
- &self,
- connection_id: ConnectionId,
- tx: &DatabaseTransaction,
- ) -> Result<()> {
- channel_chat_participant::Entity::delete_many()
- .filter(
- Condition::all()
- .add(
- channel_chat_participant::Column::ConnectionServerId
- .eq(connection_id.owner_id),
- )
- .add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id)),
- )
- .exec(tx)
- .await?;
- Ok(())
- }
-
- /// Removes `channel_chat_participant` records associated with the given user ID so they
- /// will no longer get chat notifications.
- pub async fn leave_channel_chat(
- &self,
- channel_id: ChannelId,
- connection_id: ConnectionId,
- _user_id: UserId,
- ) -> Result<()> {
- self.transaction(|tx| async move {
- channel_chat_participant::Entity::delete_many()
- .filter(
- Condition::all()
- .add(
- channel_chat_participant::Column::ConnectionServerId
- .eq(connection_id.owner_id),
- )
- .add(channel_chat_participant::Column::ConnectionId.eq(connection_id.id))
- .add(channel_chat_participant::Column::ChannelId.eq(channel_id)),
- )
- .exec(&*tx)
- .await?;
-
- Ok(())
- })
- .await
- }
-
- /// Retrieves the messages in the specified channel.
- ///
- /// Use `before_message_id` to paginate through the channel's messages.
- pub async fn get_channel_messages(
- &self,
- channel_id: ChannelId,
- user_id: UserId,
- count: usize,
- before_message_id: Option<MessageId>,
- ) -> Result<Vec<proto::ChannelMessage>> {
- self.transaction(|tx| async move {
- let channel = self.get_channel_internal(channel_id, &tx).await?;
- self.check_user_is_channel_participant(&channel, user_id, &tx)
- .await?;
-
- let mut condition =
- Condition::all().add(channel_message::Column::ChannelId.eq(channel_id));
-
- if let Some(before_message_id) = before_message_id {
- condition = condition.add(channel_message::Column::Id.lt(before_message_id));
- }
-
- let rows = channel_message::Entity::find()
- .filter(condition)
- .order_by_desc(channel_message::Column::Id)
- .limit(count as u64)
- .all(&*tx)
- .await?;
-
- self.load_channel_messages(rows, &tx).await
- })
- .await
- }
-
- /// Returns the channel messages with the given IDs.
- pub async fn get_channel_messages_by_id(
- &self,
- user_id: UserId,
- message_ids: &[MessageId],
- ) -> Result<Vec<proto::ChannelMessage>> {
- self.transaction(|tx| async move {
- let rows = channel_message::Entity::find()
- .filter(channel_message::Column::Id.is_in(message_ids.iter().copied()))
- .order_by_desc(channel_message::Column::Id)
- .all(&*tx)
- .await?;
-
- let mut channels = HashMap::<ChannelId, channel::Model>::default();
- for row in &rows {
- channels.insert(
- row.channel_id,
- self.get_channel_internal(row.channel_id, &tx).await?,
- );
- }
-
- for (_, channel) in channels {
- self.check_user_is_channel_participant(&channel, user_id, &tx)
- .await?;
- }
-
- let messages = self.load_channel_messages(rows, &tx).await?;
- Ok(messages)
- })
- .await
- }
-
- async fn load_channel_messages(
- &self,
- rows: Vec<channel_message::Model>,
- tx: &DatabaseTransaction,
- ) -> Result<Vec<proto::ChannelMessage>> {
- let mut messages = rows
- .into_iter()
- .map(|row| {
- let nonce = row.nonce.as_u64_pair();
- proto::ChannelMessage {
- id: row.id.to_proto(),
- sender_id: row.sender_id.to_proto(),
- body: row.body,
- timestamp: row.sent_at.assume_utc().unix_timestamp() as u64,
- mentions: vec![],
- nonce: Some(proto::Nonce {
- upper_half: nonce.0,
- lower_half: nonce.1,
- }),
- reply_to_message_id: row.reply_to_message_id.map(|id| id.to_proto()),
- edited_at: row
- .edited_at
- .map(|t| t.assume_utc().unix_timestamp() as u64),
- }
- })
- .collect::<Vec<_>>();
- messages.reverse();
-
- let mut mentions = channel_message_mention::Entity::find()
- .filter(channel_message_mention::Column::MessageId.is_in(messages.iter().map(|m| m.id)))
- .order_by_asc(channel_message_mention::Column::MessageId)
- .order_by_asc(channel_message_mention::Column::StartOffset)
- .stream(tx)
- .await?;
-
- let mut message_ix = 0;
- while let Some(mention) = mentions.next().await {
- let mention = mention?;
- let message_id = mention.message_id.to_proto();
- while let Some(message) = messages.get_mut(message_ix) {
- if message.id < message_id {
- message_ix += 1;
- } else {
- if message.id == message_id {
- message.mentions.push(proto::ChatMention {
- range: Some(proto::Range {
- start: mention.start_offset as u64,
- end: mention.end_offset as u64,
- }),
- user_id: mention.user_id.to_proto(),
- });
- }
- break;
- }
- }
- }
-
- Ok(messages)
- }
-
- fn format_mentions_to_entities(
- &self,
- message_id: MessageId,
- body: &str,
- mentions: &[proto::ChatMention],
- ) -> Result<Vec<tables::channel_message_mention::ActiveModel>> {
- Ok(mentions
- .iter()
- .filter_map(|mention| {
- let range = mention.range.as_ref()?;
- if !body.is_char_boundary(range.start as usize)
- || !body.is_char_boundary(range.end as usize)
- {
- return None;
- }
- Some(channel_message_mention::ActiveModel {
- message_id: ActiveValue::Set(message_id),
- start_offset: ActiveValue::Set(range.start as i32),
- end_offset: ActiveValue::Set(range.end as i32),
- user_id: ActiveValue::Set(UserId::from_proto(mention.user_id)),
- })
- })
- .collect::<Vec<_>>())
- }
-
- /// Creates a new channel message.
- pub async fn create_channel_message(
- &self,
- channel_id: ChannelId,
- user_id: UserId,
- body: &str,
- mentions: &[proto::ChatMention],
- timestamp: OffsetDateTime,
- nonce: u128,
- reply_to_message_id: Option<MessageId>,
- ) -> Result<CreatedChannelMessage> {
- self.transaction(|tx| async move {
- let channel = self.get_channel_internal(channel_id, &tx).await?;
- self.check_user_is_channel_participant(&channel, user_id, &tx)
- .await?;
-
- let mut rows = channel_chat_participant::Entity::find()
- .filter(channel_chat_participant::Column::ChannelId.eq(channel_id))
- .stream(&*tx)
- .await?;
-
- let mut is_participant = false;
- let mut participant_connection_ids = HashSet::default();
- let mut participant_user_ids = Vec::new();
- while let Some(row) = rows.next().await {
- let row = row?;
- if row.user_id == user_id {
- is_participant = true;
- }
- participant_user_ids.push(row.user_id);
- participant_connection_ids.insert(row.connection());
- }
- drop(rows);
-
- if !is_participant {
- Err(anyhow!("not a chat participant"))?;
- }
-
- let timestamp = timestamp.to_offset(time::UtcOffset::UTC);
- let timestamp = time::PrimitiveDateTime::new(timestamp.date(), timestamp.time());
-
- let result = channel_message::Entity::insert(channel_message::ActiveModel {
- channel_id: ActiveValue::Set(channel_id),
- sender_id: ActiveValue::Set(user_id),
- body: ActiveValue::Set(body.to_string()),
- sent_at: ActiveValue::Set(timestamp),
- nonce: ActiveValue::Set(Uuid::from_u128(nonce)),
- id: ActiveValue::NotSet,
- reply_to_message_id: ActiveValue::Set(reply_to_message_id),
- edited_at: ActiveValue::NotSet,
- })
- .on_conflict(
- OnConflict::columns([
- channel_message::Column::SenderId,
- channel_message::Column::Nonce,
- ])
- .do_nothing()
- .to_owned(),
- )
- .do_nothing()
- .exec(&*tx)
- .await?;
-
- let message_id;
- let mut notifications = Vec::new();
- match result {
- TryInsertResult::Inserted(result) => {
- message_id = result.last_insert_id;
- let mentioned_user_ids =
- mentions.iter().map(|m| m.user_id).collect::<HashSet<_>>();
-
- let mentions = self.format_mentions_to_entities(message_id, body, mentions)?;
- if !mentions.is_empty() {
- channel_message_mention::Entity::insert_many(mentions)
- .exec(&*tx)
- .await?;
- }
-
- for mentioned_user in mentioned_user_ids {
- notifications.extend(
- self.create_notification(
- UserId::from_proto(mentioned_user),
- rpc::Notification::ChannelMessageMention {
- message_id: message_id.to_proto(),
- sender_id: user_id.to_proto(),
- channel_id: channel_id.to_proto(),
- },
- false,
- &tx,
- )
- .await?,
- );
- }
-
- self.observe_channel_message_internal(channel_id, user_id, message_id, &tx)
- .await?;
- }
- _ => {
- message_id = channel_message::Entity::find()
- .filter(channel_message::Column::Nonce.eq(Uuid::from_u128(nonce)))
- .one(&*tx)
- .await?
- .context("failed to insert message")?
- .id;
- }
- }
-
- Ok(CreatedChannelMessage {
- message_id,
- participant_connection_ids,
- notifications,
- })
- })
- .await
- }
-
- pub async fn observe_channel_message(
- &self,
- channel_id: ChannelId,
- user_id: UserId,
- message_id: MessageId,
- ) -> Result<NotificationBatch> {
- self.transaction(|tx| async move {
- self.observe_channel_message_internal(channel_id, user_id, message_id, &tx)
- .await?;
- let mut batch = NotificationBatch::default();
- batch.extend(
- self.mark_notification_as_read(
- user_id,
- &Notification::ChannelMessageMention {
- message_id: message_id.to_proto(),
- sender_id: Default::default(),
- channel_id: Default::default(),
- },
- &tx,
- )
- .await?,
- );
- Ok(batch)
- })
- .await
- }
-
- async fn observe_channel_message_internal(
- &self,
- channel_id: ChannelId,
- user_id: UserId,
- message_id: MessageId,
- tx: &DatabaseTransaction,
- ) -> Result<()> {
- observed_channel_messages::Entity::insert(observed_channel_messages::ActiveModel {
- user_id: ActiveValue::Set(user_id),
- channel_id: ActiveValue::Set(channel_id),
- channel_message_id: ActiveValue::Set(message_id),
- })
- .on_conflict(
- OnConflict::columns([
- observed_channel_messages::Column::ChannelId,
- observed_channel_messages::Column::UserId,
- ])
- .update_column(observed_channel_messages::Column::ChannelMessageId)
- .action_cond_where(observed_channel_messages::Column::ChannelMessageId.lt(message_id))
- .to_owned(),
- )
- // TODO: Try to upgrade SeaORM so we don't have to do this hack around their bug
- .exec_without_returning(tx)
- .await?;
- Ok(())
- }
-
- pub async fn observed_channel_messages(
- &self,
- channel_ids: &[ChannelId],
- user_id: UserId,
- tx: &DatabaseTransaction,
- ) -> Result<Vec<proto::ChannelMessageId>> {
- let rows = observed_channel_messages::Entity::find()
- .filter(observed_channel_messages::Column::UserId.eq(user_id))
- .filter(
- observed_channel_messages::Column::ChannelId
- .is_in(channel_ids.iter().map(|id| id.0)),
- )
- .all(tx)
- .await?;
-
- Ok(rows
- .into_iter()
- .map(|message| proto::ChannelMessageId {
- channel_id: message.channel_id.to_proto(),
- message_id: message.channel_message_id.to_proto(),
- })
- .collect())
- }
-
- pub async fn latest_channel_messages(
- &self,
- channel_ids: &[ChannelId],
- tx: &DatabaseTransaction,
- ) -> Result<Vec<proto::ChannelMessageId>> {
- let mut values = String::new();
- for id in channel_ids {
- if !values.is_empty() {
- values.push_str(", ");
- }
- write!(&mut values, "({})", id).unwrap();
- }
-
- if values.is_empty() {
- return Ok(Vec::default());
- }
-
- let sql = format!(
- r#"
- SELECT
- *
- FROM (
- SELECT
- *,
- row_number() OVER (
- PARTITION BY channel_id
- ORDER BY id DESC
- ) as row_number
- FROM channel_messages
- WHERE
- channel_id in ({values})
- ) AS messages
- WHERE
- row_number = 1
- "#,
- );
-
- let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
- let mut last_messages = channel_message::Model::find_by_statement(stmt)
- .stream(tx)
- .await?;
-
- let mut results = Vec::new();
- while let Some(result) = last_messages.next().await {
- let message = result?;
- results.push(proto::ChannelMessageId {
- channel_id: message.channel_id.to_proto(),
- message_id: message.id.to_proto(),
- });
- }
-
- Ok(results)
- }
-
- fn get_notification_kind_id_by_name(&self, notification_kind: &str) -> Option<i32> {
- self.notification_kinds_by_id
- .iter()
- .find(|(_, kind)| **kind == notification_kind)
- .map(|kind| kind.0.0)
- }
-
- /// Removes the channel message with the given ID.
- pub async fn remove_channel_message(
- &self,
- channel_id: ChannelId,
- message_id: MessageId,
- user_id: UserId,
- ) -> Result<(Vec<ConnectionId>, Vec<NotificationId>)> {
- self.transaction(|tx| async move {
- let mut rows = channel_chat_participant::Entity::find()
- .filter(channel_chat_participant::Column::ChannelId.eq(channel_id))
- .stream(&*tx)
- .await?;
-
- let mut is_participant = false;
- let mut participant_connection_ids = Vec::new();
- while let Some(row) = rows.next().await {
- let row = row?;
- if row.user_id == user_id {
- is_participant = true;
- }
- participant_connection_ids.push(row.connection());
- }
- drop(rows);
-
- if !is_participant {
- Err(anyhow!("not a chat participant"))?;
- }
-
- let result = channel_message::Entity::delete_by_id(message_id)
- .filter(channel_message::Column::SenderId.eq(user_id))
- .exec(&*tx)
- .await?;
-
- if result.rows_affected == 0 {
- let channel = self.get_channel_internal(channel_id, &tx).await?;
- if self
- .check_user_is_channel_admin(&channel, user_id, &tx)
- .await
- .is_ok()
- {
- let result = channel_message::Entity::delete_by_id(message_id)
- .exec(&*tx)
- .await?;
- if result.rows_affected == 0 {
- Err(anyhow!("no such message"))?;
- }
- } else {
- Err(anyhow!("operation could not be completed"))?;
- }
- }
-
- let notification_kind_id =
- self.get_notification_kind_id_by_name("ChannelMessageMention");
-
- let existing_notifications = notification::Entity::find()
- .filter(notification::Column::EntityId.eq(message_id))
- .filter(notification::Column::Kind.eq(notification_kind_id))
- .select_column(notification::Column::Id)
- .all(&*tx)
- .await?;
-
- let existing_notification_ids = existing_notifications
- .into_iter()
- .map(|notification| notification.id)
- .collect();
-
- // remove all the mention notifications for this message
- notification::Entity::delete_many()
- .filter(notification::Column::EntityId.eq(message_id))
- .filter(notification::Column::Kind.eq(notification_kind_id))
- .exec(&*tx)
- .await?;
-
- Ok((participant_connection_ids, existing_notification_ids))
- })
- .await
- }
-
- /// Updates the channel message with the given ID, body and timestamp(edited_at).
- pub async fn update_channel_message(
- &self,
- channel_id: ChannelId,
- message_id: MessageId,
- user_id: UserId,
- body: &str,
- mentions: &[proto::ChatMention],
- edited_at: OffsetDateTime,
- ) -> Result<UpdatedChannelMessage> {
- self.transaction(|tx| async move {
- let channel = self.get_channel_internal(channel_id, &tx).await?;
- self.check_user_is_channel_participant(&channel, user_id, &tx)
- .await?;
-
- let mut rows = channel_chat_participant::Entity::find()
- .filter(channel_chat_participant::Column::ChannelId.eq(channel_id))
- .stream(&*tx)
- .await?;
-
- let mut is_participant = false;
- let mut participant_connection_ids = Vec::new();
- let mut participant_user_ids = Vec::new();
- while let Some(row) = rows.next().await {
- let row = row?;
- if row.user_id == user_id {
- is_participant = true;
- }
- participant_user_ids.push(row.user_id);
- participant_connection_ids.push(row.connection());
- }
- drop(rows);
-
- if !is_participant {
- Err(anyhow!("not a chat participant"))?;
- }
-
- let channel_message = channel_message::Entity::find_by_id(message_id)
- .filter(channel_message::Column::SenderId.eq(user_id))
- .one(&*tx)
- .await?;
-
- let Some(channel_message) = channel_message else {
- Err(anyhow!("Channel message not found"))?
- };
-
- let edited_at = edited_at.to_offset(time::UtcOffset::UTC);
- let edited_at = time::PrimitiveDateTime::new(edited_at.date(), edited_at.time());
-
- let updated_message = channel_message::ActiveModel {
- body: ActiveValue::Set(body.to_string()),
- edited_at: ActiveValue::Set(Some(edited_at)),
- reply_to_message_id: ActiveValue::Unchanged(channel_message.reply_to_message_id),
- id: ActiveValue::Unchanged(message_id),
- channel_id: ActiveValue::Unchanged(channel_id),
- sender_id: ActiveValue::Unchanged(user_id),
- sent_at: ActiveValue::Unchanged(channel_message.sent_at),
- nonce: ActiveValue::Unchanged(channel_message.nonce),
- };
-
- let result = channel_message::Entity::update_many()
- .set(updated_message)
- .filter(channel_message::Column::Id.eq(message_id))
- .filter(channel_message::Column::SenderId.eq(user_id))
- .exec(&*tx)
- .await?;
- if result.rows_affected == 0 {
- return Err(anyhow!(
- "Attempted to edit a message (id: {message_id}) which does not exist anymore."
- ))?;
- }
-
- // we have to fetch the old mentions,
- // so we don't send a notification when the message has been edited that you are mentioned in
- let old_mentions = channel_message_mention::Entity::find()
- .filter(channel_message_mention::Column::MessageId.eq(message_id))
- .all(&*tx)
- .await?;
-
- // remove all existing mentions
- channel_message_mention::Entity::delete_many()
- .filter(channel_message_mention::Column::MessageId.eq(message_id))
- .exec(&*tx)
- .await?;
-
- let new_mentions = self.format_mentions_to_entities(message_id, body, mentions)?;
- if !new_mentions.is_empty() {
- // insert new mentions
- channel_message_mention::Entity::insert_many(new_mentions)
- .exec(&*tx)
- .await?;
- }
-
- let mut update_mention_user_ids = HashSet::default();
- let mut new_mention_user_ids =
- mentions.iter().map(|m| m.user_id).collect::<HashSet<_>>();
- // Filter out users that were mentioned before
- for mention in &old_mentions {
- if new_mention_user_ids.contains(&mention.user_id.to_proto()) {
- update_mention_user_ids.insert(mention.user_id.to_proto());
- }
-
- new_mention_user_ids.remove(&mention.user_id.to_proto());
- }
-
- let notification_kind_id =
- self.get_notification_kind_id_by_name("ChannelMessageMention");
-
- let existing_notifications = notification::Entity::find()
- .filter(notification::Column::EntityId.eq(message_id))
- .filter(notification::Column::Kind.eq(notification_kind_id))
- .all(&*tx)
- .await?;
-
- // determine which notifications should be updated or deleted
- let mut deleted_notification_ids = HashSet::default();
- let mut updated_mention_notifications = Vec::new();
- for notification in existing_notifications {
- if update_mention_user_ids.contains(¬ification.recipient_id.to_proto()) {
- if let Some(notification) =
- self::notifications::model_to_proto(self, notification).log_err()
- {
- updated_mention_notifications.push(notification);
- }
- } else {
- deleted_notification_ids.insert(notification.id);
- }
- }
-
- let mut notifications = Vec::new();
- for mentioned_user in new_mention_user_ids {
- notifications.extend(
- self.create_notification(
- UserId::from_proto(mentioned_user),
- rpc::Notification::ChannelMessageMention {
- message_id: message_id.to_proto(),
- sender_id: user_id.to_proto(),
- channel_id: channel_id.to_proto(),
- },
- false,
- &tx,
- )
- .await?,
- );
- }
-
- Ok(UpdatedChannelMessage {
- message_id,
- participant_connection_ids,
- notifications,
- reply_to_message_id: channel_message.reply_to_message_id,
- timestamp: channel_message.sent_at,
- deleted_mention_notification_ids: deleted_notification_ids
- .into_iter()
- .collect::<Vec<_>>(),
- updated_mention_notifications,
- })
- })
- .await
- }
-}
@@ -1,69 +0,0 @@
-use super::*;
-
-#[derive(Debug)]
-pub struct CreateProcessedStripeEventParams {
- pub stripe_event_id: String,
- pub stripe_event_type: String,
- pub stripe_event_created_timestamp: i64,
-}
-
-impl Database {
- /// Creates a new processed Stripe event.
- pub async fn create_processed_stripe_event(
- &self,
- params: &CreateProcessedStripeEventParams,
- ) -> Result<()> {
- self.transaction(|tx| async move {
- processed_stripe_event::Entity::insert(processed_stripe_event::ActiveModel {
- stripe_event_id: ActiveValue::set(params.stripe_event_id.clone()),
- stripe_event_type: ActiveValue::set(params.stripe_event_type.clone()),
- stripe_event_created_timestamp: ActiveValue::set(
- params.stripe_event_created_timestamp,
- ),
- ..Default::default()
- })
- .exec_without_returning(&*tx)
- .await?;
-
- Ok(())
- })
- .await
- }
-
- /// Returns the processed Stripe event with the specified event ID.
- pub async fn get_processed_stripe_event_by_event_id(
- &self,
- event_id: &str,
- ) -> Result<Option<processed_stripe_event::Model>> {
- self.transaction(|tx| async move {
- Ok(processed_stripe_event::Entity::find_by_id(event_id)
- .one(&*tx)
- .await?)
- })
- .await
- }
-
- /// Returns the processed Stripe events with the specified event IDs.
- pub async fn get_processed_stripe_events_by_event_ids(
- &self,
- event_ids: &[&str],
- ) -> Result<Vec<processed_stripe_event::Model>> {
- self.transaction(|tx| async move {
- Ok(processed_stripe_event::Entity::find()
- .filter(
- processed_stripe_event::Column::StripeEventId.is_in(event_ids.iter().copied()),
- )
- .all(&*tx)
- .await?)
- })
- .await
- }
-
- /// Returns whether the Stripe event with the specified ID has already been processed.
- pub async fn already_processed_stripe_event(&self, event_id: &str) -> Result<bool> {
- Ok(self
- .get_processed_stripe_event_by_event_id(event_id)
- .await?
- .is_some())
- }
-}
@@ -349,11 +349,11 @@ impl Database {
serde_json::to_string(&repository.current_merge_conflicts)
.unwrap(),
)),
-
- // Old clients do not use abs path, entry ids or head_commit_details.
+ // Old clients do not use abs path, entry ids, head_commit_details, or merge_message.
abs_path: ActiveValue::set(String::new()),
entry_ids: ActiveValue::set("[]".into()),
head_commit_details: ActiveValue::set(None),
+ merge_message: ActiveValue::set(None),
}
}),
)
@@ -502,6 +502,7 @@ impl Database {
current_merge_conflicts: ActiveValue::Set(Some(
serde_json::to_string(&update.current_merge_conflicts).unwrap(),
)),
+ merge_message: ActiveValue::set(update.merge_message.clone()),
})
.on_conflict(
OnConflict::columns([
@@ -515,6 +516,7 @@ impl Database {
project_repository::Column::AbsPath,
project_repository::Column::CurrentMergeConflicts,
project_repository::Column::HeadCommitDetails,
+ project_repository::Column::MergeMessage,
])
.to_owned(),
)
@@ -692,6 +694,7 @@ impl Database {
project_id: ActiveValue::set(project_id),
id: ActiveValue::set(server.id as i64),
name: ActiveValue::set(server.name.clone()),
+ worktree_id: ActiveValue::set(server.worktree_id.map(|id| id as i64)),
capabilities: ActiveValue::set(update.capabilities.clone()),
})
.on_conflict(
@@ -702,6 +705,7 @@ impl Database {
.update_columns([
language_server::Column::Name,
language_server::Column::Capabilities,
+ language_server::Column::WorktreeId,
])
.to_owned(),
)
@@ -943,21 +947,21 @@ impl Database {
let current_merge_conflicts = db_repository_entry
.current_merge_conflicts
.as_ref()
- .map(|conflicts| serde_json::from_str(&conflicts))
+ .map(|conflicts| serde_json::from_str(conflicts))
.transpose()?
.unwrap_or_default();
let branch_summary = db_repository_entry
.branch_summary
.as_ref()
- .map(|branch_summary| serde_json::from_str(&branch_summary))
+ .map(|branch_summary| serde_json::from_str(branch_summary))
.transpose()?
.unwrap_or_default();
let head_commit_details = db_repository_entry
.head_commit_details
.as_ref()
- .map(|head_commit_details| serde_json::from_str(&head_commit_details))
+ .map(|head_commit_details| serde_json::from_str(head_commit_details))
.transpose()?
.unwrap_or_default();
@@ -990,6 +994,7 @@ impl Database {
head_commit_details,
scan_id: db_repository_entry.scan_id as u64,
is_last_update: true,
+ merge_message: db_repository_entry.merge_message,
});
}
}
@@ -1062,7 +1067,7 @@ impl Database {
server: proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
- worktree_id: None,
+ worktree_id: language_server.worktree_id.map(|id| id as u64),
},
capabilities: language_server.capabilities,
})
@@ -1318,10 +1323,10 @@ impl Database {
.await?;
let mut connection_ids = HashSet::default();
- if let Some(host_connection) = project.host_connection().log_err() {
- if !exclude_dev_server {
- connection_ids.insert(host_connection);
- }
+ if let Some(host_connection) = project.host_connection().log_err()
+ && !exclude_dev_server
+ {
+ connection_ids.insert(host_connection);
}
while let Some(collaborator) = collaborators.next().await {
@@ -746,21 +746,21 @@ impl Database {
let current_merge_conflicts = db_repository
.current_merge_conflicts
.as_ref()
- .map(|conflicts| serde_json::from_str(&conflicts))
+ .map(|conflicts| serde_json::from_str(conflicts))
.transpose()?
.unwrap_or_default();
let branch_summary = db_repository
.branch_summary
.as_ref()
- .map(|branch_summary| serde_json::from_str(&branch_summary))
+ .map(|branch_summary| serde_json::from_str(branch_summary))
.transpose()?
.unwrap_or_default();
let head_commit_details = db_repository
.head_commit_details
.as_ref()
- .map(|head_commit_details| serde_json::from_str(&head_commit_details))
+ .map(|head_commit_details| serde_json::from_str(head_commit_details))
.transpose()?
.unwrap_or_default();
@@ -793,6 +793,7 @@ impl Database {
abs_path: db_repository.abs_path,
scan_id: db_repository.scan_id as u64,
is_last_update: true,
+ merge_message: db_repository.merge_message,
});
}
}
@@ -808,7 +809,7 @@ impl Database {
server: proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
- worktree_id: None,
+ worktree_id: language_server.worktree_id.map(|id| id as u64),
},
capabilities: language_server.capabilities,
})
@@ -1192,7 +1193,6 @@ impl Database {
self.transaction(|tx| async move {
self.room_connection_lost(connection, &tx).await?;
self.channel_buffer_connection_lost(connection, &tx).await?;
- self.channel_chat_connection_lost(connection, &tx).await?;
Ok(())
})
.await
@@ -1,7 +1,4 @@
pub mod access_token;
-pub mod billing_customer;
-pub mod billing_preference;
-pub mod billing_subscription;
pub mod buffer;
pub mod buffer_operation;
pub mod buffer_snapshot;
@@ -23,7 +20,6 @@ pub mod notification;
pub mod notification_kind;
pub mod observed_buffer_edits;
pub mod observed_channel_messages;
-pub mod processed_stripe_event;
pub mod project;
pub mod project_collaborator;
pub mod project_repository;
@@ -1,41 +0,0 @@
-use crate::db::{BillingCustomerId, UserId};
-use sea_orm::entity::prelude::*;
-
-/// A billing customer.
-#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "billing_customers")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub id: BillingCustomerId,
- pub user_id: UserId,
- pub stripe_customer_id: String,
- pub has_overdue_invoices: bool,
- pub trial_started_at: Option<DateTime>,
- pub created_at: DateTime,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
- #[sea_orm(
- belongs_to = "super::user::Entity",
- from = "Column::UserId",
- to = "super::user::Column::Id"
- )]
- User,
- #[sea_orm(has_many = "super::billing_subscription::Entity")]
- BillingSubscription,
-}
-
-impl Related<super::user::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::User.def()
- }
-}
-
-impl Related<super::billing_subscription::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::BillingSubscription.def()
- }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
@@ -1,32 +0,0 @@
-use crate::db::{BillingPreferencesId, UserId};
-use sea_orm::entity::prelude::*;
-
-#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "billing_preferences")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub id: BillingPreferencesId,
- pub created_at: DateTime,
- pub user_id: UserId,
- pub max_monthly_llm_usage_spending_in_cents: i32,
- pub model_request_overages_enabled: bool,
- pub model_request_overages_spend_limit_in_cents: i32,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
- #[sea_orm(
- belongs_to = "super::user::Entity",
- from = "Column::UserId",
- to = "super::user::Column::Id"
- )]
- User,
-}
-
-impl Related<super::user::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::User.def()
- }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
@@ -1,176 +0,0 @@
-use crate::db::{BillingCustomerId, BillingSubscriptionId};
-use crate::stripe_client;
-use chrono::{Datelike as _, NaiveDate, Utc};
-use sea_orm::entity::prelude::*;
-use serde::Serialize;
-
-/// A billing subscription.
-#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
-#[sea_orm(table_name = "billing_subscriptions")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub id: BillingSubscriptionId,
- pub billing_customer_id: BillingCustomerId,
- pub kind: Option<SubscriptionKind>,
- pub stripe_subscription_id: String,
- pub stripe_subscription_status: StripeSubscriptionStatus,
- pub stripe_cancel_at: Option<DateTime>,
- pub stripe_cancellation_reason: Option<StripeCancellationReason>,
- pub stripe_current_period_start: Option<i64>,
- pub stripe_current_period_end: Option<i64>,
- pub created_at: DateTime,
-}
-
-impl Model {
- pub fn current_period_start_at(&self) -> Option<DateTimeUtc> {
- let period_start = self.stripe_current_period_start?;
- chrono::DateTime::from_timestamp(period_start, 0)
- }
-
- pub fn current_period_end_at(&self) -> Option<DateTimeUtc> {
- let period_end = self.stripe_current_period_end?;
- chrono::DateTime::from_timestamp(period_end, 0)
- }
-
- pub fn current_period(
- subscription: Option<Self>,
- is_staff: bool,
- ) -> Option<(DateTimeUtc, DateTimeUtc)> {
- if is_staff {
- let now = Utc::now();
- let year = now.year();
- let month = now.month();
-
- let first_day_of_this_month =
- NaiveDate::from_ymd_opt(year, month, 1)?.and_hms_opt(0, 0, 0)?;
-
- let next_month = if month == 12 { 1 } else { month + 1 };
- let next_month_year = if month == 12 { year + 1 } else { year };
- let first_day_of_next_month =
- NaiveDate::from_ymd_opt(next_month_year, next_month, 1)?.and_hms_opt(23, 59, 59)?;
-
- let last_day_of_this_month = first_day_of_next_month - chrono::Days::new(1);
-
- Some((
- first_day_of_this_month.and_utc(),
- last_day_of_this_month.and_utc(),
- ))
- } else {
- let subscription = subscription?;
- let period_start_at = subscription.current_period_start_at()?;
- let period_end_at = subscription.current_period_end_at()?;
-
- Some((period_start_at, period_end_at))
- }
- }
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
- #[sea_orm(
- belongs_to = "super::billing_customer::Entity",
- from = "Column::BillingCustomerId",
- to = "super::billing_customer::Column::Id"
- )]
- BillingCustomer,
-}
-
-impl Related<super::billing_customer::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::BillingCustomer.def()
- }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
-
-#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
-#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
-#[serde(rename_all = "snake_case")]
-pub enum SubscriptionKind {
- #[sea_orm(string_value = "zed_pro")]
- ZedPro,
- #[sea_orm(string_value = "zed_pro_trial")]
- ZedProTrial,
- #[sea_orm(string_value = "zed_free")]
- ZedFree,
-}
-
-impl From<SubscriptionKind> for cloud_llm_client::Plan {
- fn from(value: SubscriptionKind) -> Self {
- match value {
- SubscriptionKind::ZedPro => Self::ZedPro,
- SubscriptionKind::ZedProTrial => Self::ZedProTrial,
- SubscriptionKind::ZedFree => Self::ZedFree,
- }
- }
-}
-
-/// The status of a Stripe subscription.
-///
-/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-status)
-#[derive(
- Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash, Serialize,
-)]
-#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
-#[serde(rename_all = "snake_case")]
-pub enum StripeSubscriptionStatus {
- #[default]
- #[sea_orm(string_value = "incomplete")]
- Incomplete,
- #[sea_orm(string_value = "incomplete_expired")]
- IncompleteExpired,
- #[sea_orm(string_value = "trialing")]
- Trialing,
- #[sea_orm(string_value = "active")]
- Active,
- #[sea_orm(string_value = "past_due")]
- PastDue,
- #[sea_orm(string_value = "canceled")]
- Canceled,
- #[sea_orm(string_value = "unpaid")]
- Unpaid,
- #[sea_orm(string_value = "paused")]
- Paused,
-}
-
-impl StripeSubscriptionStatus {
- pub fn is_cancelable(&self) -> bool {
- match self {
- Self::Trialing | Self::Active | Self::PastDue => true,
- Self::Incomplete
- | Self::IncompleteExpired
- | Self::Canceled
- | Self::Unpaid
- | Self::Paused => false,
- }
- }
-}
-
-/// The cancellation reason for a Stripe subscription.
-///
-/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-cancellation_details-reason)
-#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
-#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
-#[serde(rename_all = "snake_case")]
-pub enum StripeCancellationReason {
- #[sea_orm(string_value = "cancellation_requested")]
- CancellationRequested,
- #[sea_orm(string_value = "payment_disputed")]
- PaymentDisputed,
- #[sea_orm(string_value = "payment_failed")]
- PaymentFailed,
-}
-
-impl From<stripe_client::StripeCancellationDetailsReason> for StripeCancellationReason {
- fn from(value: stripe_client::StripeCancellationDetailsReason) -> Self {
- match value {
- stripe_client::StripeCancellationDetailsReason::CancellationRequested => {
- Self::CancellationRequested
- }
- stripe_client::StripeCancellationDetailsReason::PaymentDisputed => {
- Self::PaymentDisputed
- }
- stripe_client::StripeCancellationDetailsReason::PaymentFailed => Self::PaymentFailed,
- }
- }
-}
@@ -10,6 +10,7 @@ pub struct Model {
pub id: i64,
pub name: String,
pub capabilities: String,
+ pub worktree_id: Option<i64>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -1,16 +0,0 @@
-use sea_orm::entity::prelude::*;
-
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "processed_stripe_events")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub stripe_event_id: String,
- pub stripe_event_type: String,
- pub stripe_event_created_timestamp: i64,
- pub processed_at: DateTime,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {}
-
-impl ActiveModelBehavior for ActiveModel {}
@@ -16,6 +16,8 @@ pub struct Model {
pub is_deleted: bool,
// JSON array typed string
pub current_merge_conflicts: Option<String>,
+ // The suggested merge commit message
+ pub merge_message: Option<String>,
// A JSON object representing the current Branch values
pub branch_summary: Option<String>,
// A JSON object representing the current Head commit values
@@ -29,8 +29,6 @@ pub struct Model {
pub enum Relation {
#[sea_orm(has_many = "super::access_token::Entity")]
AccessToken,
- #[sea_orm(has_one = "super::billing_customer::Entity")]
- BillingCustomer,
#[sea_orm(has_one = "super::room_participant::Entity")]
RoomParticipant,
#[sea_orm(has_many = "super::project::Entity")]
@@ -68,12 +66,6 @@ impl Related<super::access_token::Entity> for Entity {
}
}
-impl Related<super::billing_customer::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::BillingCustomer.def()
- }
-}
-
impl Related<super::room_participant::Entity> for Entity {
fn to() -> RelationDef {
Relation::RoomParticipant.def()
@@ -7,8 +7,6 @@ mod db_tests;
mod embedding_tests;
mod extension_tests;
mod feature_flag_tests;
-mod message_tests;
-mod processed_stripe_event_tests;
mod user_tests;
use crate::migrations::run_database_migrations;
@@ -22,7 +20,7 @@ use sqlx::migrate::MigrateDatabase;
use std::{
sync::{
Arc,
- atomic::{AtomicI32, AtomicU32, Ordering::SeqCst},
+ atomic::{AtomicI32, Ordering::SeqCst},
},
time::Duration,
};
@@ -76,10 +74,10 @@ impl TestDb {
static LOCK: Mutex<()> = Mutex::new(());
let _guard = LOCK.lock();
- let mut rng = StdRng::from_entropy();
+ let mut rng = StdRng::from_os_rng();
let url = format!(
"postgres://postgres@localhost/zed-test-{}",
- rng.r#gen::<u128>()
+ rng.random::<u128>()
);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_io()
@@ -225,11 +223,3 @@ async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
.unwrap()
.user_id
}
-
-static TEST_CONNECTION_ID: AtomicU32 = AtomicU32::new(1);
-fn new_test_connection(server: ServerId) -> ConnectionId {
- ConnectionId {
- id: TEST_CONNECTION_ID.fetch_add(1, SeqCst),
- owner_id: server.0 as u32,
- }
-}
@@ -1,7 +1,7 @@
use crate::{
db::{
Channel, ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
- tests::{assert_channel_tree_matches, channel_tree, new_test_connection, new_test_user},
+ tests::{assert_channel_tree_matches, channel_tree, new_test_user},
},
test_both_dbs,
};
@@ -949,41 +949,6 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
)
}
-test_both_dbs!(
- test_guest_access,
- test_guest_access_postgres,
- test_guest_access_sqlite
-);
-
-async fn test_guest_access(db: &Arc<Database>) {
- let server = db.create_server("test").await.unwrap();
-
- let admin = new_test_user(db, "admin@example.com").await;
- let guest = new_test_user(db, "guest@example.com").await;
- let guest_connection = new_test_connection(server);
-
- let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
- db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
- .await
- .unwrap();
-
- assert!(
- db.join_channel_chat(zed_channel, guest_connection, guest)
- .await
- .is_err()
- );
-
- db.join_channel(zed_channel, guest, guest_connection)
- .await
- .unwrap();
-
- assert!(
- db.join_channel_chat(zed_channel, guest_connection, guest)
- .await
- .is_ok()
- )
-}
-
#[track_caller]
fn assert_channel_tree(actual: Vec<Channel>, expected: &[(ChannelId, &[ChannelId])]) {
let actual = actual
@@ -8,7 +8,7 @@ use time::{Duration, OffsetDateTime, PrimitiveDateTime};
// SQLite does not support array arguments, so we only test this against a real postgres instance
#[gpui::test]
async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) {
- let test_db = TestDb::postgres(cx.executor().clone());
+ let test_db = TestDb::postgres(cx.executor());
let db = test_db.db();
let provider = "test_model";
@@ -38,7 +38,7 @@ async fn test_get_embeddings_postgres(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
- let test_db = TestDb::postgres(cx.executor().clone());
+ let test_db = TestDb::postgres(cx.executor());
let db = test_db.db();
let model = "test_model";
@@ -1,421 +0,0 @@
-use super::new_test_user;
-use crate::{
- db::{ChannelRole, Database, MessageId},
- test_both_dbs,
-};
-use channel::mentions_to_proto;
-use std::sync::Arc;
-use time::OffsetDateTime;
-
-test_both_dbs!(
- test_channel_message_retrieval,
- test_channel_message_retrieval_postgres,
- test_channel_message_retrieval_sqlite
-);
-
-async fn test_channel_message_retrieval(db: &Arc<Database>) {
- let user = new_test_user(db, "user@example.com").await;
- let channel = db.create_channel("channel", None, user).await.unwrap().0;
-
- let owner_id = db.create_server("test").await.unwrap().0 as u32;
- db.join_channel_chat(channel.id, rpc::ConnectionId { owner_id, id: 0 }, user)
- .await
- .unwrap();
-
- let mut all_messages = Vec::new();
- for i in 0..10 {
- all_messages.push(
- db.create_channel_message(
- channel.id,
- user,
- &i.to_string(),
- &[],
- OffsetDateTime::now_utc(),
- i,
- None,
- )
- .await
- .unwrap()
- .message_id
- .to_proto(),
- );
- }
-
- let messages = db
- .get_channel_messages(channel.id, user, 3, None)
- .await
- .unwrap()
- .into_iter()
- .map(|message| message.id)
- .collect::<Vec<_>>();
- assert_eq!(messages, &all_messages[7..10]);
-
- let messages = db
- .get_channel_messages(
- channel.id,
- user,
- 4,
- Some(MessageId::from_proto(all_messages[6])),
- )
- .await
- .unwrap()
- .into_iter()
- .map(|message| message.id)
- .collect::<Vec<_>>();
- assert_eq!(messages, &all_messages[2..6]);
-}
-
-test_both_dbs!(
- test_channel_message_nonces,
- test_channel_message_nonces_postgres,
- test_channel_message_nonces_sqlite
-);
-
-async fn test_channel_message_nonces(db: &Arc<Database>) {
- let user_a = new_test_user(db, "user_a@example.com").await;
- let user_b = new_test_user(db, "user_b@example.com").await;
- let user_c = new_test_user(db, "user_c@example.com").await;
- let channel = db.create_root_channel("channel", user_a).await.unwrap();
- db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
- .await
- .unwrap();
- db.invite_channel_member(channel, user_c, user_a, ChannelRole::Member)
- .await
- .unwrap();
- db.respond_to_channel_invite(channel, user_b, true)
- .await
- .unwrap();
- db.respond_to_channel_invite(channel, user_c, true)
- .await
- .unwrap();
-
- let owner_id = db.create_server("test").await.unwrap().0 as u32;
- db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user_a)
- .await
- .unwrap();
- db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 1 }, user_b)
- .await
- .unwrap();
-
- // As user A, create messages that reuse the same nonces. The requests
- // succeed, but return the same ids.
- let id1 = db
- .create_channel_message(
- channel,
- user_a,
- "hi @user_b",
- &mentions_to_proto(&[(3..10, user_b.to_proto())]),
- OffsetDateTime::now_utc(),
- 100,
- None,
- )
- .await
- .unwrap()
- .message_id;
- let id2 = db
- .create_channel_message(
- channel,
- user_a,
- "hello, fellow users",
- &mentions_to_proto(&[]),
- OffsetDateTime::now_utc(),
- 200,
- None,
- )
- .await
- .unwrap()
- .message_id;
- let id3 = db
- .create_channel_message(
- channel,
- user_a,
- "bye @user_c (same nonce as first message)",
- &mentions_to_proto(&[(4..11, user_c.to_proto())]),
- OffsetDateTime::now_utc(),
- 100,
- None,
- )
- .await
- .unwrap()
- .message_id;
- let id4 = db
- .create_channel_message(
- channel,
- user_a,
- "omg (same nonce as second message)",
- &mentions_to_proto(&[]),
- OffsetDateTime::now_utc(),
- 200,
- None,
- )
- .await
- .unwrap()
- .message_id;
-
- // As a different user, reuse one of the same nonces. This request succeeds
- // and returns a different id.
- let id5 = db
- .create_channel_message(
- channel,
- user_b,
- "omg @user_a (same nonce as user_a's first message)",
- &mentions_to_proto(&[(4..11, user_a.to_proto())]),
- OffsetDateTime::now_utc(),
- 100,
- None,
- )
- .await
- .unwrap()
- .message_id;
-
- assert_ne!(id1, id2);
- assert_eq!(id1, id3);
- assert_eq!(id2, id4);
- assert_ne!(id5, id1);
-
- let messages = db
- .get_channel_messages(channel, user_a, 5, None)
- .await
- .unwrap()
- .into_iter()
- .map(|m| (m.id, m.body, m.mentions))
- .collect::<Vec<_>>();
- assert_eq!(
- messages,
- &[
- (
- id1.to_proto(),
- "hi @user_b".into(),
- mentions_to_proto(&[(3..10, user_b.to_proto())]),
- ),
- (
- id2.to_proto(),
- "hello, fellow users".into(),
- mentions_to_proto(&[])
- ),
- (
- id5.to_proto(),
- "omg @user_a (same nonce as user_a's first message)".into(),
- mentions_to_proto(&[(4..11, user_a.to_proto())]),
- ),
- ]
- );
-}
-
-test_both_dbs!(
- test_unseen_channel_messages,
- test_unseen_channel_messages_postgres,
- test_unseen_channel_messages_sqlite
-);
-
-async fn test_unseen_channel_messages(db: &Arc<Database>) {
- let user = new_test_user(db, "user_a@example.com").await;
- let observer = new_test_user(db, "user_b@example.com").await;
-
- let channel_1 = db.create_root_channel("channel", user).await.unwrap();
- let channel_2 = db.create_root_channel("channel-2", user).await.unwrap();
-
- db.invite_channel_member(channel_1, observer, user, ChannelRole::Member)
- .await
- .unwrap();
- db.invite_channel_member(channel_2, observer, user, ChannelRole::Member)
- .await
- .unwrap();
-
- db.respond_to_channel_invite(channel_1, observer, true)
- .await
- .unwrap();
- db.respond_to_channel_invite(channel_2, observer, true)
- .await
- .unwrap();
-
- let owner_id = db.create_server("test").await.unwrap().0 as u32;
- let user_connection_id = rpc::ConnectionId { owner_id, id: 0 };
-
- db.join_channel_chat(channel_1, user_connection_id, user)
- .await
- .unwrap();
-
- let _ = db
- .create_channel_message(
- channel_1,
- user,
- "1_1",
- &[],
- OffsetDateTime::now_utc(),
- 1,
- None,
- )
- .await
- .unwrap();
-
- let _ = db
- .create_channel_message(
- channel_1,
- user,
- "1_2",
- &[],
- OffsetDateTime::now_utc(),
- 2,
- None,
- )
- .await
- .unwrap();
-
- let third_message = db
- .create_channel_message(
- channel_1,
- user,
- "1_3",
- &[],
- OffsetDateTime::now_utc(),
- 3,
- None,
- )
- .await
- .unwrap()
- .message_id;
-
- db.join_channel_chat(channel_2, user_connection_id, user)
- .await
- .unwrap();
-
- let fourth_message = db
- .create_channel_message(
- channel_2,
- user,
- "2_1",
- &[],
- OffsetDateTime::now_utc(),
- 4,
- None,
- )
- .await
- .unwrap()
- .message_id;
-
- // Check that observer has new messages
- let latest_messages = db
- .transaction(|tx| async move {
- db.latest_channel_messages(&[channel_1, channel_2], &tx)
- .await
- })
- .await
- .unwrap();
-
- assert_eq!(
- latest_messages,
- [
- rpc::proto::ChannelMessageId {
- channel_id: channel_1.to_proto(),
- message_id: third_message.to_proto(),
- },
- rpc::proto::ChannelMessageId {
- channel_id: channel_2.to_proto(),
- message_id: fourth_message.to_proto(),
- },
- ]
- );
-}
-
-test_both_dbs!(
- test_channel_message_mentions,
- test_channel_message_mentions_postgres,
- test_channel_message_mentions_sqlite
-);
-
-async fn test_channel_message_mentions(db: &Arc<Database>) {
- let user_a = new_test_user(db, "user_a@example.com").await;
- let user_b = new_test_user(db, "user_b@example.com").await;
- let user_c = new_test_user(db, "user_c@example.com").await;
-
- let channel = db
- .create_channel("channel", None, user_a)
- .await
- .unwrap()
- .0
- .id;
- db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
- .await
- .unwrap();
- db.respond_to_channel_invite(channel, user_b, true)
- .await
- .unwrap();
-
- let owner_id = db.create_server("test").await.unwrap().0 as u32;
- let connection_id = rpc::ConnectionId { owner_id, id: 0 };
- db.join_channel_chat(channel, connection_id, user_a)
- .await
- .unwrap();
-
- db.create_channel_message(
- channel,
- user_a,
- "hi @user_b and @user_c",
- &mentions_to_proto(&[(3..10, user_b.to_proto()), (15..22, user_c.to_proto())]),
- OffsetDateTime::now_utc(),
- 1,
- None,
- )
- .await
- .unwrap();
- db.create_channel_message(
- channel,
- user_a,
- "bye @user_c",
- &mentions_to_proto(&[(4..11, user_c.to_proto())]),
- OffsetDateTime::now_utc(),
- 2,
- None,
- )
- .await
- .unwrap();
- db.create_channel_message(
- channel,
- user_a,
- "umm",
- &mentions_to_proto(&[]),
- OffsetDateTime::now_utc(),
- 3,
- None,
- )
- .await
- .unwrap();
- db.create_channel_message(
- channel,
- user_a,
- "@user_b, stop.",
- &mentions_to_proto(&[(0..7, user_b.to_proto())]),
- OffsetDateTime::now_utc(),
- 4,
- None,
- )
- .await
- .unwrap();
-
- let messages = db
- .get_channel_messages(channel, user_b, 5, None)
- .await
- .unwrap()
- .into_iter()
- .map(|m| (m.body, m.mentions))
- .collect::<Vec<_>>();
- assert_eq!(
- &messages,
- &[
- (
- "hi @user_b and @user_c".into(),
- mentions_to_proto(&[(3..10, user_b.to_proto()), (15..22, user_c.to_proto())]),
- ),
- (
- "bye @user_c".into(),
- mentions_to_proto(&[(4..11, user_c.to_proto())]),
- ),
- ("umm".into(), mentions_to_proto(&[]),),
- (
- "@user_b, stop.".into(),
- mentions_to_proto(&[(0..7, user_b.to_proto())]),
- ),
- ]
- );
-}
@@ -1,38 +0,0 @@
-use std::sync::Arc;
-
-use crate::test_both_dbs;
-
-use super::{CreateProcessedStripeEventParams, Database};
-
-test_both_dbs!(
- test_already_processed_stripe_event,
- test_already_processed_stripe_event_postgres,
- test_already_processed_stripe_event_sqlite
-);
-
-async fn test_already_processed_stripe_event(db: &Arc<Database>) {
- let unprocessed_event_id = "evt_1PiJOuRxOf7d5PNaw2zzWiyO".to_string();
- let processed_event_id = "evt_1PiIfMRxOf7d5PNakHrAUe8P".to_string();
-
- db.create_processed_stripe_event(&CreateProcessedStripeEventParams {
- stripe_event_id: processed_event_id.clone(),
- stripe_event_type: "customer.created".into(),
- stripe_event_created_timestamp: 1722355968,
- })
- .await
- .unwrap();
-
- assert!(
- db.already_processed_stripe_event(&processed_event_id)
- .await
- .unwrap(),
- "Expected {processed_event_id} to already be processed"
- );
-
- assert!(
- !db.already_processed_stripe_event(&unprocessed_event_id)
- .await
- .unwrap(),
- "Expected {unprocessed_event_id} to be unprocessed"
- );
-}
@@ -7,8 +7,6 @@ pub mod llm;
pub mod migrations;
pub mod rpc;
pub mod seed;
-pub mod stripe_billing;
-pub mod stripe_client;
pub mod user_backfiller;
#[cfg(test)]
@@ -22,21 +20,16 @@ use axum::{
};
use db::{ChannelId, Database};
use executor::Executor;
-use llm::db::LlmDatabase;
use serde::Deserialize;
use std::{path::PathBuf, sync::Arc};
use util::ResultExt;
-use crate::stripe_billing::StripeBilling;
-use crate::stripe_client::{RealStripeClient, StripeClient};
-
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub enum Error {
Http(StatusCode, String, HeaderMap),
Database(sea_orm::error::DbErr),
Internal(anyhow::Error),
- Stripe(stripe::StripeError),
}
impl From<anyhow::Error> for Error {
@@ -51,12 +44,6 @@ impl From<sea_orm::error::DbErr> for Error {
}
}
-impl From<stripe::StripeError> for Error {
- fn from(error: stripe::StripeError) -> Self {
- Self::Stripe(error)
- }
-}
-
impl From<axum::Error> for Error {
fn from(error: axum::Error) -> Self {
Self::Internal(error.into())
@@ -104,14 +91,6 @@ impl IntoResponse for Error {
);
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
}
- Error::Stripe(error) => {
- log::error!(
- "HTTP error {}: {:?}",
- StatusCode::INTERNAL_SERVER_ERROR,
- &error
- );
- (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", &error)).into_response()
- }
}
}
}
@@ -122,7 +101,6 @@ impl std::fmt::Debug for Error {
Error::Http(code, message, _headers) => (code, message).fmt(f),
Error::Database(error) => error.fmt(f),
Error::Internal(error) => error.fmt(f),
- Error::Stripe(error) => error.fmt(f),
}
}
}
@@ -133,7 +111,6 @@ impl std::fmt::Display for Error {
Error::Http(code, message, _) => write!(f, "{code}: {message}"),
Error::Database(error) => error.fmt(f),
Error::Internal(error) => error.fmt(f),
- Error::Stripe(error) => error.fmt(f),
}
}
}
@@ -179,7 +156,6 @@ pub struct Config {
pub zed_client_checksum_seed: Option<String>,
pub slack_panics_webhook: Option<String>,
pub auto_join_channel_id: Option<ChannelId>,
- pub stripe_api_key: Option<String>,
pub supermaven_admin_api_key: Option<Arc<str>>,
pub user_backfiller_github_access_token: Option<Arc<str>>,
}
@@ -234,7 +210,6 @@ impl Config {
auto_join_channel_id: None,
migrations_path: None,
seed_path: None,
- stripe_api_key: None,
supermaven_admin_api_key: None,
user_backfiller_github_access_token: None,
kinesis_region: None,
@@ -266,14 +241,8 @@ impl ServiceMode {
pub struct AppState {
pub db: Arc<Database>,
- pub llm_db: Option<Arc<LlmDatabase>>,
pub livekit_client: Option<Arc<dyn livekit_api::Client>>,
pub blob_store_client: Option<aws_sdk_s3::Client>,
- /// This is a real instance of the Stripe client; we're working to replace references to this with the
- /// [`StripeClient`] trait.
- pub real_stripe_client: Option<Arc<stripe::Client>>,
- pub stripe_client: Option<Arc<dyn StripeClient>>,
- pub stripe_billing: Option<Arc<StripeBilling>>,
pub executor: Executor,
pub kinesis_client: Option<::aws_sdk_kinesis::Client>,
pub config: Config,
@@ -286,20 +255,6 @@ impl AppState {
let mut db = Database::new(db_options).await?;
db.initialize_notification_kinds().await?;
- let llm_db = if let Some((llm_database_url, llm_database_max_connections)) = config
- .llm_database_url
- .clone()
- .zip(config.llm_database_max_connections)
- {
- let mut llm_db_options = db::ConnectOptions::new(llm_database_url);
- llm_db_options.max_connections(llm_database_max_connections);
- let mut llm_db = LlmDatabase::new(llm_db_options, executor.clone()).await?;
- llm_db.initialize().await?;
- Some(Arc::new(llm_db))
- } else {
- None
- };
-
let livekit_client = if let Some(((server, key), secret)) = config
.livekit_server
.as_ref()
@@ -316,18 +271,10 @@ impl AppState {
};
let db = Arc::new(db);
- let stripe_client = build_stripe_client(&config).map(Arc::new).log_err();
let this = Self {
db: db.clone(),
- llm_db,
livekit_client,
blob_store_client: build_blob_store_client(&config).await.log_err(),
- stripe_billing: stripe_client
- .clone()
- .map(|stripe_client| Arc::new(StripeBilling::new(stripe_client))),
- real_stripe_client: stripe_client.clone(),
- stripe_client: stripe_client
- .map(|stripe_client| Arc::new(RealStripeClient::new(stripe_client)) as _),
executor,
kinesis_client: if config.kinesis_access_key.is_some() {
build_kinesis_client(&config).await.log_err()
@@ -340,14 +287,6 @@ impl AppState {
}
}
-fn build_stripe_client(config: &Config) -> anyhow::Result<stripe::Client> {
- let api_key = config
- .stripe_api_key
- .as_ref()
- .context("missing stripe_api_key")?;
- Ok(stripe::Client::new(api_key))
-}
-
async fn build_blob_store_client(config: &Config) -> anyhow::Result<aws_sdk_s3::Client> {
let keys = aws_sdk_s3::config::Credentials::new(
config
@@ -1,12 +1 @@
pub mod db;
-mod token;
-
-pub use token::*;
-
-pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
-
-/// The name of the feature flag that bypasses the account age check.
-pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-check";
-
-/// The minimum account age an account must have in order to use the LLM service.
-pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
@@ -1,30 +1,9 @@
-mod ids;
-mod queries;
-mod seed;
-mod tables;
-
-#[cfg(test)]
-mod tests;
-
-use cloud_llm_client::LanguageModelProvider;
-use collections::HashMap;
-pub use ids::*;
-pub use seed::*;
-pub use tables::*;
-
-#[cfg(test)]
-pub use tests::TestLlmDb;
-use usage_measure::UsageMeasure;
-
use std::future::Future;
use std::sync::Arc;
use anyhow::Context;
pub use sea_orm::ConnectOptions;
-use sea_orm::prelude::*;
-use sea_orm::{
- ActiveValue, DatabaseConnection, DatabaseTransaction, IsolationLevel, TransactionTrait,
-};
+use sea_orm::{DatabaseConnection, DatabaseTransaction, IsolationLevel, TransactionTrait};
use crate::Result;
use crate::db::TransactionHandle;
@@ -36,9 +15,6 @@ pub struct LlmDatabase {
pool: DatabaseConnection,
#[allow(unused)]
executor: Executor,
- provider_ids: HashMap<LanguageModelProvider, ProviderId>,
- models: HashMap<(LanguageModelProvider, String), model::Model>,
- usage_measure_ids: HashMap<UsageMeasure, UsageMeasureId>,
#[cfg(test)]
runtime: Option<tokio::runtime::Runtime>,
}
@@ -51,59 +27,11 @@ impl LlmDatabase {
options: options.clone(),
pool: sea_orm::Database::connect(options).await?,
executor,
- provider_ids: HashMap::default(),
- models: HashMap::default(),
- usage_measure_ids: HashMap::default(),
#[cfg(test)]
runtime: None,
})
}
- pub async fn initialize(&mut self) -> Result<()> {
- self.initialize_providers().await?;
- self.initialize_models().await?;
- self.initialize_usage_measures().await?;
- Ok(())
- }
-
- /// Returns the list of all known models, with their [`LanguageModelProvider`].
- pub fn all_models(&self) -> Vec<(LanguageModelProvider, model::Model)> {
- self.models
- .iter()
- .map(|((model_provider, _model_name), model)| (*model_provider, model.clone()))
- .collect::<Vec<_>>()
- }
-
- /// Returns the names of the known models for the given [`LanguageModelProvider`].
- pub fn model_names_for_provider(&self, provider: LanguageModelProvider) -> Vec<String> {
- self.models
- .keys()
- .filter_map(|(model_provider, model_name)| {
- if model_provider == &provider {
- Some(model_name)
- } else {
- None
- }
- })
- .cloned()
- .collect::<Vec<_>>()
- }
-
- pub fn model(&self, provider: LanguageModelProvider, name: &str) -> Result<&model::Model> {
- Ok(self
- .models
- .get(&(provider, name.to_string()))
- .with_context(|| format!("unknown model {provider:?}:{name}"))?)
- }
-
- pub fn model_by_id(&self, id: ModelId) -> Result<&model::Model> {
- Ok(self
- .models
- .values()
- .find(|model| model.id == id)
- .with_context(|| format!("no model for ID {id:?}"))?)
- }
-
pub fn options(&self) -> &ConnectOptions {
&self.options
}
@@ -1,11 +0,0 @@
-use sea_orm::{DbErr, entity::prelude::*};
-use serde::{Deserialize, Serialize};
-
-use crate::id_type;
-
-id_type!(BillingEventId);
-id_type!(ModelId);
-id_type!(ProviderId);
-id_type!(RevokedAccessTokenId);
-id_type!(UsageId);
-id_type!(UsageMeasureId);
@@ -1,5 +0,0 @@
-use super::*;
-
-pub mod providers;
-pub mod subscription_usages;
-pub mod usages;
@@ -1,134 +0,0 @@
-use super::*;
-use sea_orm::{QueryOrder, sea_query::OnConflict};
-use std::str::FromStr;
-use strum::IntoEnumIterator as _;
-
-pub struct ModelParams {
- pub provider: LanguageModelProvider,
- pub name: String,
- pub max_requests_per_minute: i64,
- pub max_tokens_per_minute: i64,
- pub max_tokens_per_day: i64,
- pub price_per_million_input_tokens: i32,
- pub price_per_million_output_tokens: i32,
-}
-
-impl LlmDatabase {
- pub async fn initialize_providers(&mut self) -> Result<()> {
- self.provider_ids = self
- .transaction(|tx| async move {
- let existing_providers = provider::Entity::find().all(&*tx).await?;
-
- let mut new_providers = LanguageModelProvider::iter()
- .filter(|provider| {
- !existing_providers
- .iter()
- .any(|p| p.name == provider.to_string())
- })
- .map(|provider| provider::ActiveModel {
- name: ActiveValue::set(provider.to_string()),
- ..Default::default()
- })
- .peekable();
-
- if new_providers.peek().is_some() {
- provider::Entity::insert_many(new_providers)
- .exec(&*tx)
- .await?;
- }
-
- let all_providers: HashMap<_, _> = provider::Entity::find()
- .all(&*tx)
- .await?
- .iter()
- .filter_map(|provider| {
- LanguageModelProvider::from_str(&provider.name)
- .ok()
- .map(|p| (p, provider.id))
- })
- .collect();
-
- Ok(all_providers)
- })
- .await?;
- Ok(())
- }
-
- pub async fn initialize_models(&mut self) -> Result<()> {
- let all_provider_ids = &self.provider_ids;
- self.models = self
- .transaction(|tx| async move {
- let all_models: HashMap<_, _> = model::Entity::find()
- .all(&*tx)
- .await?
- .into_iter()
- .filter_map(|model| {
- let provider = all_provider_ids.iter().find_map(|(provider, id)| {
- if *id == model.provider_id {
- Some(provider)
- } else {
- None
- }
- })?;
- Some(((*provider, model.name.clone()), model))
- })
- .collect();
- Ok(all_models)
- })
- .await?;
- Ok(())
- }
-
- pub async fn insert_models(&mut self, models: &[ModelParams]) -> Result<()> {
- let all_provider_ids = &self.provider_ids;
- self.transaction(|tx| async move {
- model::Entity::insert_many(models.iter().map(|model_params| {
- let provider_id = all_provider_ids[&model_params.provider];
- model::ActiveModel {
- provider_id: ActiveValue::set(provider_id),
- name: ActiveValue::set(model_params.name.clone()),
- max_requests_per_minute: ActiveValue::set(model_params.max_requests_per_minute),
- max_tokens_per_minute: ActiveValue::set(model_params.max_tokens_per_minute),
- max_tokens_per_day: ActiveValue::set(model_params.max_tokens_per_day),
- price_per_million_input_tokens: ActiveValue::set(
- model_params.price_per_million_input_tokens,
- ),
- price_per_million_output_tokens: ActiveValue::set(
- model_params.price_per_million_output_tokens,
- ),
- ..Default::default()
- }
- }))
- .on_conflict(
- OnConflict::columns([model::Column::ProviderId, model::Column::Name])
- .update_columns([
- model::Column::MaxRequestsPerMinute,
- model::Column::MaxTokensPerMinute,
- model::Column::MaxTokensPerDay,
- model::Column::PricePerMillionInputTokens,
- model::Column::PricePerMillionOutputTokens,
- ])
- .to_owned(),
- )
- .exec_without_returning(&*tx)
- .await?;
- Ok(())
- })
- .await?;
- self.initialize_models().await
- }
-
- /// Returns the list of LLM providers.
- pub async fn list_providers(&self) -> Result<Vec<LanguageModelProvider>> {
- self.transaction(|tx| async move {
- Ok(provider::Entity::find()
- .order_by_asc(provider::Column::Name)
- .all(&*tx)
- .await?
- .into_iter()
- .filter_map(|p| LanguageModelProvider::from_str(&p.name).ok())
- .collect())
- })
- .await
- }
-}
@@ -1,38 +0,0 @@
-use crate::db::UserId;
-
-use super::*;
-
-impl LlmDatabase {
- pub async fn get_subscription_usage_for_period(
- &self,
- user_id: UserId,
- period_start_at: DateTimeUtc,
- period_end_at: DateTimeUtc,
- ) -> Result<Option<subscription_usage::Model>> {
- self.transaction(|tx| async move {
- self.get_subscription_usage_for_period_in_tx(
- user_id,
- period_start_at,
- period_end_at,
- &tx,
- )
- .await
- })
- .await
- }
-
- async fn get_subscription_usage_for_period_in_tx(
- &self,
- user_id: UserId,
- period_start_at: DateTimeUtc,
- period_end_at: DateTimeUtc,
- tx: &DatabaseTransaction,
- ) -> Result<Option<subscription_usage::Model>> {
- Ok(subscription_usage::Entity::find()
- .filter(subscription_usage::Column::UserId.eq(user_id))
- .filter(subscription_usage::Column::PeriodStartAt.eq(period_start_at))
- .filter(subscription_usage::Column::PeriodEndAt.eq(period_end_at))
- .one(tx)
- .await?)
- }
-}
@@ -1,44 +0,0 @@
-use std::str::FromStr;
-use strum::IntoEnumIterator as _;
-
-use super::*;
-
-impl LlmDatabase {
- pub async fn initialize_usage_measures(&mut self) -> Result<()> {
- let all_measures = self
- .transaction(|tx| async move {
- let existing_measures = usage_measure::Entity::find().all(&*tx).await?;
-
- let new_measures = UsageMeasure::iter()
- .filter(|measure| {
- !existing_measures
- .iter()
- .any(|m| m.name == measure.to_string())
- })
- .map(|measure| usage_measure::ActiveModel {
- name: ActiveValue::set(measure.to_string()),
- ..Default::default()
- })
- .collect::<Vec<_>>();
-
- if !new_measures.is_empty() {
- usage_measure::Entity::insert_many(new_measures)
- .exec(&*tx)
- .await?;
- }
-
- Ok(usage_measure::Entity::find().all(&*tx).await?)
- })
- .await?;
-
- self.usage_measure_ids = all_measures
- .into_iter()
- .filter_map(|measure| {
- UsageMeasure::from_str(&measure.name)
- .ok()
- .map(|um| (um, measure.id))
- })
- .collect();
- Ok(())
- }
-}
@@ -1,45 +0,0 @@
-use super::*;
-use crate::{Config, Result};
-use queries::providers::ModelParams;
-
-pub async fn seed_database(_config: &Config, db: &mut LlmDatabase, _force: bool) -> Result<()> {
- db.insert_models(&[
- ModelParams {
- provider: LanguageModelProvider::Anthropic,
- name: "claude-3-5-sonnet".into(),
- max_requests_per_minute: 5,
- max_tokens_per_minute: 20_000,
- max_tokens_per_day: 300_000,
- price_per_million_input_tokens: 300, // $3.00/MTok
- price_per_million_output_tokens: 1500, // $15.00/MTok
- },
- ModelParams {
- provider: LanguageModelProvider::Anthropic,
- name: "claude-3-opus".into(),
- max_requests_per_minute: 5,
- max_tokens_per_minute: 10_000,
- max_tokens_per_day: 300_000,
- price_per_million_input_tokens: 1500, // $15.00/MTok
- price_per_million_output_tokens: 7500, // $75.00/MTok
- },
- ModelParams {
- provider: LanguageModelProvider::Anthropic,
- name: "claude-3-sonnet".into(),
- max_requests_per_minute: 5,
- max_tokens_per_minute: 20_000,
- max_tokens_per_day: 300_000,
- price_per_million_input_tokens: 1500, // $15.00/MTok
- price_per_million_output_tokens: 7500, // $75.00/MTok
- },
- ModelParams {
- provider: LanguageModelProvider::Anthropic,
- name: "claude-3-haiku".into(),
- max_requests_per_minute: 5,
- max_tokens_per_minute: 25_000,
- max_tokens_per_day: 300_000,
- price_per_million_input_tokens: 25, // $0.25/MTok
- price_per_million_output_tokens: 125, // $1.25/MTok
- },
- ])
- .await
-}
@@ -1,6 +0,0 @@
-pub mod model;
-pub mod provider;
-pub mod subscription_usage;
-pub mod subscription_usage_meter;
-pub mod usage;
-pub mod usage_measure;
@@ -1,48 +0,0 @@
-use sea_orm::entity::prelude::*;
-
-use crate::llm::db::{ModelId, ProviderId};
-
-/// An LLM model.
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "models")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub id: ModelId,
- pub provider_id: ProviderId,
- pub name: String,
- pub max_requests_per_minute: i64,
- pub max_tokens_per_minute: i64,
- pub max_input_tokens_per_minute: i64,
- pub max_output_tokens_per_minute: i64,
- pub max_tokens_per_day: i64,
- pub price_per_million_input_tokens: i32,
- pub price_per_million_cache_creation_input_tokens: i32,
- pub price_per_million_cache_read_input_tokens: i32,
- pub price_per_million_output_tokens: i32,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
- #[sea_orm(
- belongs_to = "super::provider::Entity",
- from = "Column::ProviderId",
- to = "super::provider::Column::Id"
- )]
- Provider,
- #[sea_orm(has_many = "super::usage::Entity")]
- Usages,
-}
-
-impl Related<super::provider::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::Provider.def()
- }
-}
-
-impl Related<super::usage::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::Usages.def()
- }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
@@ -1,25 +0,0 @@
-use crate::llm::db::ProviderId;
-use sea_orm::entity::prelude::*;
-
-/// An LLM provider.
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "providers")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub id: ProviderId,
- pub name: String,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
- #[sea_orm(has_many = "super::model::Entity")]
- Models,
-}
-
-impl Related<super::model::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::Models.def()
- }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
@@ -1,22 +0,0 @@
-use crate::db::UserId;
-use crate::db::billing_subscription::SubscriptionKind;
-use sea_orm::entity::prelude::*;
-use time::PrimitiveDateTime;
-
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "subscription_usages_v2")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub id: Uuid,
- pub user_id: UserId,
- pub period_start_at: PrimitiveDateTime,
- pub period_end_at: PrimitiveDateTime,
- pub plan: SubscriptionKind,
- pub model_requests: i32,
- pub edit_predictions: i32,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {}
-
-impl ActiveModelBehavior for ActiveModel {}
@@ -1,55 +0,0 @@
-use sea_orm::entity::prelude::*;
-use serde::Serialize;
-
-use crate::llm::db::ModelId;
-
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "subscription_usage_meters_v2")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub id: Uuid,
- pub subscription_usage_id: Uuid,
- pub model_id: ModelId,
- pub mode: CompletionMode,
- pub requests: i32,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
- #[sea_orm(
- belongs_to = "super::subscription_usage::Entity",
- from = "Column::SubscriptionUsageId",
- to = "super::subscription_usage::Column::Id"
- )]
- SubscriptionUsage,
- #[sea_orm(
- belongs_to = "super::model::Entity",
- from = "Column::ModelId",
- to = "super::model::Column::Id"
- )]
- Model,
-}
-
-impl Related<super::subscription_usage::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::SubscriptionUsage.def()
- }
-}
-
-impl Related<super::model::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::Model.def()
- }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
-
-#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Hash, Serialize)]
-#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
-#[serde(rename_all = "snake_case")]
-pub enum CompletionMode {
- #[sea_orm(string_value = "normal")]
- Normal,
- #[sea_orm(string_value = "max")]
- Max,
-}
@@ -1,52 +0,0 @@
-use crate::{
- db::UserId,
- llm::db::{ModelId, UsageId, UsageMeasureId},
-};
-use sea_orm::entity::prelude::*;
-
-/// An LLM usage record.
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "usages")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub id: UsageId,
- /// The ID of the Zed user.
- ///
- /// Corresponds to the `users` table in the primary collab database.
- pub user_id: UserId,
- pub model_id: ModelId,
- pub measure_id: UsageMeasureId,
- pub timestamp: DateTime,
- pub buckets: Vec<i64>,
- pub is_staff: bool,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
- #[sea_orm(
- belongs_to = "super::model::Entity",
- from = "Column::ModelId",
- to = "super::model::Column::Id"
- )]
- Model,
- #[sea_orm(
- belongs_to = "super::usage_measure::Entity",
- from = "Column::MeasureId",
- to = "super::usage_measure::Column::Id"
- )]
- UsageMeasure,
-}
-
-impl Related<super::model::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::Model.def()
- }
-}
-
-impl Related<super::usage_measure::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::UsageMeasure.def()
- }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
@@ -1,36 +0,0 @@
-use crate::llm::db::UsageMeasureId;
-use sea_orm::entity::prelude::*;
-
-#[derive(
- Copy, Clone, Debug, PartialEq, Eq, Hash, strum::EnumString, strum::Display, strum::EnumIter,
-)]
-#[strum(serialize_all = "snake_case")]
-pub enum UsageMeasure {
- RequestsPerMinute,
- TokensPerMinute,
- InputTokensPerMinute,
- OutputTokensPerMinute,
- TokensPerDay,
-}
-
-#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
-#[sea_orm(table_name = "usage_measures")]
-pub struct Model {
- #[sea_orm(primary_key)]
- pub id: UsageMeasureId,
- pub name: String,
-}
-
-#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
-pub enum Relation {
- #[sea_orm(has_many = "super::usage::Entity")]
- Usages,
-}
-
-impl Related<super::usage::Entity> for Entity {
- fn to() -> RelationDef {
- Relation::Usages.def()
- }
-}
-
-impl ActiveModelBehavior for ActiveModel {}
@@ -1,107 +0,0 @@
-mod provider_tests;
-
-use gpui::BackgroundExecutor;
-use parking_lot::Mutex;
-use rand::prelude::*;
-use sea_orm::ConnectionTrait;
-use sqlx::migrate::MigrateDatabase;
-use std::time::Duration;
-
-use crate::migrations::run_database_migrations;
-
-use super::*;
-
-pub struct TestLlmDb {
- pub db: Option<LlmDatabase>,
- pub connection: Option<sqlx::AnyConnection>,
-}
-
-impl TestLlmDb {
- pub fn postgres(background: BackgroundExecutor) -> Self {
- static LOCK: Mutex<()> = Mutex::new(());
-
- let _guard = LOCK.lock();
- let mut rng = StdRng::from_entropy();
- let url = format!(
- "postgres://postgres@localhost/zed-llm-test-{}",
- rng.r#gen::<u128>()
- );
- let runtime = tokio::runtime::Builder::new_current_thread()
- .enable_io()
- .enable_time()
- .build()
- .unwrap();
-
- let mut db = runtime.block_on(async {
- sqlx::Postgres::create_database(&url)
- .await
- .expect("failed to create test db");
- let mut options = ConnectOptions::new(url);
- options
- .max_connections(5)
- .idle_timeout(Duration::from_secs(0));
- let db = LlmDatabase::new(options, Executor::Deterministic(background))
- .await
- .unwrap();
- let migrations_path = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm");
- run_database_migrations(db.options(), migrations_path)
- .await
- .unwrap();
- db
- });
-
- db.runtime = Some(runtime);
-
- Self {
- db: Some(db),
- connection: None,
- }
- }
-
- pub fn db(&mut self) -> &mut LlmDatabase {
- self.db.as_mut().unwrap()
- }
-}
-
-#[macro_export]
-macro_rules! test_llm_db {
- ($test_name:ident, $postgres_test_name:ident) => {
- #[gpui::test]
- async fn $postgres_test_name(cx: &mut gpui::TestAppContext) {
- if !cfg!(target_os = "macos") {
- return;
- }
-
- let mut test_db = $crate::llm::db::TestLlmDb::postgres(cx.executor().clone());
- $test_name(test_db.db()).await;
- }
- };
-}
-
-impl Drop for TestLlmDb {
- fn drop(&mut self) {
- let db = self.db.take().unwrap();
- if let sea_orm::DatabaseBackend::Postgres = db.pool.get_database_backend() {
- db.runtime.as_ref().unwrap().block_on(async {
- use util::ResultExt;
- let query = "
- SELECT pg_terminate_backend(pg_stat_activity.pid)
- FROM pg_stat_activity
- WHERE
- pg_stat_activity.datname = current_database() AND
- pid <> pg_backend_pid();
- ";
- db.pool
- .execute(sea_orm::Statement::from_string(
- db.pool.get_database_backend(),
- query,
- ))
- .await
- .log_err();
- sqlx::Postgres::drop_database(db.options.get_url())
- .await
- .log_err();
- })
- }
- }
-}
@@ -1,31 +0,0 @@
-use cloud_llm_client::LanguageModelProvider;
-use pretty_assertions::assert_eq;
-
-use crate::llm::db::LlmDatabase;
-use crate::test_llm_db;
-
-test_llm_db!(
- test_initialize_providers,
- test_initialize_providers_postgres
-);
-
-async fn test_initialize_providers(db: &mut LlmDatabase) {
- let initial_providers = db.list_providers().await.unwrap();
- assert_eq!(initial_providers, vec![]);
-
- db.initialize_providers().await.unwrap();
-
- // Do it twice, to make sure the operation is idempotent.
- db.initialize_providers().await.unwrap();
-
- let providers = db.list_providers().await.unwrap();
-
- assert_eq!(
- providers,
- &[
- LanguageModelProvider::Anthropic,
- LanguageModelProvider::Google,
- LanguageModelProvider::OpenAi,
- ]
- )
-}
@@ -1,146 +0,0 @@
-use crate::db::billing_subscription::SubscriptionKind;
-use crate::db::{billing_customer, billing_subscription, user};
-use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG};
-use crate::{Config, db::billing_preference};
-use anyhow::{Context as _, Result};
-use chrono::{NaiveDateTime, Utc};
-use cloud_llm_client::Plan;
-use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
-use serde::{Deserialize, Serialize};
-use std::time::Duration;
-use thiserror::Error;
-use uuid::Uuid;
-
-#[derive(Clone, Debug, Default, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct LlmTokenClaims {
- pub iat: u64,
- pub exp: u64,
- pub jti: String,
- pub user_id: u64,
- pub system_id: Option<String>,
- pub metrics_id: Uuid,
- pub github_user_login: String,
- pub account_created_at: NaiveDateTime,
- pub is_staff: bool,
- pub has_llm_closed_beta_feature_flag: bool,
- pub bypass_account_age_check: bool,
- pub use_llm_request_queue: bool,
- pub plan: Plan,
- pub has_extended_trial: bool,
- pub subscription_period: (NaiveDateTime, NaiveDateTime),
- pub enable_model_request_overages: bool,
- pub model_request_overages_spend_limit_in_cents: u32,
- pub can_use_web_search_tool: bool,
- #[serde(default)]
- pub has_overdue_invoices: bool,
-}
-
-const LLM_TOKEN_LIFETIME: Duration = Duration::from_secs(60 * 60);
-
-impl LlmTokenClaims {
- pub fn create(
- user: &user::Model,
- is_staff: bool,
- billing_customer: billing_customer::Model,
- billing_preferences: Option<billing_preference::Model>,
- feature_flags: &Vec<String>,
- subscription: billing_subscription::Model,
- system_id: Option<String>,
- config: &Config,
- ) -> Result<String> {
- let secret = config
- .llm_api_secret
- .as_ref()
- .context("no LLM API secret")?;
-
- let plan = if is_staff {
- Plan::ZedPro
- } else {
- subscription.kind.map_or(Plan::ZedFree, |kind| match kind {
- SubscriptionKind::ZedFree => Plan::ZedFree,
- SubscriptionKind::ZedPro => Plan::ZedPro,
- SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
- })
- };
- let subscription_period =
- billing_subscription::Model::current_period(Some(subscription), is_staff)
- .map(|(start, end)| (start.naive_utc(), end.naive_utc()))
- .context("A plan is required to use Zed's hosted models or edit predictions. Visit https://zed.dev/account to get started.")?;
-
- let now = Utc::now();
- let claims = Self {
- iat: now.timestamp() as u64,
- exp: (now + LLM_TOKEN_LIFETIME).timestamp() as u64,
- jti: uuid::Uuid::new_v4().to_string(),
- user_id: user.id.to_proto(),
- system_id,
- metrics_id: user.metrics_id,
- github_user_login: user.github_login.clone(),
- account_created_at: user.account_created_at(),
- is_staff,
- has_llm_closed_beta_feature_flag: feature_flags
- .iter()
- .any(|flag| flag == "llm-closed-beta"),
- bypass_account_age_check: feature_flags
- .iter()
- .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG),
- can_use_web_search_tool: true,
- use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
- plan,
- has_extended_trial: feature_flags
- .iter()
- .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG),
- subscription_period,
- enable_model_request_overages: billing_preferences
- .as_ref()
- .map_or(false, |preferences| {
- preferences.model_request_overages_enabled
- }),
- model_request_overages_spend_limit_in_cents: billing_preferences
- .as_ref()
- .map_or(0, |preferences| {
- preferences.model_request_overages_spend_limit_in_cents as u32
- }),
- has_overdue_invoices: billing_customer.has_overdue_invoices,
- };
-
- Ok(jsonwebtoken::encode(
- &Header::default(),
- &claims,
- &EncodingKey::from_secret(secret.as_ref()),
- )?)
- }
-
- pub fn validate(token: &str, config: &Config) -> Result<LlmTokenClaims, ValidateLlmTokenError> {
- let secret = config
- .llm_api_secret
- .as_ref()
- .context("no LLM API secret")?;
-
- match jsonwebtoken::decode::<Self>(
- token,
- &DecodingKey::from_secret(secret.as_ref()),
- &Validation::default(),
- ) {
- Ok(token) => Ok(token.claims),
- Err(e) => {
- if e.kind() == &jsonwebtoken::errors::ErrorKind::ExpiredSignature {
- Err(ValidateLlmTokenError::Expired)
- } else {
- Err(ValidateLlmTokenError::JwtError(e))
- }
- }
- }
- }
-}
-
-#[derive(Error, Debug)]
-pub enum ValidateLlmTokenError {
- #[error("access token is expired")]
- Expired,
- #[error("access token validation error: {0}")]
- JwtError(#[from] jsonwebtoken::errors::Error),
- #[error("{0}")]
- Other(#[from] anyhow::Error),
-}
@@ -62,13 +62,6 @@ async fn main() -> Result<()> {
db.initialize_notification_kinds().await?;
collab::seed::seed(&config, &db, false).await?;
-
- if let Some(llm_database_url) = config.llm_database_url.clone() {
- let db_options = db::ConnectOptions::new(llm_database_url);
- let mut db = LlmDatabase::new(db_options.clone(), Executor::Production).await?;
- db.initialize().await?;
- collab::llm::db::seed_database(&config, &mut db, true).await?;
- }
}
Some("serve") => {
let mode = match args.next().as_deref() {
@@ -102,13 +95,6 @@ async fn main() -> Result<()> {
let state = AppState::new(config, Executor::Production).await?;
- if let Some(stripe_billing) = state.stripe_billing.clone() {
- let executor = state.executor.clone();
- executor.spawn_detached(async move {
- stripe_billing.initialize().await.trace_err();
- });
- }
-
if mode.is_collab() {
state.db.purge_old_embeddings().await.trace_err();
@@ -270,9 +256,6 @@ async fn setup_llm_database(config: &Config) -> Result<()> {
.llm_database_migrations_path
.as_deref()
.unwrap_or_else(|| {
- #[cfg(feature = "sqlite")]
- let default_migrations = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm.sqlite");
- #[cfg(not(feature = "sqlite"))]
let default_migrations = concat!(env!("CARGO_MANIFEST_DIR"), "/migrations_llm");
Path::new(default_migrations)
@@ -1,21 +1,12 @@
mod connection_pool;
-use crate::api::billing::find_or_create_billing_customer;
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
-use crate::db::billing_subscription::SubscriptionKind;
-use crate::llm::db::LlmDatabase;
-use crate::llm::{
- AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, LlmTokenClaims,
- MIN_ACCOUNT_AGE_FOR_LLM_USE,
-};
-use crate::stripe_client::StripeCustomerId;
use crate::{
AppState, Error, Result, auth,
db::{
- self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser,
- CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
- NotificationId, ProjectId, RejoinedProject, RemoveChannelMemberResult,
- RespondToChannelInvite, RoomId, ServerId, UpdatedChannelMessage, User, UserId,
+ self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser, Database,
+ InviteMemberResult, MembershipUpdated, NotificationId, ProjectId, RejoinedProject,
+ RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, User, UserId,
},
executor::Executor,
};
@@ -37,7 +28,6 @@ use axum::{
response::IntoResponse,
routing::get,
};
-use chrono::Utc;
use collections::{HashMap, HashSet};
pub use connection_pool::{ConnectionPool, ZedVersion};
use core::fmt::{self, Debug, Formatter};
@@ -75,7 +65,6 @@ use std::{
},
time::{Duration, Instant},
};
-use time::OffsetDateTime;
use tokio::sync::{Semaphore, watch};
use tower::ServiceBuilder;
use tracing::{
@@ -89,8 +78,6 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
// kubernetes gives terminated pods 10s to shutdown gracefully. After they're gone, we can clean up old resources.
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15);
-const MESSAGE_COUNT_PER_PAGE: usize = 100;
-const MAX_MESSAGE_LEN: usize = 1024;
const NOTIFICATION_COUNT_PER_PAGE: usize = 50;
const MAX_CONCURRENT_CONNECTIONS: usize = 512;
@@ -148,13 +135,6 @@ pub enum Principal {
}
impl Principal {
- fn user(&self) -> &User {
- match self {
- Principal::User(user) => user,
- Principal::Impersonated { user, .. } => user,
- }
- }
-
fn update_span(&self, span: &tracing::Span) {
match &self {
Principal::User(user) => {
@@ -218,6 +198,7 @@ struct Session {
/// The GeoIP country code for the user.
#[allow(unused)]
geoip_country_code: Option<String>,
+ #[allow(unused)]
system_id: Option<String>,
_executor: Executor,
}
@@ -325,7 +306,7 @@ impl Server {
let mut server = Self {
id: parking_lot::Mutex::new(id),
peer: Peer::new(id.0 as u32),
- app_state: app_state.clone(),
+ app_state,
connection_pool: Default::default(),
handlers: Default::default(),
teardown: watch::channel(false).0,
@@ -415,6 +396,8 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
.add_request_handler(forward_mutating_project_request::<proto::BlameBuffer>)
.add_request_handler(multi_lsp_query)
+ .add_request_handler(lsp_query)
+ .add_message_handler(broadcast_project_message_from_host::<proto::LspQueryResponse>)
.add_request_handler(forward_mutating_project_request::<proto::RestartLanguageServers>)
.add_request_handler(forward_mutating_project_request::<proto::StopLanguageServers>)
.add_request_handler(forward_mutating_project_request::<proto::LinkedEditingRange>)
@@ -463,9 +446,6 @@ impl Server {
.add_request_handler(follow)
.add_message_handler(unfollow)
.add_message_handler(update_followers)
- .add_request_handler(get_private_user_info)
- .add_request_handler(get_llm_api_token)
- .add_request_handler(accept_terms_of_service)
.add_message_handler(acknowledge_channel_message)
.add_message_handler(acknowledge_buffer_version)
.add_request_handler(get_supermaven_api_key)
@@ -492,7 +472,9 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
- .add_message_handler(update_context);
+ .add_message_handler(update_context)
+ .add_request_handler(forward_mutating_project_request::<proto::ToggleLspLogs>)
+ .add_message_handler(broadcast_project_message_from_host::<proto::LanguageServerLog>);
Arc::new(server)
}
@@ -634,10 +616,10 @@ impl Server {
}
}
- if let Some(live_kit) = livekit_client.as_ref() {
- if delete_livekit_room {
- live_kit.delete_room(livekit_room).await.trace_err();
- }
+ if let Some(live_kit) = livekit_client.as_ref()
+ && delete_livekit_room
+ {
+ live_kit.delete_room(livekit_room).await.trace_err();
}
}
}
@@ -928,7 +910,9 @@ impl Server {
user_id=field::Empty,
login=field::Empty,
impersonator=field::Empty,
+ // todo(lsp) remove after Zed Stable hits v0.204.x
multi_lsp_query_request=field::Empty,
+ lsp_query_request=field::Empty,
release_channel=field::Empty,
{ TOTAL_DURATION_MS }=field::Empty,
{ PROCESSING_DURATION_MS }=field::Empty,
@@ -1000,8 +984,6 @@ impl Server {
.await?;
}
- update_user_plan(session).await?;
-
let contacts = self.app_state.db.get_contacts(user.id).await?;
{
@@ -1035,99 +1017,52 @@ impl Server {
inviter_id: UserId,
invitee_id: UserId,
) -> Result<()> {
- if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await? {
- if let Some(code) = &user.invite_code {
- let pool = self.connection_pool.lock();
- let invitee_contact = contact_for_user(invitee_id, false, &pool);
- for connection_id in pool.user_connection_ids(inviter_id) {
- self.peer.send(
- connection_id,
- proto::UpdateContacts {
- contacts: vec![invitee_contact.clone()],
- ..Default::default()
- },
- )?;
- self.peer.send(
- connection_id,
- proto::UpdateInviteInfo {
- url: format!("{}{}", self.app_state.config.invite_link_prefix, &code),
- count: user.invite_count as u32,
- },
- )?;
- }
+ if let Some(user) = self.app_state.db.get_user_by_id(inviter_id).await?
+ && let Some(code) = &user.invite_code
+ {
+ let pool = self.connection_pool.lock();
+ let invitee_contact = contact_for_user(invitee_id, false, &pool);
+ for connection_id in pool.user_connection_ids(inviter_id) {
+ self.peer.send(
+ connection_id,
+ proto::UpdateContacts {
+ contacts: vec![invitee_contact.clone()],
+ ..Default::default()
+ },
+ )?;
+ self.peer.send(
+ connection_id,
+ proto::UpdateInviteInfo {
+ url: format!("{}{}", self.app_state.config.invite_link_prefix, &code),
+ count: user.invite_count as u32,
+ },
+ )?;
}
}
Ok(())
}
pub async fn invite_count_updated(self: &Arc<Self>, user_id: UserId) -> Result<()> {
- if let Some(user) = self.app_state.db.get_user_by_id(user_id).await? {
- if let Some(invite_code) = &user.invite_code {
- let pool = self.connection_pool.lock();
- for connection_id in pool.user_connection_ids(user_id) {
- self.peer.send(
- connection_id,
- proto::UpdateInviteInfo {
- url: format!(
- "{}{}",
- self.app_state.config.invite_link_prefix, invite_code
- ),
- count: user.invite_count as u32,
- },
- )?;
- }
+ if let Some(user) = self.app_state.db.get_user_by_id(user_id).await?
+ && let Some(invite_code) = &user.invite_code
+ {
+ let pool = self.connection_pool.lock();
+ for connection_id in pool.user_connection_ids(user_id) {
+ self.peer.send(
+ connection_id,
+ proto::UpdateInviteInfo {
+ url: format!(
+ "{}{}",
+ self.app_state.config.invite_link_prefix, invite_code
+ ),
+ count: user.invite_count as u32,
+ },
+ )?;
}
}
Ok(())
}
- pub async fn update_plan_for_user(
- self: &Arc<Self>,
- user_id: UserId,
- update_user_plan: proto::UpdateUserPlan,
- ) -> Result<()> {
- let pool = self.connection_pool.lock();
- for connection_id in pool.user_connection_ids(user_id) {
- self.peer
- .send(connection_id, update_user_plan.clone())
- .trace_err();
- }
-
- Ok(())
- }
-
- /// This is the legacy way of updating the user's plan, where we fetch the data to construct the `UpdateUserPlan`
- /// message on the Collab server.
- ///
- /// The new way is to receive the data from Cloud via the `POST /users/:id/update_plan` endpoint.
- pub async fn update_plan_for_user_legacy(self: &Arc<Self>, user_id: UserId) -> Result<()> {
- let user = self
- .app_state
- .db
- .get_user_by_id(user_id)
- .await?
- .context("user not found")?;
-
- let update_user_plan = make_update_user_plan_message(
- &user,
- user.admin,
- &self.app_state.db,
- self.app_state.llm_db.clone(),
- )
- .await?;
-
- self.update_plan_for_user(user_id, update_user_plan).await
- }
-
- pub async fn refresh_llm_tokens_for_user(self: &Arc<Self>, user_id: UserId) {
- let pool = self.connection_pool.lock();
- for connection_id in pool.user_connection_ids(user_id) {
- self.peer
- .send(connection_id, proto::RefreshLlmToken {})
- .trace_err();
- }
- }
-
pub async fn snapshot(self: &Arc<Self>) -> ServerSnapshot<'_> {
ServerSnapshot {
connection_pool: ConnectionPoolGuard {
@@ -1168,10 +1103,10 @@ fn broadcast<F>(
F: FnMut(ConnectionId) -> anyhow::Result<()>,
{
for receiver_id in receiver_ids {
- if Some(receiver_id) != sender_id {
- if let Err(error) = f(receiver_id) {
- tracing::error!("failed to send to {:?} {}", receiver_id, error);
- }
+ if Some(receiver_id) != sender_id
+ && let Err(error) = f(receiver_id)
+ {
+ tracing::error!("failed to send to {:?} {}", receiver_id, error);
}
}
}
@@ -1453,9 +1388,7 @@ async fn create_room(
let live_kit = live_kit?;
let user_id = session.user_id().to_string();
- let token = live_kit
- .room_token(&livekit_room, &user_id.to_string())
- .trace_err()?;
+ let token = live_kit.room_token(&livekit_room, &user_id).trace_err()?;
Some(proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
@@ -2082,9 +2015,9 @@ async fn join_project(
.unzip();
response.send(proto::JoinProjectResponse {
project_id: project.id.0 as u64,
- worktrees: worktrees.clone(),
+ worktrees,
replica_id: replica_id.0 as u32,
- collaborators: collaborators.clone(),
+ collaborators,
language_servers,
language_server_capabilities,
role: project.role.into(),
@@ -2361,11 +2294,10 @@ async fn update_language_server(
let db = session.db().await;
if let Some(proto::update_language_server::Variant::MetadataUpdated(update)) = &request.variant
+ && let Some(capabilities) = update.capabilities.clone()
{
- if let Some(capabilities) = update.capabilities.clone() {
- db.update_server_capabilities(project_id, request.language_server_id, capabilities)
- .await?;
- }
+ db.update_server_capabilities(project_id, request.language_server_id, capabilities)
+ .await?;
}
let project_connection_ids = db
@@ -2426,6 +2358,7 @@ where
Ok(())
}
+// todo(lsp) remove after Zed Stable hits v0.204.x
async fn multi_lsp_query(
request: MultiLspQuery,
response: Response<MultiLspQuery>,
@@ -2436,6 +2369,21 @@ async fn multi_lsp_query(
forward_mutating_project_request(request, response, session).await
}
+async fn lsp_query(
+ request: proto::LspQuery,
+ response: Response<proto::LspQuery>,
+ session: MessageContext,
+) -> Result<()> {
+ let (name, should_write) = request.query_name_and_write_permissions();
+ tracing::Span::current().record("lsp_query_request", name);
+ tracing::info!("lsp_query message received");
+ if should_write {
+ forward_mutating_project_request(request, response, session).await
+ } else {
+ forward_read_only_project_request(request, response, session).await
+ }
+}
+
/// Notify other participants that a new buffer has been created
async fn create_buffer_for_peer(
request: proto::CreateBufferForPeer,
@@ -2882,214 +2830,6 @@ fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool {
version.0.minor() < 139
}
-async fn current_plan(db: &Arc<Database>, user_id: UserId, is_staff: bool) -> Result<proto::Plan> {
- if is_staff {
- return Ok(proto::Plan::ZedPro);
- }
-
- let subscription = db.get_active_billing_subscription(user_id).await?;
- let subscription_kind = subscription.and_then(|subscription| subscription.kind);
-
- let plan = if let Some(subscription_kind) = subscription_kind {
- match subscription_kind {
- SubscriptionKind::ZedPro => proto::Plan::ZedPro,
- SubscriptionKind::ZedProTrial => proto::Plan::ZedProTrial,
- SubscriptionKind::ZedFree => proto::Plan::Free,
- }
- } else {
- proto::Plan::Free
- };
-
- Ok(plan)
-}
-
-async fn make_update_user_plan_message(
- user: &User,
- is_staff: bool,
- db: &Arc<Database>,
- llm_db: Option<Arc<LlmDatabase>>,
-) -> Result<proto::UpdateUserPlan> {
- let feature_flags = db.get_user_flags(user.id).await?;
- let plan = current_plan(db, user.id, is_staff).await?;
- let billing_customer = db.get_billing_customer_by_user_id(user.id).await?;
- let billing_preferences = db.get_billing_preferences(user.id).await?;
-
- let (subscription_period, usage) = if let Some(llm_db) = llm_db {
- let subscription = db.get_active_billing_subscription(user.id).await?;
-
- let subscription_period =
- crate::db::billing_subscription::Model::current_period(subscription, is_staff);
-
- let usage = if let Some((period_start_at, period_end_at)) = subscription_period {
- llm_db
- .get_subscription_usage_for_period(user.id, period_start_at, period_end_at)
- .await?
- } else {
- None
- };
-
- (subscription_period, usage)
- } else {
- (None, None)
- };
-
- let bypass_account_age_check = feature_flags
- .iter()
- .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG);
- let account_too_young = !matches!(plan, proto::Plan::ZedPro)
- && !bypass_account_age_check
- && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
-
- Ok(proto::UpdateUserPlan {
- plan: plan.into(),
- trial_started_at: billing_customer
- .as_ref()
- .and_then(|billing_customer| billing_customer.trial_started_at)
- .map(|trial_started_at| trial_started_at.and_utc().timestamp() as u64),
- is_usage_based_billing_enabled: if is_staff {
- Some(true)
- } else {
- billing_preferences.map(|preferences| preferences.model_request_overages_enabled)
- },
- subscription_period: subscription_period.map(|(started_at, ended_at)| {
- proto::SubscriptionPeriod {
- started_at: started_at.timestamp() as u64,
- ended_at: ended_at.timestamp() as u64,
- }
- }),
- account_too_young: Some(account_too_young),
- has_overdue_invoices: billing_customer
- .map(|billing_customer| billing_customer.has_overdue_invoices),
- usage: Some(
- usage
- .map(|usage| subscription_usage_to_proto(plan, usage, &feature_flags))
- .unwrap_or_else(|| make_default_subscription_usage(plan, &feature_flags)),
- ),
- })
-}
-
-fn model_requests_limit(
- plan: cloud_llm_client::Plan,
- feature_flags: &Vec<String>,
-) -> cloud_llm_client::UsageLimit {
- match plan.model_requests_limit() {
- cloud_llm_client::UsageLimit::Limited(limit) => {
- let limit = if plan == cloud_llm_client::Plan::ZedProTrial
- && feature_flags
- .iter()
- .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
- {
- 1_000
- } else {
- limit
- };
-
- cloud_llm_client::UsageLimit::Limited(limit)
- }
- cloud_llm_client::UsageLimit::Unlimited => cloud_llm_client::UsageLimit::Unlimited,
- }
-}
-
-fn subscription_usage_to_proto(
- plan: proto::Plan,
- usage: crate::llm::db::subscription_usage::Model,
- feature_flags: &Vec<String>,
-) -> proto::SubscriptionUsage {
- let plan = match plan {
- proto::Plan::Free => cloud_llm_client::Plan::ZedFree,
- proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
- proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
- };
-
- proto::SubscriptionUsage {
- model_requests_usage_amount: usage.model_requests as u32,
- model_requests_usage_limit: Some(proto::UsageLimit {
- variant: Some(match model_requests_limit(plan, feature_flags) {
- cloud_llm_client::UsageLimit::Limited(limit) => {
- proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
- limit: limit as u32,
- })
- }
- cloud_llm_client::UsageLimit::Unlimited => {
- proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
- }
- }),
- }),
- edit_predictions_usage_amount: usage.edit_predictions as u32,
- edit_predictions_usage_limit: Some(proto::UsageLimit {
- variant: Some(match plan.edit_predictions_limit() {
- cloud_llm_client::UsageLimit::Limited(limit) => {
- proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
- limit: limit as u32,
- })
- }
- cloud_llm_client::UsageLimit::Unlimited => {
- proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
- }
- }),
- }),
- }
-}
-
-fn make_default_subscription_usage(
- plan: proto::Plan,
- feature_flags: &Vec<String>,
-) -> proto::SubscriptionUsage {
- let plan = match plan {
- proto::Plan::Free => cloud_llm_client::Plan::ZedFree,
- proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
- proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
- };
-
- proto::SubscriptionUsage {
- model_requests_usage_amount: 0,
- model_requests_usage_limit: Some(proto::UsageLimit {
- variant: Some(match model_requests_limit(plan, feature_flags) {
- cloud_llm_client::UsageLimit::Limited(limit) => {
- proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
- limit: limit as u32,
- })
- }
- cloud_llm_client::UsageLimit::Unlimited => {
- proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
- }
- }),
- }),
- edit_predictions_usage_amount: 0,
- edit_predictions_usage_limit: Some(proto::UsageLimit {
- variant: Some(match plan.edit_predictions_limit() {
- cloud_llm_client::UsageLimit::Limited(limit) => {
- proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
- limit: limit as u32,
- })
- }
- cloud_llm_client::UsageLimit::Unlimited => {
- proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
- }
- }),
- }),
- }
-}
-
-async fn update_user_plan(session: &Session) -> Result<()> {
- let db = session.db().await;
-
- let update_user_plan = make_update_user_plan_message(
- session.principal.user(),
- session.is_staff(),
- &db.0,
- session.app_state.llm_db.clone(),
- )
- .await?;
-
- session
- .peer
- .send(session.connection_id, update_user_plan)
- .trace_err();
-
- Ok(())
-}
-
async fn subscribe_to_channels(
_: proto::SubscribeToChannels,
session: MessageContext,
@@ -3853,235 +3593,36 @@ fn send_notifications(
/// Send a message to the channel
async fn send_channel_message(
- request: proto::SendChannelMessage,
- response: Response<proto::SendChannelMessage>,
- session: MessageContext,
+ _request: proto::SendChannelMessage,
+ _response: Response<proto::SendChannelMessage>,
+ _session: MessageContext,
) -> Result<()> {
- // Validate the message body.
- let body = request.body.trim().to_string();
- if body.len() > MAX_MESSAGE_LEN {
- return Err(anyhow!("message is too long"))?;
- }
- if body.is_empty() {
- return Err(anyhow!("message can't be blank"))?;
- }
-
- // TODO: adjust mentions if body is trimmed
-
- let timestamp = OffsetDateTime::now_utc();
- let nonce = request.nonce.context("nonce can't be blank")?;
-
- let channel_id = ChannelId::from_proto(request.channel_id);
- let CreatedChannelMessage {
- message_id,
- participant_connection_ids,
- notifications,
- } = session
- .db()
- .await
- .create_channel_message(
- channel_id,
- session.user_id(),
- &body,
- &request.mentions,
- timestamp,
- nonce.clone().into(),
- request.reply_to_message_id.map(MessageId::from_proto),
- )
- .await?;
-
- let message = proto::ChannelMessage {
- sender_id: session.user_id().to_proto(),
- id: message_id.to_proto(),
- body,
- mentions: request.mentions,
- timestamp: timestamp.unix_timestamp() as u64,
- nonce: Some(nonce),
- reply_to_message_id: request.reply_to_message_id,
- edited_at: None,
- };
- broadcast(
- Some(session.connection_id),
- participant_connection_ids.clone(),
- |connection| {
- session.peer.send(
- connection,
- proto::ChannelMessageSent {
- channel_id: channel_id.to_proto(),
- message: Some(message.clone()),
- },
- )
- },
- );
- response.send(proto::SendChannelMessageResponse {
- message: Some(message),
- })?;
-
- let pool = &*session.connection_pool().await;
- let non_participants =
- pool.channel_connection_ids(channel_id)
- .filter_map(|(connection_id, _)| {
- if participant_connection_ids.contains(&connection_id) {
- None
- } else {
- Some(connection_id)
- }
- });
- broadcast(None, non_participants, |peer_id| {
- session.peer.send(
- peer_id,
- proto::UpdateChannels {
- latest_channel_message_ids: vec![proto::ChannelMessageId {
- channel_id: channel_id.to_proto(),
- message_id: message_id.to_proto(),
- }],
- ..Default::default()
- },
- )
- });
- send_notifications(pool, &session.peer, notifications);
-
- Ok(())
+ Err(anyhow!("chat has been removed in the latest version of Zed").into())
}
/// Delete a channel message
async fn remove_channel_message(
- request: proto::RemoveChannelMessage,
- response: Response<proto::RemoveChannelMessage>,
- session: MessageContext,
+ _request: proto::RemoveChannelMessage,
+ _response: Response<proto::RemoveChannelMessage>,
+ _session: MessageContext,
) -> Result<()> {
- let channel_id = ChannelId::from_proto(request.channel_id);
- let message_id = MessageId::from_proto(request.message_id);
- let (connection_ids, existing_notification_ids) = session
- .db()
- .await
- .remove_channel_message(channel_id, message_id, session.user_id())
- .await?;
-
- broadcast(
- Some(session.connection_id),
- connection_ids,
- move |connection| {
- session.peer.send(connection, request.clone())?;
-
- for notification_id in &existing_notification_ids {
- session.peer.send(
- connection,
- proto::DeleteNotification {
- notification_id: (*notification_id).to_proto(),
- },
- )?;
- }
-
- Ok(())
- },
- );
- response.send(proto::Ack {})?;
- Ok(())
+ Err(anyhow!("chat has been removed in the latest version of Zed").into())
}
async fn update_channel_message(
- request: proto::UpdateChannelMessage,
- response: Response<proto::UpdateChannelMessage>,
- session: MessageContext,
+ _request: proto::UpdateChannelMessage,
+ _response: Response<proto::UpdateChannelMessage>,
+ _session: MessageContext,
) -> Result<()> {
- let channel_id = ChannelId::from_proto(request.channel_id);
- let message_id = MessageId::from_proto(request.message_id);
- let updated_at = OffsetDateTime::now_utc();
- let UpdatedChannelMessage {
- message_id,
- participant_connection_ids,
- notifications,
- reply_to_message_id,
- timestamp,
- deleted_mention_notification_ids,
- updated_mention_notifications,
- } = session
- .db()
- .await
- .update_channel_message(
- channel_id,
- message_id,
- session.user_id(),
- request.body.as_str(),
- &request.mentions,
- updated_at,
- )
- .await?;
-
- let nonce = request.nonce.clone().context("nonce can't be blank")?;
-
- let message = proto::ChannelMessage {
- sender_id: session.user_id().to_proto(),
- id: message_id.to_proto(),
- body: request.body.clone(),
- mentions: request.mentions.clone(),
- timestamp: timestamp.assume_utc().unix_timestamp() as u64,
- nonce: Some(nonce),
- reply_to_message_id: reply_to_message_id.map(|id| id.to_proto()),
- edited_at: Some(updated_at.unix_timestamp() as u64),
- };
-
- response.send(proto::Ack {})?;
-
- let pool = &*session.connection_pool().await;
- broadcast(
- Some(session.connection_id),
- participant_connection_ids,
- |connection| {
- session.peer.send(
- connection,
- proto::ChannelMessageUpdate {
- channel_id: channel_id.to_proto(),
- message: Some(message.clone()),
- },
- )?;
-
- for notification_id in &deleted_mention_notification_ids {
- session.peer.send(
- connection,
- proto::DeleteNotification {
- notification_id: (*notification_id).to_proto(),
- },
- )?;
- }
-
- for notification in &updated_mention_notifications {
- session.peer.send(
- connection,
- proto::UpdateNotification {
- notification: Some(notification.clone()),
- },
- )?;
- }
-
- Ok(())
- },
- );
-
- send_notifications(pool, &session.peer, notifications);
-
- Ok(())
+ Err(anyhow!("chat has been removed in the latest version of Zed").into())
}
/// Mark a channel message as read
async fn acknowledge_channel_message(
- request: proto::AckChannelMessage,
- session: MessageContext,
+ _request: proto::AckChannelMessage,
+ _session: MessageContext,
) -> Result<()> {
- let channel_id = ChannelId::from_proto(request.channel_id);
- let message_id = MessageId::from_proto(request.message_id);
- let notifications = session
- .db()
- .await
- .observe_channel_message(channel_id, session.user_id(), message_id)
- .await?;
- send_notifications(
- &*session.connection_pool().await,
- &session.peer,
- notifications,
- );
- Ok(())
+ Err(anyhow!("chat has been removed in the latest version of Zed").into())
}
/// Mark a buffer version as synced
@@ -4134,84 +3675,37 @@ async fn get_supermaven_api_key(
/// Start receiving chat updates for a channel
async fn join_channel_chat(
- request: proto::JoinChannelChat,
- response: Response<proto::JoinChannelChat>,
- session: MessageContext,
+ _request: proto::JoinChannelChat,
+ _response: Response<proto::JoinChannelChat>,
+ _session: MessageContext,
) -> Result<()> {
- let channel_id = ChannelId::from_proto(request.channel_id);
-
- let db = session.db().await;
- db.join_channel_chat(channel_id, session.connection_id, session.user_id())
- .await?;
- let messages = db
- .get_channel_messages(channel_id, session.user_id(), MESSAGE_COUNT_PER_PAGE, None)
- .await?;
- response.send(proto::JoinChannelChatResponse {
- done: messages.len() < MESSAGE_COUNT_PER_PAGE,
- messages,
- })?;
- Ok(())
+ Err(anyhow!("chat has been removed in the latest version of Zed").into())
}
/// Stop receiving chat updates for a channel
async fn leave_channel_chat(
- request: proto::LeaveChannelChat,
- session: MessageContext,
+ _request: proto::LeaveChannelChat,
+ _session: MessageContext,
) -> Result<()> {
- let channel_id = ChannelId::from_proto(request.channel_id);
- session
- .db()
- .await
- .leave_channel_chat(channel_id, session.connection_id, session.user_id())
- .await?;
- Ok(())
+ Err(anyhow!("chat has been removed in the latest version of Zed").into())
}
/// Retrieve the chat history for a channel
async fn get_channel_messages(
- request: proto::GetChannelMessages,
- response: Response<proto::GetChannelMessages>,
- session: MessageContext,
+ _request: proto::GetChannelMessages,
+ _response: Response<proto::GetChannelMessages>,
+ _session: MessageContext,
) -> Result<()> {
- let channel_id = ChannelId::from_proto(request.channel_id);
- let messages = session
- .db()
- .await
- .get_channel_messages(
- channel_id,
- session.user_id(),
- MESSAGE_COUNT_PER_PAGE,
- Some(MessageId::from_proto(request.before_message_id)),
- )
- .await?;
- response.send(proto::GetChannelMessagesResponse {
- done: messages.len() < MESSAGE_COUNT_PER_PAGE,
- messages,
- })?;
- Ok(())
+ Err(anyhow!("chat has been removed in the latest version of Zed").into())
}
/// Retrieve specific chat messages
async fn get_channel_messages_by_id(
- request: proto::GetChannelMessagesById,
- response: Response<proto::GetChannelMessagesById>,
- session: MessageContext,
+ _request: proto::GetChannelMessagesById,
+ _response: Response<proto::GetChannelMessagesById>,
+ _session: MessageContext,
) -> Result<()> {
- let message_ids = request
- .message_ids
- .iter()
- .map(|id| MessageId::from_proto(*id))
- .collect::<Vec<_>>();
- let messages = session
- .db()
- .await
- .get_channel_messages_by_id(session.user_id(), &message_ids)
- .await?;
- response.send(proto::GetChannelMessagesResponse {
- done: messages.len() < MESSAGE_COUNT_PER_PAGE,
- messages,
- })?;
- Ok(())
+ Err(anyhow!("chat has been removed in the latest version of Zed").into())
}
/// Retrieve the current users notifications
@@ -4258,139 +3752,6 @@ async fn mark_notification_as_read(
Ok(())
}
-/// Get the current users information
-async fn get_private_user_info(
- _request: proto::GetPrivateUserInfo,
- response: Response<proto::GetPrivateUserInfo>,
- session: MessageContext,
-) -> Result<()> {
- let db = session.db().await;
-
- let metrics_id = db.get_user_metrics_id(session.user_id()).await?;
- let user = db
- .get_user_by_id(session.user_id())
- .await?
- .context("user not found")?;
- let flags = db.get_user_flags(session.user_id()).await?;
-
- response.send(proto::GetPrivateUserInfoResponse {
- metrics_id,
- staff: user.admin,
- flags,
- accepted_tos_at: user.accepted_tos_at.map(|t| t.and_utc().timestamp() as u64),
- })?;
- Ok(())
-}
-
-/// Accept the terms of service (tos) on behalf of the current user
-async fn accept_terms_of_service(
- _request: proto::AcceptTermsOfService,
- response: Response<proto::AcceptTermsOfService>,
- session: MessageContext,
-) -> Result<()> {
- let db = session.db().await;
-
- let accepted_tos_at = Utc::now();
- db.set_user_accepted_tos_at(session.user_id(), Some(accepted_tos_at.naive_utc()))
- .await?;
-
- response.send(proto::AcceptTermsOfServiceResponse {
- accepted_tos_at: accepted_tos_at.timestamp() as u64,
- })?;
-
- // When the user accepts the terms of service, we want to refresh their LLM
- // token to grant access.
- session
- .peer
- .send(session.connection_id, proto::RefreshLlmToken {})?;
-
- Ok(())
-}
-
-async fn get_llm_api_token(
- _request: proto::GetLlmToken,
- response: Response<proto::GetLlmToken>,
- session: MessageContext,
-) -> Result<()> {
- let db = session.db().await;
-
- let flags = db.get_user_flags(session.user_id()).await?;
-
- let user_id = session.user_id();
- let user = db
- .get_user_by_id(user_id)
- .await?
- .with_context(|| format!("user {user_id} not found"))?;
-
- if user.accepted_tos_at.is_none() {
- Err(anyhow!("terms of service not accepted"))?
- }
-
- let stripe_client = session
- .app_state
- .stripe_client
- .as_ref()
- .context("failed to retrieve Stripe client")?;
-
- let stripe_billing = session
- .app_state
- .stripe_billing
- .as_ref()
- .context("failed to retrieve Stripe billing object")?;
-
- let billing_customer = if let Some(billing_customer) =
- db.get_billing_customer_by_user_id(user.id).await?
- {
- billing_customer
- } else {
- let customer_id = stripe_billing
- .find_or_create_customer_by_email(user.email_address.as_deref())
- .await?;
-
- find_or_create_billing_customer(&session.app_state, stripe_client.as_ref(), &customer_id)
- .await?
- .context("billing customer not found")?
- };
-
- let billing_subscription =
- if let Some(billing_subscription) = db.get_active_billing_subscription(user.id).await? {
- billing_subscription
- } else {
- let stripe_customer_id =
- StripeCustomerId(billing_customer.stripe_customer_id.clone().into());
-
- let stripe_subscription = stripe_billing
- .subscribe_to_zed_free(stripe_customer_id)
- .await?;
-
- db.create_billing_subscription(&db::CreateBillingSubscriptionParams {
- billing_customer_id: billing_customer.id,
- kind: Some(SubscriptionKind::ZedFree),
- stripe_subscription_id: stripe_subscription.id.to_string(),
- stripe_subscription_status: stripe_subscription.status.into(),
- stripe_cancellation_reason: None,
- stripe_current_period_start: Some(stripe_subscription.current_period_start),
- stripe_current_period_end: Some(stripe_subscription.current_period_end),
- })
- .await?
- };
-
- let billing_preferences = db.get_billing_preferences(user.id).await?;
-
- let token = LlmTokenClaims::create(
- &user,
- session.is_staff(),
- billing_customer,
- billing_preferences,
- &flags,
- billing_subscription,
- session.system_id.clone(),
- &session.app_state.config,
- )?;
- response.send(proto::GetLlmTokenResponse { token })?;
- Ok(())
-}
-
fn to_axum_message(message: TungsteniteMessage) -> anyhow::Result<AxumMessage> {
let message = match message {
TungsteniteMessage::Text(payload) => AxumMessage::Text(payload.as_str().to_string()),
@@ -30,7 +30,19 @@ impl fmt::Display for ZedVersion {
impl ZedVersion {
pub fn can_collaborate(&self) -> bool {
- self.0 >= SemanticVersion::new(0, 157, 0)
+ // v0.198.4 is the first version where we no longer connect to Collab automatically.
+ // We reject any clients older than that to prevent them from connecting to Collab just for authentication.
+ if self.0 < SemanticVersion::new(0, 198, 4) {
+ return false;
+ }
+
+ // Since we hotfixed the changes to no longer connect to Collab automatically to Preview, we also need to reject
+ // versions in the range [v0.199.0, v0.199.1].
+ if self.0 >= SemanticVersion::new(0, 199, 0) && self.0 < SemanticVersion::new(0, 199, 2) {
+ return false;
+ }
+
+ true
}
}
@@ -1,156 +0,0 @@
-use std::sync::Arc;
-
-use anyhow::anyhow;
-use collections::HashMap;
-use stripe::SubscriptionStatus;
-use tokio::sync::RwLock;
-
-use crate::Result;
-use crate::stripe_client::{
- RealStripeClient, StripeAutomaticTax, StripeClient, StripeCreateSubscriptionItems,
- StripeCreateSubscriptionParams, StripeCustomerId, StripePrice, StripePriceId,
- StripeSubscription,
-};
-
-pub struct StripeBilling {
- state: RwLock<StripeBillingState>,
- client: Arc<dyn StripeClient>,
-}
-
-#[derive(Default)]
-struct StripeBillingState {
- prices_by_lookup_key: HashMap<String, StripePrice>,
-}
-
-impl StripeBilling {
- pub fn new(client: Arc<stripe::Client>) -> Self {
- Self {
- client: Arc::new(RealStripeClient::new(client.clone())),
- state: RwLock::default(),
- }
- }
-
- #[cfg(test)]
- pub fn test(client: Arc<crate::stripe_client::FakeStripeClient>) -> Self {
- Self {
- client,
- state: RwLock::default(),
- }
- }
-
- pub fn client(&self) -> &Arc<dyn StripeClient> {
- &self.client
- }
-
- pub async fn initialize(&self) -> Result<()> {
- log::info!("StripeBilling: initializing");
-
- let mut state = self.state.write().await;
-
- let prices = self.client.list_prices().await?;
-
- for price in prices {
- if let Some(lookup_key) = price.lookup_key.clone() {
- state.prices_by_lookup_key.insert(lookup_key, price);
- }
- }
-
- log::info!("StripeBilling: initialized");
-
- Ok(())
- }
-
- pub async fn zed_pro_price_id(&self) -> Result<StripePriceId> {
- self.find_price_id_by_lookup_key("zed-pro").await
- }
-
- pub async fn zed_free_price_id(&self) -> Result<StripePriceId> {
- self.find_price_id_by_lookup_key("zed-free").await
- }
-
- pub async fn find_price_id_by_lookup_key(&self, lookup_key: &str) -> Result<StripePriceId> {
- self.state
- .read()
- .await
- .prices_by_lookup_key
- .get(lookup_key)
- .map(|price| price.id.clone())
- .ok_or_else(|| crate::Error::Internal(anyhow!("no price ID found for {lookup_key:?}")))
- }
-
- pub async fn find_price_by_lookup_key(&self, lookup_key: &str) -> Result<StripePrice> {
- self.state
- .read()
- .await
- .prices_by_lookup_key
- .get(lookup_key)
- .cloned()
- .ok_or_else(|| crate::Error::Internal(anyhow!("no price found for {lookup_key:?}")))
- }
-
- /// Returns the Stripe customer associated with the provided email address, or creates a new customer, if one does
- /// not already exist.
- ///
- /// Always returns a new Stripe customer if the email address is `None`.
- pub async fn find_or_create_customer_by_email(
- &self,
- email_address: Option<&str>,
- ) -> Result<StripeCustomerId> {
- let existing_customer = if let Some(email) = email_address {
- let customers = self.client.list_customers_by_email(email).await?;
-
- customers.first().cloned()
- } else {
- None
- };
-
- let customer_id = if let Some(existing_customer) = existing_customer {
- existing_customer.id
- } else {
- let customer = self
- .client
- .create_customer(crate::stripe_client::CreateCustomerParams {
- email: email_address,
- })
- .await?;
-
- customer.id
- };
-
- Ok(customer_id)
- }
-
- pub async fn subscribe_to_zed_free(
- &self,
- customer_id: StripeCustomerId,
- ) -> Result<StripeSubscription> {
- let zed_free_price_id = self.zed_free_price_id().await?;
-
- let existing_subscriptions = self
- .client
- .list_subscriptions_for_customer(&customer_id)
- .await?;
-
- let existing_active_subscription =
- existing_subscriptions.into_iter().find(|subscription| {
- subscription.status == SubscriptionStatus::Active
- || subscription.status == SubscriptionStatus::Trialing
- });
- if let Some(subscription) = existing_active_subscription {
- return Ok(subscription);
- }
-
- let params = StripeCreateSubscriptionParams {
- customer: customer_id,
- items: vec![StripeCreateSubscriptionItems {
- price: Some(zed_free_price_id),
- quantity: Some(1),
- }],
- automatic_tax: Some(StripeAutomaticTax { enabled: true }),
- };
-
- let subscription = self.client.create_subscription(params).await?;
-
- Ok(subscription)
- }
-}
@@ -1,285 +0,0 @@
-#[cfg(test)]
-mod fake_stripe_client;
-mod real_stripe_client;
-
-use std::collections::HashMap;
-use std::sync::Arc;
-
-use anyhow::Result;
-use async_trait::async_trait;
-
-#[cfg(test)]
-pub use fake_stripe_client::*;
-pub use real_stripe_client::*;
-use serde::{Deserialize, Serialize};
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display, Serialize)]
-pub struct StripeCustomerId(pub Arc<str>);
-
-#[derive(Debug, Clone)]
-pub struct StripeCustomer {
- pub id: StripeCustomerId,
- pub email: Option<String>,
-}
-
-#[derive(Debug)]
-pub struct CreateCustomerParams<'a> {
- pub email: Option<&'a str>,
-}
-
-#[derive(Debug)]
-pub struct UpdateCustomerParams<'a> {
- pub email: Option<&'a str>,
-}
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)]
-pub struct StripeSubscriptionId(pub Arc<str>);
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeSubscription {
- pub id: StripeSubscriptionId,
- pub customer: StripeCustomerId,
- // TODO: Create our own version of this enum.
- pub status: stripe::SubscriptionStatus,
- pub current_period_end: i64,
- pub current_period_start: i64,
- pub items: Vec<StripeSubscriptionItem>,
- pub cancel_at: Option<i64>,
- pub cancellation_details: Option<StripeCancellationDetails>,
-}
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)]
-pub struct StripeSubscriptionItemId(pub Arc<str>);
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeSubscriptionItem {
- pub id: StripeSubscriptionItemId,
- pub price: Option<StripePrice>,
-}
-
-#[derive(Debug, Clone, PartialEq)]
-pub struct StripeCancellationDetails {
- pub reason: Option<StripeCancellationDetailsReason>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCancellationDetailsReason {
- CancellationRequested,
- PaymentDisputed,
- PaymentFailed,
-}
-
-#[derive(Debug)]
-pub struct StripeCreateSubscriptionParams {
- pub customer: StripeCustomerId,
- pub items: Vec<StripeCreateSubscriptionItems>,
- pub automatic_tax: Option<StripeAutomaticTax>,
-}
-
-#[derive(Debug)]
-pub struct StripeCreateSubscriptionItems {
- pub price: Option<StripePriceId>,
- pub quantity: Option<u64>,
-}
-
-#[derive(Debug, Clone)]
-pub struct UpdateSubscriptionParams {
- pub items: Option<Vec<UpdateSubscriptionItems>>,
- pub trial_settings: Option<StripeSubscriptionTrialSettings>,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct UpdateSubscriptionItems {
- pub price: Option<StripePriceId>,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeSubscriptionTrialSettings {
- pub end_behavior: StripeSubscriptionTrialSettingsEndBehavior,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeSubscriptionTrialSettingsEndBehavior {
- pub missing_payment_method: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod {
- Cancel,
- CreateInvoice,
- Pause,
-}
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display)]
-pub struct StripePriceId(pub Arc<str>);
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripePrice {
- pub id: StripePriceId,
- pub unit_amount: Option<i64>,
- pub lookup_key: Option<String>,
- pub recurring: Option<StripePriceRecurring>,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripePriceRecurring {
- pub meter: Option<String>,
-}
-
-#[derive(Debug, PartialEq, Eq, Hash, Clone, derive_more::Display, Deserialize)]
-pub struct StripeMeterId(pub Arc<str>);
-
-#[derive(Debug, Clone, Deserialize)]
-pub struct StripeMeter {
- pub id: StripeMeterId,
- pub event_name: String,
-}
-
-#[derive(Debug, Serialize)]
-pub struct StripeCreateMeterEventParams<'a> {
- pub identifier: &'a str,
- pub event_name: &'a str,
- pub payload: StripeCreateMeterEventPayload<'a>,
- pub timestamp: Option<i64>,
-}
-
-#[derive(Debug, Serialize)]
-pub struct StripeCreateMeterEventPayload<'a> {
- pub value: u64,
- pub stripe_customer_id: &'a StripeCustomerId,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeBillingAddressCollection {
- Auto,
- Required,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeCustomerUpdate {
- pub address: Option<StripeCustomerUpdateAddress>,
- pub name: Option<StripeCustomerUpdateName>,
- pub shipping: Option<StripeCustomerUpdateShipping>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCustomerUpdateAddress {
- Auto,
- Never,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCustomerUpdateName {
- Auto,
- Never,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCustomerUpdateShipping {
- Auto,
- Never,
-}
-
-#[derive(Debug, Default)]
-pub struct StripeCreateCheckoutSessionParams<'a> {
- pub customer: Option<&'a StripeCustomerId>,
- pub client_reference_id: Option<&'a str>,
- pub mode: Option<StripeCheckoutSessionMode>,
- pub line_items: Option<Vec<StripeCreateCheckoutSessionLineItems>>,
- pub payment_method_collection: Option<StripeCheckoutSessionPaymentMethodCollection>,
- pub subscription_data: Option<StripeCreateCheckoutSessionSubscriptionData>,
- pub success_url: Option<&'a str>,
- pub billing_address_collection: Option<StripeBillingAddressCollection>,
- pub customer_update: Option<StripeCustomerUpdate>,
- pub tax_id_collection: Option<StripeTaxIdCollection>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCheckoutSessionMode {
- Payment,
- Setup,
- Subscription,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeCreateCheckoutSessionLineItems {
- pub price: Option<String>,
- pub quantity: Option<u64>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum StripeCheckoutSessionPaymentMethodCollection {
- Always,
- IfRequired,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeCreateCheckoutSessionSubscriptionData {
- pub metadata: Option<HashMap<String, String>>,
- pub trial_period_days: Option<u32>,
- pub trial_settings: Option<StripeSubscriptionTrialSettings>,
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct StripeTaxIdCollection {
- pub enabled: bool,
-}
-
-#[derive(Debug, Clone)]
-pub struct StripeAutomaticTax {
- pub enabled: bool,
-}
-
-#[derive(Debug)]
-pub struct StripeCheckoutSession {
- pub url: Option<String>,
-}
-
-#[async_trait]
-pub trait StripeClient: Send + Sync {
- async fn list_customers_by_email(&self, email: &str) -> Result<Vec<StripeCustomer>>;
-
- async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result<StripeCustomer>;
-
- async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result<StripeCustomer>;
-
- async fn update_customer(
- &self,
- customer_id: &StripeCustomerId,
- params: UpdateCustomerParams<'_>,
- ) -> Result<StripeCustomer>;
-
- async fn list_subscriptions_for_customer(
- &self,
- customer_id: &StripeCustomerId,
- ) -> Result<Vec<StripeSubscription>>;
-
- async fn get_subscription(
- &self,
- subscription_id: &StripeSubscriptionId,
- ) -> Result<StripeSubscription>;
-
- async fn create_subscription(
- &self,
- params: StripeCreateSubscriptionParams,
- ) -> Result<StripeSubscription>;
-
- async fn update_subscription(
- &self,
- subscription_id: &StripeSubscriptionId,
- params: UpdateSubscriptionParams,
- ) -> Result<()>;
-
- async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()>;
-
- async fn list_prices(&self) -> Result<Vec<StripePrice>>;
-
- async fn list_meters(&self) -> Result<Vec<StripeMeter>>;
-
- async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()>;
-
- async fn create_checkout_session(
- &self,
- params: StripeCreateCheckoutSessionParams<'_>,
- ) -> Result<StripeCheckoutSession>;
-}
@@ -1,247 +0,0 @@
-use std::sync::Arc;
-
-use anyhow::{Result, anyhow};
-use async_trait::async_trait;
-use chrono::{Duration, Utc};
-use collections::HashMap;
-use parking_lot::Mutex;
-use uuid::Uuid;
-
-use crate::stripe_client::{
- CreateCustomerParams, StripeBillingAddressCollection, StripeCheckoutSession,
- StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient,
- StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
- StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
- StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
- StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription,
- StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeTaxIdCollection,
- UpdateCustomerParams, UpdateSubscriptionParams,
-};
-
-#[derive(Debug, Clone)]
-pub struct StripeCreateMeterEventCall {
- pub identifier: Arc<str>,
- pub event_name: Arc<str>,
- pub value: u64,
- pub stripe_customer_id: StripeCustomerId,
- pub timestamp: Option<i64>,
-}
-
-#[derive(Debug, Clone)]
-pub struct StripeCreateCheckoutSessionCall {
- pub customer: Option<StripeCustomerId>,
- pub client_reference_id: Option<String>,
- pub mode: Option<StripeCheckoutSessionMode>,
- pub line_items: Option<Vec<StripeCreateCheckoutSessionLineItems>>,
- pub payment_method_collection: Option<StripeCheckoutSessionPaymentMethodCollection>,
- pub subscription_data: Option<StripeCreateCheckoutSessionSubscriptionData>,
- pub success_url: Option<String>,
- pub billing_address_collection: Option<StripeBillingAddressCollection>,
- pub customer_update: Option<StripeCustomerUpdate>,
- pub tax_id_collection: Option<StripeTaxIdCollection>,
-}
-
-pub struct FakeStripeClient {
- pub customers: Arc<Mutex<HashMap<StripeCustomerId, StripeCustomer>>>,
- pub subscriptions: Arc<Mutex<HashMap<StripeSubscriptionId, StripeSubscription>>>,
- pub update_subscription_calls:
- Arc<Mutex<Vec<(StripeSubscriptionId, UpdateSubscriptionParams)>>>,
- pub prices: Arc<Mutex<HashMap<StripePriceId, StripePrice>>>,
- pub meters: Arc<Mutex<HashMap<StripeMeterId, StripeMeter>>>,
- pub create_meter_event_calls: Arc<Mutex<Vec<StripeCreateMeterEventCall>>>,
- pub create_checkout_session_calls: Arc<Mutex<Vec<StripeCreateCheckoutSessionCall>>>,
-}
-
-impl FakeStripeClient {
- pub fn new() -> Self {
- Self {
- customers: Arc::new(Mutex::new(HashMap::default())),
- subscriptions: Arc::new(Mutex::new(HashMap::default())),
- update_subscription_calls: Arc::new(Mutex::new(Vec::new())),
- prices: Arc::new(Mutex::new(HashMap::default())),
- meters: Arc::new(Mutex::new(HashMap::default())),
- create_meter_event_calls: Arc::new(Mutex::new(Vec::new())),
- create_checkout_session_calls: Arc::new(Mutex::new(Vec::new())),
- }
- }
-}
-
-#[async_trait]
-impl StripeClient for FakeStripeClient {
- async fn list_customers_by_email(&self, email: &str) -> Result<Vec<StripeCustomer>> {
- Ok(self
- .customers
- .lock()
- .values()
- .filter(|customer| customer.email.as_deref() == Some(email))
- .cloned()
- .collect())
- }
-
- async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result<StripeCustomer> {
- self.customers
- .lock()
- .get(customer_id)
- .cloned()
- .ok_or_else(|| anyhow!("no customer found for {customer_id:?}"))
- }
-
- async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result<StripeCustomer> {
- let customer = StripeCustomer {
- id: StripeCustomerId(format!("cus_{}", Uuid::new_v4()).into()),
- email: params.email.map(|email| email.to_string()),
- };
-
- self.customers
- .lock()
- .insert(customer.id.clone(), customer.clone());
-
- Ok(customer)
- }
-
- async fn update_customer(
- &self,
- customer_id: &StripeCustomerId,
- params: UpdateCustomerParams<'_>,
- ) -> Result<StripeCustomer> {
- let mut customers = self.customers.lock();
- if let Some(customer) = customers.get_mut(customer_id) {
- if let Some(email) = params.email {
- customer.email = Some(email.to_string());
- }
- Ok(customer.clone())
- } else {
- Err(anyhow!("no customer found for {customer_id:?}"))
- }
- }
-
- async fn list_subscriptions_for_customer(
- &self,
- customer_id: &StripeCustomerId,
- ) -> Result<Vec<StripeSubscription>> {
- let subscriptions = self
- .subscriptions
- .lock()
- .values()
- .filter(|subscription| subscription.customer == *customer_id)
- .cloned()
- .collect();
-
- Ok(subscriptions)
- }
-
- async fn get_subscription(
- &self,
- subscription_id: &StripeSubscriptionId,
- ) -> Result<StripeSubscription> {
- self.subscriptions
- .lock()
- .get(subscription_id)
- .cloned()
- .ok_or_else(|| anyhow!("no subscription found for {subscription_id:?}"))
- }
-
- async fn create_subscription(
- &self,
- params: StripeCreateSubscriptionParams,
- ) -> Result<StripeSubscription> {
- let now = Utc::now();
-
- let subscription = StripeSubscription {
- id: StripeSubscriptionId(format!("sub_{}", Uuid::new_v4()).into()),
- customer: params.customer,
- status: stripe::SubscriptionStatus::Active,
- current_period_start: now.timestamp(),
- current_period_end: (now + Duration::days(30)).timestamp(),
- items: params
- .items
- .into_iter()
- .map(|item| StripeSubscriptionItem {
- id: StripeSubscriptionItemId(format!("si_{}", Uuid::new_v4()).into()),
- price: item
- .price
- .and_then(|price_id| self.prices.lock().get(&price_id).cloned()),
- })
- .collect(),
- cancel_at: None,
- cancellation_details: None,
- };
-
- self.subscriptions
- .lock()
- .insert(subscription.id.clone(), subscription.clone());
-
- Ok(subscription)
- }
-
- async fn update_subscription(
- &self,
- subscription_id: &StripeSubscriptionId,
- params: UpdateSubscriptionParams,
- ) -> Result<()> {
- let subscription = self.get_subscription(subscription_id).await?;
-
- self.update_subscription_calls
- .lock()
- .push((subscription.id, params));
-
- Ok(())
- }
-
- async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()> {
- // TODO: Implement fake subscription cancellation.
- let _ = subscription_id;
-
- Ok(())
- }
-
- async fn list_prices(&self) -> Result<Vec<StripePrice>> {
- let prices = self.prices.lock().values().cloned().collect();
-
- Ok(prices)
- }
-
- async fn list_meters(&self) -> Result<Vec<StripeMeter>> {
- let meters = self.meters.lock().values().cloned().collect();
-
- Ok(meters)
- }
-
- async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()> {
- self.create_meter_event_calls
- .lock()
- .push(StripeCreateMeterEventCall {
- identifier: params.identifier.into(),
- event_name: params.event_name.into(),
- value: params.payload.value,
- stripe_customer_id: params.payload.stripe_customer_id.clone(),
- timestamp: params.timestamp,
- });
-
- Ok(())
- }
-
- async fn create_checkout_session(
- &self,
- params: StripeCreateCheckoutSessionParams<'_>,
- ) -> Result<StripeCheckoutSession> {
- self.create_checkout_session_calls
- .lock()
- .push(StripeCreateCheckoutSessionCall {
- customer: params.customer.cloned(),
- client_reference_id: params.client_reference_id.map(|id| id.to_string()),
- mode: params.mode,
- line_items: params.line_items,
- payment_method_collection: params.payment_method_collection,
- subscription_data: params.subscription_data,
- success_url: params.success_url.map(|url| url.to_string()),
- billing_address_collection: params.billing_address_collection,
- customer_update: params.customer_update,
- tax_id_collection: params.tax_id_collection,
- });
-
- Ok(StripeCheckoutSession {
- url: Some("https://checkout.stripe.com/c/pay/cs_test_1".to_string()),
- })
- }
-}
@@ -1,612 +0,0 @@
-use std::str::FromStr as _;
-use std::sync::Arc;
-
-use anyhow::{Context as _, Result, anyhow};
-use async_trait::async_trait;
-use serde::{Deserialize, Serialize};
-use stripe::{
- CancellationDetails, CancellationDetailsReason, CheckoutSession, CheckoutSessionMode,
- CheckoutSessionPaymentMethodCollection, CreateCheckoutSession, CreateCheckoutSessionLineItems,
- CreateCheckoutSessionSubscriptionData, CreateCheckoutSessionSubscriptionDataTrialSettings,
- CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior,
- CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod,
- CreateCustomer, CreateSubscriptionAutomaticTax, Customer, CustomerId, ListCustomers, Price,
- PriceId, Recurring, Subscription, SubscriptionId, SubscriptionItem, SubscriptionItemId,
- UpdateCustomer, UpdateSubscriptionItems, UpdateSubscriptionTrialSettings,
- UpdateSubscriptionTrialSettingsEndBehavior,
- UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod,
-};
-
-use crate::stripe_client::{
- CreateCustomerParams, StripeAutomaticTax, StripeBillingAddressCollection,
- StripeCancellationDetails, StripeCancellationDetailsReason, StripeCheckoutSession,
- StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient,
- StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
- StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
- StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate,
- StripeCustomerUpdateAddress, StripeCustomerUpdateName, StripeCustomerUpdateShipping,
- StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription,
- StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId,
- StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior,
- StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection,
- UpdateCustomerParams, UpdateSubscriptionParams,
-};
-
-pub struct RealStripeClient {
- client: Arc<stripe::Client>,
-}
-
-impl RealStripeClient {
- pub fn new(client: Arc<stripe::Client>) -> Self {
- Self { client }
- }
-}
-
-#[async_trait]
-impl StripeClient for RealStripeClient {
- async fn list_customers_by_email(&self, email: &str) -> Result<Vec<StripeCustomer>> {
- let response = Customer::list(
- &self.client,
- &ListCustomers {
- email: Some(email),
- ..Default::default()
- },
- )
- .await?;
-
- Ok(response
- .data
- .into_iter()
- .map(StripeCustomer::from)
- .collect())
- }
-
- async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result<StripeCustomer> {
- let customer_id = customer_id.try_into()?;
-
- let customer = Customer::retrieve(&self.client, &customer_id, &[]).await?;
-
- Ok(StripeCustomer::from(customer))
- }
-
- async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result<StripeCustomer> {
- let customer = Customer::create(
- &self.client,
- CreateCustomer {
- email: params.email,
- ..Default::default()
- },
- )
- .await?;
-
- Ok(StripeCustomer::from(customer))
- }
-
- async fn update_customer(
- &self,
- customer_id: &StripeCustomerId,
- params: UpdateCustomerParams<'_>,
- ) -> Result<StripeCustomer> {
- let customer = Customer::update(
- &self.client,
- &customer_id.try_into()?,
- UpdateCustomer {
- email: params.email,
- ..Default::default()
- },
- )
- .await?;
-
- Ok(StripeCustomer::from(customer))
- }
-
- async fn list_subscriptions_for_customer(
- &self,
- customer_id: &StripeCustomerId,
- ) -> Result<Vec<StripeSubscription>> {
- let customer_id = customer_id.try_into()?;
-
- let subscriptions = stripe::Subscription::list(
- &self.client,
- &stripe::ListSubscriptions {
- customer: Some(customer_id),
- status: None,
- ..Default::default()
- },
- )
- .await?;
-
- Ok(subscriptions
- .data
- .into_iter()
- .map(StripeSubscription::from)
- .collect())
- }
-
- async fn get_subscription(
- &self,
- subscription_id: &StripeSubscriptionId,
- ) -> Result<StripeSubscription> {
- let subscription_id = subscription_id.try_into()?;
-
- let subscription = Subscription::retrieve(&self.client, &subscription_id, &[]).await?;
-
- Ok(StripeSubscription::from(subscription))
- }
-
- async fn create_subscription(
- &self,
- params: StripeCreateSubscriptionParams,
- ) -> Result<StripeSubscription> {
- let customer_id = params.customer.try_into()?;
-
- let mut create_subscription = stripe::CreateSubscription::new(customer_id);
- create_subscription.items = Some(
- params
- .items
- .into_iter()
- .map(|item| stripe::CreateSubscriptionItems {
- price: item.price.map(|price| price.to_string()),
- quantity: item.quantity,
- ..Default::default()
- })
- .collect(),
- );
- create_subscription.automatic_tax = params.automatic_tax.map(Into::into);
-
- let subscription = Subscription::create(&self.client, create_subscription).await?;
-
- Ok(StripeSubscription::from(subscription))
- }
-
- async fn update_subscription(
- &self,
- subscription_id: &StripeSubscriptionId,
- params: UpdateSubscriptionParams,
- ) -> Result<()> {
- let subscription_id = subscription_id.try_into()?;
-
- stripe::Subscription::update(
- &self.client,
- &subscription_id,
- stripe::UpdateSubscription {
- items: params.items.map(|items| {
- items
- .into_iter()
- .map(|item| UpdateSubscriptionItems {
- price: item.price.map(|price| price.to_string()),
- ..Default::default()
- })
- .collect()
- }),
- trial_settings: params.trial_settings.map(Into::into),
- ..Default::default()
- },
- )
- .await?;
-
- Ok(())
- }
-
- async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()> {
- let subscription_id = subscription_id.try_into()?;
-
- Subscription::cancel(
- &self.client,
- &subscription_id,
- stripe::CancelSubscription {
- invoice_now: None,
- ..Default::default()
- },
- )
- .await?;
-
- Ok(())
- }
-
- async fn list_prices(&self) -> Result<Vec<StripePrice>> {
- let response = stripe::Price::list(
- &self.client,
- &stripe::ListPrices {
- limit: Some(100),
- ..Default::default()
- },
- )
- .await?;
-
- Ok(response.data.into_iter().map(StripePrice::from).collect())
- }
-
- async fn list_meters(&self) -> Result<Vec<StripeMeter>> {
- #[derive(Serialize)]
- struct Params {
- #[serde(skip_serializing_if = "Option::is_none")]
- limit: Option<u64>,
- }
-
- let response = self
- .client
- .get_query::<stripe::List<StripeMeter>, _>(
- "/billing/meters",
- Params { limit: Some(100) },
- )
- .await?;
-
- Ok(response.data)
- }
-
- async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()> {
- #[derive(Deserialize)]
- struct StripeMeterEvent {
- pub identifier: String,
- }
-
- let identifier = params.identifier;
- match self
- .client
- .post_form::<StripeMeterEvent, _>("/billing/meter_events", params)
- .await
- {
- Ok(_event) => Ok(()),
- Err(stripe::StripeError::Stripe(error)) => {
- if error.http_status == 400
- && error
- .message
- .as_ref()
- .map_or(false, |message| message.contains(identifier))
- {
- Ok(())
- } else {
- Err(anyhow!(stripe::StripeError::Stripe(error)))
- }
- }
- Err(error) => Err(anyhow!("failed to create meter event: {error:?}")),
- }
- }
-
- async fn create_checkout_session(
- &self,
- params: StripeCreateCheckoutSessionParams<'_>,
- ) -> Result<StripeCheckoutSession> {
- let params = params.try_into()?;
- let session = CheckoutSession::create(&self.client, params).await?;
-
- Ok(session.into())
- }
-}
-
-impl From<CustomerId> for StripeCustomerId {
- fn from(value: CustomerId) -> Self {
- Self(value.as_str().into())
- }
-}
-
-impl TryFrom<StripeCustomerId> for CustomerId {
- type Error = anyhow::Error;
-
- fn try_from(value: StripeCustomerId) -> Result<Self, Self::Error> {
- Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID")
- }
-}
-
-impl TryFrom<&StripeCustomerId> for CustomerId {
- type Error = anyhow::Error;
-
- fn try_from(value: &StripeCustomerId) -> Result<Self, Self::Error> {
- Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID")
- }
-}
-
-impl From<Customer> for StripeCustomer {
- fn from(value: Customer) -> Self {
- StripeCustomer {
- id: value.id.into(),
- email: value.email,
- }
- }
-}
-
-impl From<SubscriptionId> for StripeSubscriptionId {
- fn from(value: SubscriptionId) -> Self {
- Self(value.as_str().into())
- }
-}
-
-impl TryFrom<&StripeSubscriptionId> for SubscriptionId {
- type Error = anyhow::Error;
-
- fn try_from(value: &StripeSubscriptionId) -> Result<Self, Self::Error> {
- Self::from_str(value.0.as_ref()).context("failed to parse Stripe subscription ID")
- }
-}
-
-impl From<Subscription> for StripeSubscription {
- fn from(value: Subscription) -> Self {
- Self {
- id: value.id.into(),
- customer: value.customer.id().into(),
- status: value.status,
- current_period_start: value.current_period_start,
- current_period_end: value.current_period_end,
- items: value.items.data.into_iter().map(Into::into).collect(),
- cancel_at: value.cancel_at,
- cancellation_details: value.cancellation_details.map(Into::into),
- }
- }
-}
-
-impl From<CancellationDetails> for StripeCancellationDetails {
- fn from(value: CancellationDetails) -> Self {
- Self {
- reason: value.reason.map(Into::into),
- }
- }
-}
-
-impl From<CancellationDetailsReason> for StripeCancellationDetailsReason {
- fn from(value: CancellationDetailsReason) -> Self {
- match value {
- CancellationDetailsReason::CancellationRequested => Self::CancellationRequested,
- CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed,
- CancellationDetailsReason::PaymentFailed => Self::PaymentFailed,
- }
- }
-}
-
-impl From<SubscriptionItemId> for StripeSubscriptionItemId {
- fn from(value: SubscriptionItemId) -> Self {
- Self(value.as_str().into())
- }
-}
-
-impl From<SubscriptionItem> for StripeSubscriptionItem {
- fn from(value: SubscriptionItem) -> Self {
- Self {
- id: value.id.into(),
- price: value.price.map(Into::into),
- }
- }
-}
-
-impl From<StripeAutomaticTax> for CreateSubscriptionAutomaticTax {
- fn from(value: StripeAutomaticTax) -> Self {
- Self {
- enabled: value.enabled,
- liability: None,
- }
- }
-}
-
-impl From<StripeSubscriptionTrialSettings> for UpdateSubscriptionTrialSettings {
- fn from(value: StripeSubscriptionTrialSettings) -> Self {
- Self {
- end_behavior: value.end_behavior.into(),
- }
- }
-}
-
-impl From<StripeSubscriptionTrialSettingsEndBehavior>
- for UpdateSubscriptionTrialSettingsEndBehavior
-{
- fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self {
- Self {
- missing_payment_method: value.missing_payment_method.into(),
- }
- }
-}
-
-impl From<StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod>
- for UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod
-{
- fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self {
- match value {
- StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel,
- StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => {
- Self::CreateInvoice
- }
- StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause,
- }
- }
-}
-
-impl From<PriceId> for StripePriceId {
- fn from(value: PriceId) -> Self {
- Self(value.as_str().into())
- }
-}
-
-impl TryFrom<StripePriceId> for PriceId {
- type Error = anyhow::Error;
-
- fn try_from(value: StripePriceId) -> Result<Self, Self::Error> {
- Self::from_str(value.0.as_ref()).context("failed to parse Stripe price ID")
- }
-}
-
-impl From<Price> for StripePrice {
- fn from(value: Price) -> Self {
- Self {
- id: value.id.into(),
- unit_amount: value.unit_amount,
- lookup_key: value.lookup_key,
- recurring: value.recurring.map(StripePriceRecurring::from),
- }
- }
-}
-
-impl From<Recurring> for StripePriceRecurring {
- fn from(value: Recurring) -> Self {
- Self { meter: value.meter }
- }
-}
-
-impl<'a> TryFrom<StripeCreateCheckoutSessionParams<'a>> for CreateCheckoutSession<'a> {
- type Error = anyhow::Error;
-
- fn try_from(value: StripeCreateCheckoutSessionParams<'a>) -> Result<Self, Self::Error> {
- Ok(Self {
- customer: value
- .customer
- .map(|customer_id| customer_id.try_into())
- .transpose()?,
- client_reference_id: value.client_reference_id,
- mode: value.mode.map(Into::into),
- line_items: value
- .line_items
- .map(|line_items| line_items.into_iter().map(Into::into).collect()),
- payment_method_collection: value.payment_method_collection.map(Into::into),
- subscription_data: value.subscription_data.map(Into::into),
- success_url: value.success_url,
- billing_address_collection: value.billing_address_collection.map(Into::into),
- customer_update: value.customer_update.map(Into::into),
- tax_id_collection: value.tax_id_collection.map(Into::into),
- ..Default::default()
- })
- }
-}
-
-impl From<StripeCheckoutSessionMode> for CheckoutSessionMode {
- fn from(value: StripeCheckoutSessionMode) -> Self {
- match value {
- StripeCheckoutSessionMode::Payment => Self::Payment,
- StripeCheckoutSessionMode::Setup => Self::Setup,
- StripeCheckoutSessionMode::Subscription => Self::Subscription,
- }
- }
-}
-
-impl From<StripeCreateCheckoutSessionLineItems> for CreateCheckoutSessionLineItems {
- fn from(value: StripeCreateCheckoutSessionLineItems) -> Self {
- Self {
- price: value.price,
- quantity: value.quantity,
- ..Default::default()
- }
- }
-}
-
-impl From<StripeCheckoutSessionPaymentMethodCollection> for CheckoutSessionPaymentMethodCollection {
- fn from(value: StripeCheckoutSessionPaymentMethodCollection) -> Self {
- match value {
- StripeCheckoutSessionPaymentMethodCollection::Always => Self::Always,
- StripeCheckoutSessionPaymentMethodCollection::IfRequired => Self::IfRequired,
- }
- }
-}
-
-impl From<StripeCreateCheckoutSessionSubscriptionData> for CreateCheckoutSessionSubscriptionData {
- fn from(value: StripeCreateCheckoutSessionSubscriptionData) -> Self {
- Self {
- trial_period_days: value.trial_period_days,
- trial_settings: value.trial_settings.map(Into::into),
- metadata: value.metadata,
- ..Default::default()
- }
- }
-}
-
-impl From<StripeSubscriptionTrialSettings> for CreateCheckoutSessionSubscriptionDataTrialSettings {
- fn from(value: StripeSubscriptionTrialSettings) -> Self {
- Self {
- end_behavior: value.end_behavior.into(),
- }
- }
-}
-
-impl From<StripeSubscriptionTrialSettingsEndBehavior>
- for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior
-{
- fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self {
- Self {
- missing_payment_method: value.missing_payment_method.into(),
- }
- }
-}
-
-impl From<StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod>
- for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod
-{
- fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self {
- match value {
- StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel,
- StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => {
- Self::CreateInvoice
- }
- StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause,
- }
- }
-}
-
-impl From<CheckoutSession> for StripeCheckoutSession {
- fn from(value: CheckoutSession) -> Self {
- Self { url: value.url }
- }
-}
-
-impl From<StripeBillingAddressCollection> for stripe::CheckoutSessionBillingAddressCollection {
- fn from(value: StripeBillingAddressCollection) -> Self {
- match value {
- StripeBillingAddressCollection::Auto => {
- stripe::CheckoutSessionBillingAddressCollection::Auto
- }
- StripeBillingAddressCollection::Required => {
- stripe::CheckoutSessionBillingAddressCollection::Required
- }
- }
- }
-}
-
-impl From<StripeCustomerUpdateAddress> for stripe::CreateCheckoutSessionCustomerUpdateAddress {
- fn from(value: StripeCustomerUpdateAddress) -> Self {
- match value {
- StripeCustomerUpdateAddress::Auto => {
- stripe::CreateCheckoutSessionCustomerUpdateAddress::Auto
- }
- StripeCustomerUpdateAddress::Never => {
- stripe::CreateCheckoutSessionCustomerUpdateAddress::Never
- }
- }
- }
-}
-
-impl From<StripeCustomerUpdateName> for stripe::CreateCheckoutSessionCustomerUpdateName {
- fn from(value: StripeCustomerUpdateName) -> Self {
- match value {
- StripeCustomerUpdateName::Auto => stripe::CreateCheckoutSessionCustomerUpdateName::Auto,
- StripeCustomerUpdateName::Never => {
- stripe::CreateCheckoutSessionCustomerUpdateName::Never
- }
- }
- }
-}
-
-impl From<StripeCustomerUpdateShipping> for stripe::CreateCheckoutSessionCustomerUpdateShipping {
- fn from(value: StripeCustomerUpdateShipping) -> Self {
- match value {
- StripeCustomerUpdateShipping::Auto => {
- stripe::CreateCheckoutSessionCustomerUpdateShipping::Auto
- }
- StripeCustomerUpdateShipping::Never => {
- stripe::CreateCheckoutSessionCustomerUpdateShipping::Never
- }
- }
- }
-}
-
-impl From<StripeCustomerUpdate> for stripe::CreateCheckoutSessionCustomerUpdate {
- fn from(value: StripeCustomerUpdate) -> Self {
- stripe::CreateCheckoutSessionCustomerUpdate {
- address: value.address.map(Into::into),
- name: value.name.map(Into::into),
- shipping: value.shipping.map(Into::into),
- }
- }
-}
-
-impl From<StripeTaxIdCollection> for stripe::CreateCheckoutSessionTaxIdCollection {
- fn from(value: StripeTaxIdCollection) -> Self {
- stripe::CreateCheckoutSessionTaxIdCollection {
- enabled: value.enabled,
- }
- }
-}
@@ -6,9 +6,7 @@ use gpui::{Entity, TestAppContext};
mod channel_buffer_tests;
mod channel_guest_tests;
-mod channel_message_tests;
mod channel_tests;
-// mod debug_panel_tests;
mod editor_tests;
mod following_tests;
mod git_tests;
@@ -18,7 +16,6 @@ mod random_channel_buffer_tests;
mod random_project_collaboration_tests;
mod randomized_test_helpers;
mod remote_editing_collaboration_tests;
-mod stripe_billing_tests;
mod test_server;
use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
@@ -1,725 +0,0 @@
-use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
-use channel::{ChannelChat, ChannelMessageId, MessageParams};
-use collab_ui::chat_panel::ChatPanel;
-use gpui::{BackgroundExecutor, Entity, TestAppContext};
-use rpc::Notification;
-use workspace::dock::Panel;
-
-#[gpui::test]
-async fn test_basic_channel_messages(
- executor: BackgroundExecutor,
- mut cx_a: &mut TestAppContext,
- mut cx_b: &mut TestAppContext,
- mut cx_c: &mut TestAppContext,
-) {
- let mut server = TestServer::start(executor.clone()).await;
- let client_a = server.create_client(cx_a, "user_a").await;
- let client_b = server.create_client(cx_b, "user_b").await;
- let client_c = server.create_client(cx_c, "user_c").await;
-
- let channel_id = server
- .make_channel(
- "the-channel",
- None,
- (&client_a, cx_a),
- &mut [(&client_b, cx_b), (&client_c, cx_c)],
- )
- .await;
-
- let channel_chat_a = client_a
- .channel_store()
- .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
- let channel_chat_b = client_b
- .channel_store()
- .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
-
- let message_id = channel_chat_a
- .update(cx_a, |c, cx| {
- c.send_message(
- MessageParams {
- text: "hi @user_c!".into(),
- mentions: vec![(3..10, client_c.id())],
- reply_to_message_id: None,
- },
- cx,
- )
- .unwrap()
- })
- .await
- .unwrap();
- channel_chat_a
- .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
- .await
- .unwrap();
-
- executor.run_until_parked();
- channel_chat_b
- .update(cx_b, |c, cx| c.send_message("three".into(), cx).unwrap())
- .await
- .unwrap();
-
- executor.run_until_parked();
-
- let channel_chat_c = client_c
- .channel_store()
- .update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
-
- for (chat, cx) in [
- (&channel_chat_a, &mut cx_a),
- (&channel_chat_b, &mut cx_b),
- (&channel_chat_c, &mut cx_c),
- ] {
- chat.update(*cx, |c, _| {
- assert_eq!(
- c.messages()
- .iter()
- .map(|m| (m.body.as_str(), m.mentions.as_slice()))
- .collect::<Vec<_>>(),
- vec![
- ("hi @user_c!", [(3..10, client_c.id())].as_slice()),
- ("two", &[]),
- ("three", &[])
- ],
- "results for user {}",
- c.client().id(),
- );
- });
- }
-
- client_c.notification_store().update(cx_c, |store, _| {
- assert_eq!(store.notification_count(), 2);
- assert_eq!(store.unread_notification_count(), 1);
- assert_eq!(
- store.notification_at(0).unwrap().notification,
- Notification::ChannelMessageMention {
- message_id,
- sender_id: client_a.id(),
- channel_id: channel_id.0,
- }
- );
- assert_eq!(
- store.notification_at(1).unwrap().notification,
- Notification::ChannelInvitation {
- channel_id: channel_id.0,
- channel_name: "the-channel".to_string(),
- inviter_id: client_a.id()
- }
- );
- });
-}
-
-#[gpui::test]
-async fn test_rejoin_channel_chat(
- executor: BackgroundExecutor,
- cx_a: &mut TestAppContext,
- cx_b: &mut TestAppContext,
-) {
- let mut server = TestServer::start(executor.clone()).await;
- let client_a = server.create_client(cx_a, "user_a").await;
- let client_b = server.create_client(cx_b, "user_b").await;
-
- let channel_id = server
- .make_channel(
- "the-channel",
- None,
- (&client_a, cx_a),
- &mut [(&client_b, cx_b)],
- )
- .await;
-
- let channel_chat_a = client_a
- .channel_store()
- .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
- let channel_chat_b = client_b
- .channel_store()
- .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
-
- channel_chat_a
- .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
- .await
- .unwrap();
- channel_chat_b
- .update(cx_b, |c, cx| c.send_message("two".into(), cx).unwrap())
- .await
- .unwrap();
-
- server.forbid_connections();
- server.disconnect_client(client_a.peer_id().unwrap());
-
- // While client A is disconnected, clients A and B both send new messages.
- channel_chat_a
- .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
- .await
- .unwrap_err();
- channel_chat_a
- .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
- .await
- .unwrap_err();
- channel_chat_b
- .update(cx_b, |c, cx| c.send_message("five".into(), cx).unwrap())
- .await
- .unwrap();
- channel_chat_b
- .update(cx_b, |c, cx| c.send_message("six".into(), cx).unwrap())
- .await
- .unwrap();
-
- // Client A reconnects.
- server.allow_connections();
- executor.advance_clock(RECONNECT_TIMEOUT);
-
- // Client A fetches the messages that were sent while they were disconnected
- // and resends their own messages which failed to send.
- let expected_messages = &["one", "two", "five", "six", "three", "four"];
- assert_messages(&channel_chat_a, expected_messages, cx_a);
- assert_messages(&channel_chat_b, expected_messages, cx_b);
-}
-
-#[gpui::test]
-async fn test_remove_channel_message(
- executor: BackgroundExecutor,
- cx_a: &mut TestAppContext,
- cx_b: &mut TestAppContext,
- cx_c: &mut TestAppContext,
-) {
- let mut server = TestServer::start(executor.clone()).await;
- let client_a = server.create_client(cx_a, "user_a").await;
- let client_b = server.create_client(cx_b, "user_b").await;
- let client_c = server.create_client(cx_c, "user_c").await;
-
- let channel_id = server
- .make_channel(
- "the-channel",
- None,
- (&client_a, cx_a),
- &mut [(&client_b, cx_b), (&client_c, cx_c)],
- )
- .await;
-
- let channel_chat_a = client_a
- .channel_store()
- .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
- let channel_chat_b = client_b
- .channel_store()
- .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
-
- // Client A sends some messages.
- channel_chat_a
- .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
- .await
- .unwrap();
- let msg_id_2 = channel_chat_a
- .update(cx_a, |c, cx| {
- c.send_message(
- MessageParams {
- text: "two @user_b".to_string(),
- mentions: vec![(4..12, client_b.id())],
- reply_to_message_id: None,
- },
- cx,
- )
- .unwrap()
- })
- .await
- .unwrap();
- channel_chat_a
- .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
- .await
- .unwrap();
-
- // Clients A and B see all of the messages.
- executor.run_until_parked();
- let expected_messages = &["one", "two @user_b", "three"];
- assert_messages(&channel_chat_a, expected_messages, cx_a);
- assert_messages(&channel_chat_b, expected_messages, cx_b);
-
- // Ensure that client B received a notification for the mention.
- client_b.notification_store().read_with(cx_b, |store, _| {
- assert_eq!(store.notification_count(), 2);
- let entry = store.notification_at(0).unwrap();
- assert_eq!(
- entry.notification,
- Notification::ChannelMessageMention {
- message_id: msg_id_2,
- sender_id: client_a.id(),
- channel_id: channel_id.0,
- }
- );
- });
-
- // Client A deletes one of their messages.
- channel_chat_a
- .update(cx_a, |c, cx| {
- let ChannelMessageId::Saved(id) = c.message(1).id else {
- panic!("message not saved")
- };
- c.remove_message(id, cx)
- })
- .await
- .unwrap();
-
- // Client B sees that the message is gone.
- executor.run_until_parked();
- let expected_messages = &["one", "three"];
- assert_messages(&channel_chat_a, expected_messages, cx_a);
- assert_messages(&channel_chat_b, expected_messages, cx_b);
-
- // Client C joins the channel chat, and does not see the deleted message.
- let channel_chat_c = client_c
- .channel_store()
- .update(cx_c, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
- assert_messages(&channel_chat_c, expected_messages, cx_c);
-
- // Ensure we remove the notifications when the message is removed
- client_b.notification_store().read_with(cx_b, |store, _| {
- // First notification is the channel invitation, second would be the mention
- // notification, which should now be removed.
- assert_eq!(store.notification_count(), 1);
- });
-}
-
-#[track_caller]
-fn assert_messages(chat: &Entity<ChannelChat>, messages: &[&str], cx: &mut TestAppContext) {
- assert_eq!(
- chat.read_with(cx, |chat, _| {
- chat.messages()
- .iter()
- .map(|m| m.body.clone())
- .collect::<Vec<_>>()
- }),
- messages
- );
-}
-
-#[gpui::test]
-async fn test_channel_message_changes(
- executor: BackgroundExecutor,
- cx_a: &mut TestAppContext,
- cx_b: &mut TestAppContext,
-) {
- let mut server = TestServer::start(executor.clone()).await;
- let client_a = server.create_client(cx_a, "user_a").await;
- let client_b = server.create_client(cx_b, "user_b").await;
-
- let channel_id = server
- .make_channel(
- "the-channel",
- None,
- (&client_a, cx_a),
- &mut [(&client_b, cx_b)],
- )
- .await;
-
- // Client A sends a message, client B should see that there is a new message.
- let channel_chat_a = client_a
- .channel_store()
- .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
-
- channel_chat_a
- .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
- .await
- .unwrap();
-
- executor.run_until_parked();
-
- let b_has_messages = cx_b.update(|cx| {
- client_b
- .channel_store()
- .read(cx)
- .has_new_messages(channel_id)
- });
-
- assert!(b_has_messages);
-
- // Opening the chat should clear the changed flag.
- cx_b.update(|cx| {
- collab_ui::init(&client_b.app_state, cx);
- });
- let project_b = client_b.build_empty_local_project(cx_b);
- let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
-
- let chat_panel_b = workspace_b.update_in(cx_b, ChatPanel::new);
- chat_panel_b
- .update_in(cx_b, |chat_panel, window, cx| {
- chat_panel.set_active(true, window, cx);
- chat_panel.select_channel(channel_id, None, cx)
- })
- .await
- .unwrap();
-
- executor.run_until_parked();
-
- let b_has_messages = cx_b.update(|_, cx| {
- client_b
- .channel_store()
- .read(cx)
- .has_new_messages(channel_id)
- });
-
- assert!(!b_has_messages);
-
- // Sending a message while the chat is open should not change the flag.
- channel_chat_a
- .update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
- .await
- .unwrap();
-
- executor.run_until_parked();
-
- let b_has_messages = cx_b.update(|_, cx| {
- client_b
- .channel_store()
- .read(cx)
- .has_new_messages(channel_id)
- });
-
- assert!(!b_has_messages);
-
- // Sending a message while the chat is closed should change the flag.
- chat_panel_b.update_in(cx_b, |chat_panel, window, cx| {
- chat_panel.set_active(false, window, cx);
- });
-
- // Sending a message while the chat is open should not change the flag.
- channel_chat_a
- .update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
- .await
- .unwrap();
-
- executor.run_until_parked();
-
- let b_has_messages = cx_b.update(|_, cx| {
- client_b
- .channel_store()
- .read(cx)
- .has_new_messages(channel_id)
- });
-
- assert!(b_has_messages);
-
- // Closing the chat should re-enable change tracking
- cx_b.update(|_, _| drop(chat_panel_b));
-
- channel_chat_a
- .update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
- .await
- .unwrap();
-
- executor.run_until_parked();
-
- let b_has_messages = cx_b.update(|_, cx| {
- client_b
- .channel_store()
- .read(cx)
- .has_new_messages(channel_id)
- });
-
- assert!(b_has_messages);
-}
-
-#[gpui::test]
-async fn test_chat_replies(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
- let mut server = TestServer::start(cx_a.executor()).await;
- let client_a = server.create_client(cx_a, "user_a").await;
- let client_b = server.create_client(cx_b, "user_b").await;
-
- let channel_id = server
- .make_channel(
- "the-channel",
- None,
- (&client_a, cx_a),
- &mut [(&client_b, cx_b)],
- )
- .await;
-
- // Client A sends a message, client B should see that there is a new message.
- let channel_chat_a = client_a
- .channel_store()
- .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
-
- let channel_chat_b = client_b
- .channel_store()
- .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
-
- let msg_id = channel_chat_a
- .update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
- .await
- .unwrap();
-
- cx_a.run_until_parked();
-
- let reply_id = channel_chat_b
- .update(cx_b, |c, cx| {
- c.send_message(
- MessageParams {
- text: "reply".into(),
- reply_to_message_id: Some(msg_id),
- mentions: Vec::new(),
- },
- cx,
- )
- .unwrap()
- })
- .await
- .unwrap();
-
- cx_a.run_until_parked();
-
- channel_chat_a.update(cx_a, |channel_chat, _| {
- assert_eq!(
- channel_chat
- .find_loaded_message(reply_id)
- .unwrap()
- .reply_to_message_id,
- Some(msg_id),
- )
- });
-}
-
-#[gpui::test]
-async fn test_chat_editing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
- let mut server = TestServer::start(cx_a.executor()).await;
- let client_a = server.create_client(cx_a, "user_a").await;
- let client_b = server.create_client(cx_b, "user_b").await;
-
- let channel_id = server
- .make_channel(
- "the-channel",
- None,
- (&client_a, cx_a),
- &mut [(&client_b, cx_b)],
- )
- .await;
-
- // Client A sends a message, client B should see that there is a new message.
- let channel_chat_a = client_a
- .channel_store()
- .update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
-
- let channel_chat_b = client_b
- .channel_store()
- .update(cx_b, |store, cx| store.open_channel_chat(channel_id, cx))
- .await
- .unwrap();
-
- let msg_id = channel_chat_a
- .update(cx_a, |c, cx| {
- c.send_message(
- MessageParams {
- text: "Initial message".into(),
- reply_to_message_id: None,
- mentions: Vec::new(),
- },
- cx,
- )
- .unwrap()
- })
- .await
- .unwrap();
-
- cx_a.run_until_parked();
-
- channel_chat_a
- .update(cx_a, |c, cx| {
- c.update_message(
- msg_id,
- MessageParams {
- text: "Updated body".into(),
- reply_to_message_id: None,
- mentions: Vec::new(),
- },
- cx,
- )
- .unwrap()
- })
- .await
- .unwrap();
-
- cx_a.run_until_parked();
- cx_b.run_until_parked();
-
- channel_chat_a.update(cx_a, |channel_chat, _| {
- let update_message = channel_chat.find_loaded_message(msg_id).unwrap();
-
- assert_eq!(update_message.body, "Updated body");
- assert_eq!(update_message.mentions, Vec::new());
- });
- channel_chat_b.update(cx_b, |channel_chat, _| {
- let update_message = channel_chat.find_loaded_message(msg_id).unwrap();
-
- assert_eq!(update_message.body, "Updated body");
- assert_eq!(update_message.mentions, Vec::new());
- });
-
- // test mentions are updated correctly
-
- client_b.notification_store().read_with(cx_b, |store, _| {
- assert_eq!(store.notification_count(), 1);
- let entry = store.notification_at(0).unwrap();
- assert!(matches!(
- entry.notification,
- Notification::ChannelInvitation { .. }
- ),);
- });
-
- channel_chat_a
- .update(cx_a, |c, cx| {
- c.update_message(
- msg_id,
- MessageParams {
- text: "Updated body including a mention for @user_b".into(),
- reply_to_message_id: None,
- mentions: vec![(37..45, client_b.id())],
- },
- cx,
- )
- .unwrap()
- })
- .await
- .unwrap();
-
- cx_a.run_until_parked();
- cx_b.run_until_parked();
-
- channel_chat_a.update(cx_a, |channel_chat, _| {
- assert_eq!(
- channel_chat.find_loaded_message(msg_id).unwrap().body,
- "Updated body including a mention for @user_b",
- )
- });
- channel_chat_b.update(cx_b, |channel_chat, _| {
- assert_eq!(
- channel_chat.find_loaded_message(msg_id).unwrap().body,
- "Updated body including a mention for @user_b",
- )
- });
- client_b.notification_store().read_with(cx_b, |store, _| {
- assert_eq!(store.notification_count(), 2);
- let entry = store.notification_at(0).unwrap();
- assert_eq!(
- entry.notification,
- Notification::ChannelMessageMention {
- message_id: msg_id,
- sender_id: client_a.id(),
- channel_id: channel_id.0,
- }
- );
- });
-
- // Test update message and keep the mention and check that the body is updated correctly
-
- channel_chat_a
- .update(cx_a, |c, cx| {
- c.update_message(
- msg_id,
- MessageParams {
- text: "Updated body v2 including a mention for @user_b".into(),
- reply_to_message_id: None,
- mentions: vec![(37..45, client_b.id())],
- },
- cx,
- )
- .unwrap()
- })
- .await
- .unwrap();
-
- cx_a.run_until_parked();
- cx_b.run_until_parked();
-
- channel_chat_a.update(cx_a, |channel_chat, _| {
- assert_eq!(
- channel_chat.find_loaded_message(msg_id).unwrap().body,
- "Updated body v2 including a mention for @user_b",
- )
- });
- channel_chat_b.update(cx_b, |channel_chat, _| {
- assert_eq!(
- channel_chat.find_loaded_message(msg_id).unwrap().body,
- "Updated body v2 including a mention for @user_b",
- )
- });
-
- client_b.notification_store().read_with(cx_b, |store, _| {
- let message = store.channel_message_for_id(msg_id);
- assert!(message.is_some());
- assert_eq!(
- message.unwrap().body,
- "Updated body v2 including a mention for @user_b"
- );
- assert_eq!(store.notification_count(), 2);
- let entry = store.notification_at(0).unwrap();
- assert_eq!(
- entry.notification,
- Notification::ChannelMessageMention {
- message_id: msg_id,
- sender_id: client_a.id(),
- channel_id: channel_id.0,
- }
- );
- });
-
- // If we remove a mention from a message the corresponding mention notification
- // should also be removed.
-
- channel_chat_a
- .update(cx_a, |c, cx| {
- c.update_message(
- msg_id,
- MessageParams {
- text: "Updated body without a mention".into(),
- reply_to_message_id: None,
- mentions: vec![],
- },
- cx,
- )
- .unwrap()
- })
- .await
- .unwrap();
-
- cx_a.run_until_parked();
- cx_b.run_until_parked();
-
- channel_chat_a.update(cx_a, |channel_chat, _| {
- assert_eq!(
- channel_chat.find_loaded_message(msg_id).unwrap().body,
- "Updated body without a mention",
- )
- });
- channel_chat_b.update(cx_b, |channel_chat, _| {
- assert_eq!(
- channel_chat.find_loaded_message(msg_id).unwrap().body,
- "Updated body without a mention",
- )
- });
- client_b.notification_store().read_with(cx_b, |store, _| {
- // First notification is the channel invitation, second would be the mention
- // notification, which should now be removed.
- assert_eq!(store.notification_count(), 1);
- });
-}
@@ -15,13 +15,14 @@ use editor::{
},
};
use fs::Fs;
-use futures::{StreamExt, lock::Mutex};
+use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
use indoc::indoc;
use language::{
FakeLspAdapter,
language_settings::{AllLanguageSettings, InlayHintSettings},
};
+use lsp::LSP_REQUEST_TIMEOUT;
use project::{
ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
@@ -368,7 +369,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -487,7 +488,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -614,7 +615,7 @@ async fn test_collaborating_with_code_actions(
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.range.start, lsp::Position::new(0, 0));
assert_eq!(params.range.end, lsp::Position::new(0, 0));
@@ -636,7 +637,7 @@ async fn test_collaborating_with_code_actions(
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.range.start, lsp::Position::new(1, 31));
assert_eq!(params.range.end, lsp::Position::new(1, 31));
@@ -648,7 +649,7 @@ async fn test_collaborating_with_code_actions(
changes: Some(
[
(
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(1, 22),
@@ -658,7 +659,7 @@ async fn test_collaborating_with_code_actions(
)],
),
(
- lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(0, 0),
@@ -720,7 +721,7 @@ async fn test_collaborating_with_code_actions(
changes: Some(
[
(
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(1, 22),
@@ -730,7 +731,7 @@ async fn test_collaborating_with_code_actions(
)],
),
(
- lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(0, 0),
@@ -948,14 +949,14 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
changes: Some(
[
(
- lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/dir/one.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
"THREE".to_string(),
)],
),
(
- lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/dir/two.rs")).unwrap(),
vec![
lsp::TextEdit::new(
lsp::Range::new(
@@ -1017,6 +1018,211 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
})
}
+#[gpui::test]
+async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
+ let mut server = TestServer::start(cx_a.executor()).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ cx_b.update(editor::init);
+
+ let command_name = "test_command";
+ let capabilities = lsp::ServerCapabilities {
+ code_lens_provider: Some(lsp::CodeLensOptions {
+ resolve_provider: None,
+ }),
+ execute_command_provider: Some(lsp::ExecuteCommandOptions {
+ commands: vec![command_name.to_string()],
+ ..lsp::ExecuteCommandOptions::default()
+ }),
+ ..lsp::ServerCapabilities::default()
+ };
+ client_a.language_registry().add(rust_lang());
+ let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
+ "Rust",
+ FakeLspAdapter {
+ capabilities: capabilities.clone(),
+ ..FakeLspAdapter::default()
+ },
+ );
+ client_b.language_registry().add(rust_lang());
+ client_b.language_registry().register_fake_lsp_adapter(
+ "Rust",
+ FakeLspAdapter {
+ capabilities,
+ ..FakeLspAdapter::default()
+ },
+ );
+
+ client_a
+ .fs()
+ .insert_tree(
+ path!("/dir"),
+ json!({
+ "one.rs": "const ONE: usize = 1;"
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.join_remote_project(project_id, cx_b).await;
+
+ let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
+ let editor_b = workspace_b
+ .update_in(cx_b, |workspace, window, cx| {
+ workspace.open_path((worktree_id, "one.rs"), None, true, window, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| {
+ let lsp_store = editor.project().unwrap().read(cx).lsp_store();
+ let buffer = editor.buffer().read(cx).as_singleton().unwrap();
+ (lsp_store, buffer)
+ });
+ let fake_language_server = fake_language_servers.next().await.unwrap();
+ cx_a.run_until_parked();
+ cx_b.run_until_parked();
+
+ let long_request_time = LSP_REQUEST_TIMEOUT / 2;
+ let (request_started_tx, mut request_started_rx) = mpsc::unbounded();
+ let requests_started = Arc::new(AtomicUsize::new(0));
+ let requests_completed = Arc::new(AtomicUsize::new(0));
+ let _lens_requests = fake_language_server
+ .set_request_handler::<lsp::request::CodeLensRequest, _, _>({
+ let request_started_tx = request_started_tx.clone();
+ let requests_started = requests_started.clone();
+ let requests_completed = requests_completed.clone();
+ move |params, cx| {
+ let mut request_started_tx = request_started_tx.clone();
+ let requests_started = requests_started.clone();
+ let requests_completed = requests_completed.clone();
+ async move {
+ assert_eq!(
+ params.text_document.uri.as_str(),
+ uri!("file:///dir/one.rs")
+ );
+ requests_started.fetch_add(1, atomic::Ordering::Release);
+ request_started_tx.send(()).await.unwrap();
+ cx.background_executor().timer(long_request_time).await;
+ let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1;
+ Ok(Some(vec![lsp::CodeLens {
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)),
+ command: Some(lsp::Command {
+ title: format!("LSP Command {i}"),
+ command: command_name.to_string(),
+ arguments: None,
+ }),
+ data: None,
+ }]))
+ }
+ }
+ });
+
+ // Move cursor to a location, this should trigger the code lens call.
+ editor_b.update_in(cx_b, |editor, window, cx| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([7..7])
+ });
+ });
+ let () = request_started_rx.next().await.unwrap();
+ assert_eq!(
+ requests_started.load(atomic::Ordering::Acquire),
+ 1,
+ "Selection change should have initiated the first request"
+ );
+ assert_eq!(
+ requests_completed.load(atomic::Ordering::Acquire),
+ 0,
+ "Slow requests should be running still"
+ );
+ let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
+ lsp_store
+ .forget_code_lens_task(buffer_b.read(cx).remote_id())
+ .expect("Should have the fetch task started")
+ });
+
+ editor_b.update_in(cx_b, |editor, window, cx| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([1..1])
+ });
+ });
+ let () = request_started_rx.next().await.unwrap();
+ assert_eq!(
+ requests_started.load(atomic::Ordering::Acquire),
+ 2,
+ "Selection change should have initiated the second request"
+ );
+ assert_eq!(
+ requests_completed.load(atomic::Ordering::Acquire),
+ 0,
+ "Slow requests should be running still"
+ );
+ let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
+ lsp_store
+ .forget_code_lens_task(buffer_b.read(cx).remote_id())
+ .expect("Should have the fetch task started for the 2nd time")
+ });
+
+ editor_b.update_in(cx_b, |editor, window, cx| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([2..2])
+ });
+ });
+ let () = request_started_rx.next().await.unwrap();
+ assert_eq!(
+ requests_started.load(atomic::Ordering::Acquire),
+ 3,
+ "Selection change should have initiated the third request"
+ );
+ assert_eq!(
+ requests_completed.load(atomic::Ordering::Acquire),
+ 0,
+ "Slow requests should be running still"
+ );
+
+ _first_task.await.unwrap();
+ _second_task.await.unwrap();
+ cx_b.run_until_parked();
+ assert_eq!(
+ requests_started.load(atomic::Ordering::Acquire),
+ 3,
+ "No selection changes should trigger no more code lens requests"
+ );
+ assert_eq!(
+ requests_completed.load(atomic::Ordering::Acquire),
+ 3,
+ "After enough time, all 3 LSP requests should have been served by the language server"
+ );
+ let resulting_lens_actions = editor_b
+ .update(cx_b, |editor, cx| {
+ let lsp_store = editor.project().unwrap().read(cx).lsp_store();
+ lsp_store.update(cx, |lsp_store, cx| {
+ lsp_store.code_lens_actions(&buffer_b, cx)
+ })
+ })
+ .await
+ .unwrap()
+ .unwrap();
+ assert_eq!(
+ resulting_lens_actions.len(),
+ 1,
+ "Should have fetched one code lens action, but got: {resulting_lens_actions:?}"
+ );
+ assert_eq!(
+ resulting_lens_actions.first().unwrap().lsp_action.title(),
+ "LSP Command 3",
+ "Only the final code lens action should be in the data"
+ )
+}
+
#[gpui::test(iterations = 10)]
async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
let mut server = TestServer::start(cx_a.executor()).await;
@@ -1368,7 +1574,7 @@ async fn test_on_input_format_from_host_to_guest(
|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -1511,7 +1717,7 @@ async fn test_on_input_format_from_guest_to_host(
.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -1695,7 +1901,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
Ok(Some(vec![lsp::InlayHint {
@@ -1945,7 +2151,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
let character = if other_hints { 0 } else { 2 };
@@ -2126,7 +2332,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
requests_made.fetch_add(1, atomic::Ordering::Release);
Ok(vec![lsp::ColorInformation {
@@ -2415,11 +2621,11 @@ async fn test_lsp_pull_diagnostics(
let requests_made = closure_diagnostics_pulls_made.clone();
let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone();
async move {
- let message = if lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
+ let message = if lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
== params.text_document.uri
{
expected_pull_diagnostic_main_message.to_string()
- } else if lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap()
+ } else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
== params.text_document.uri
{
expected_pull_diagnostic_lib_message.to_string()
@@ -2511,7 +2717,7 @@ async fn test_lsp_pull_diagnostics(
items: vec![
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
- uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -2540,7 +2746,7 @@ async fn test_lsp_pull_diagnostics(
),
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
- uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -2615,7 +2821,7 @@ async fn test_lsp_pull_diagnostics(
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range {
start: lsp::Position {
@@ -2636,7 +2842,7 @@ async fn test_lsp_pull_diagnostics(
);
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range {
start: lsp::Position {
@@ -2664,7 +2870,7 @@ async fn test_lsp_pull_diagnostics(
items: vec![
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
- uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -2696,7 +2902,7 @@ async fn test_lsp_pull_diagnostics(
),
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
- uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -2845,7 +3051,7 @@ async fn test_lsp_pull_diagnostics(
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
- uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
version: None,
full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
result_id: Some(format!(
@@ -2908,7 +3114,7 @@ async fn test_lsp_pull_diagnostics(
{
assert!(
- diagnostics_pulls_result_ids.lock().await.len() > 0,
+ !diagnostics_pulls_result_ids.lock().await.is_empty(),
"Initial diagnostics pulls should report None at least"
);
assert_eq!(
@@ -3219,16 +3425,16 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
assert_eq!(
entries,
vec![
- Some(blame_entry("1b1b1b", 0..1)),
- Some(blame_entry("0d0d0d", 1..2)),
- Some(blame_entry("3a3a3a", 2..3)),
- Some(blame_entry("4c4c4c", 3..4)),
+ Some((buffer_id_b, blame_entry("1b1b1b", 0..1))),
+ Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
+ Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
+ Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
]
);
blame.update(cx, |blame, _| {
- for (idx, entry) in entries.iter().flatten().enumerate() {
- let details = blame.details_for_entry(entry).unwrap();
+ for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
+ let details = blame.details_for_entry(*buffer, entry).unwrap();
assert_eq!(details.message, format!("message for idx-{}", idx));
assert_eq!(
details.permalink.unwrap().to_string(),
@@ -3268,9 +3474,9 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
entries,
vec![
None,
- Some(blame_entry("0d0d0d", 1..2)),
- Some(blame_entry("3a3a3a", 2..3)),
- Some(blame_entry("4c4c4c", 3..4)),
+ Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
+ Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
+ Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
]
);
});
@@ -3305,8 +3511,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
vec![
None,
None,
- Some(blame_entry("3a3a3a", 2..3)),
- Some(blame_entry("4c4c4c", 3..4)),
+ Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
+ Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
]
);
});
@@ -3593,7 +3799,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
let abs_path = project_a.read_with(cx_a, |project, cx| {
project
.absolute_path(&project_path, cx)
- .map(|path_buf| Arc::from(path_buf.to_owned()))
+ .map(Arc::from)
.unwrap()
});
@@ -3647,20 +3853,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
editor
.breakpoint_store()
- .clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
- .clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
editor
.breakpoint_store()
- .clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
- .clone()
});
assert_eq!(1, breakpoints_a.len());
@@ -3680,20 +3882,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
editor
.breakpoint_store()
- .clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
- .clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
editor
.breakpoint_store()
- .clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
- .clone()
});
assert_eq!(1, breakpoints_a.len());
@@ -3713,20 +3911,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
editor
.breakpoint_store()
- .clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
- .clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
editor
.breakpoint_store()
- .clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
- .clone()
});
assert_eq!(1, breakpoints_a.len());
@@ -3746,20 +3940,16 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
editor
.breakpoint_store()
- .clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
- .clone()
});
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
editor
.breakpoint_store()
- .clone()
.unwrap()
.read(cx)
.all_source_breakpoints(cx)
- .clone()
});
assert_eq!(0, breakpoints_a.len());
@@ -3850,7 +4040,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|params, _| async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.position, lsp::Position::new(0, 0));
Ok(Some(ExpandedMacro {
@@ -3885,7 +4075,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|params, _| async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.position,
@@ -970,7 +970,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// the follow.
workspace_b.update_in(cx_b, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
- pane.activate_prev_item(true, window, cx);
+ pane.activate_previous_item(&Default::default(), window, cx);
});
});
executor.run_until_parked();
@@ -1073,7 +1073,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
// Client A cycles through some tabs.
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
- pane.activate_prev_item(true, window, cx);
+ pane.activate_previous_item(&Default::default(), window, cx);
});
});
executor.run_until_parked();
@@ -1117,7 +1117,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
- pane.activate_prev_item(true, window, cx);
+ pane.activate_previous_item(&Default::default(), window, cx);
});
});
executor.run_until_parked();
@@ -1164,7 +1164,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
workspace_a.update_in(cx_a, |workspace, window, cx| {
workspace.active_pane().update(cx, |pane, cx| {
- pane.activate_prev_item(true, window, cx);
+ pane.activate_previous_item(&Default::default(), window, cx);
});
});
executor.run_until_parked();
@@ -2098,7 +2098,7 @@ async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut
share_workspace(&workspace, cx_a).await.unwrap();
let buffer = workspace.update(cx_a, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
- project.create_local_buffer(&sample_text(26, 5, 'a'), None, cx)
+ project.create_local_buffer(&sample_text(26, 5, 'a'), None, false, cx)
})
});
let multibuffer = cx_a.new(|cx| {
@@ -2506,7 +2506,7 @@ async fn test_propagate_saves_and_fs_changes(
});
let new_buffer_a = project_a
- .update(cx_a, |p, cx| p.create_buffer(cx))
+ .update(cx_a, |p, cx| p.create_buffer(false, cx))
.await
.unwrap();
@@ -3208,7 +3208,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
- .to_included()
+ .into_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -3237,7 +3237,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
- .to_included()
+ .into_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -3266,7 +3266,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
- .to_included()
+ .into_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -3295,7 +3295,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
- .to_included()
+ .into_included()
.unwrap();
project_b
@@ -3304,7 +3304,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
- .to_included()
+ .into_included()
.unwrap();
project_b
@@ -3313,7 +3313,7 @@ async fn test_fs_operations(
})
.await
.unwrap()
- .to_included()
+ .into_included()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -4075,7 +4075,7 @@ async fn test_collaborating_with_diagnostics(
.await;
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -4095,7 +4095,7 @@ async fn test_collaborating_with_diagnostics(
.unwrap();
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::ERROR),
@@ -4169,7 +4169,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting more errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
@@ -4265,7 +4265,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting no errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: Vec::new(),
},
@@ -4372,7 +4372,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
for file_name in file_names {
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
+ uri: lsp::Uri::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -4838,7 +4838,7 @@ async fn test_definition(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
- lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
),
)))
@@ -4850,6 +4850,7 @@ async fn test_definition(
let definitions_1 = project_b
.update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx))
.await
+ .unwrap()
.unwrap();
cx_b.read(|cx| {
assert_eq!(
@@ -4875,7 +4876,7 @@ async fn test_definition(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
- lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
),
)))
@@ -4885,6 +4886,7 @@ async fn test_definition(
let definitions_2 = project_b
.update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx))
.await
+ .unwrap()
.unwrap();
cx_b.read(|cx| {
assert_eq!(definitions_2.len(), 1);
@@ -4912,7 +4914,7 @@ async fn test_definition(
);
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
- lsp::Url::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
),
)))
@@ -4922,6 +4924,7 @@ async fn test_definition(
let type_definitions = project_b
.update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx))
.await
+ .unwrap()
.unwrap();
cx_b.read(|cx| {
assert_eq!(
@@ -4970,7 +4973,7 @@ async fn test_references(
"Rust",
FakeLspAdapter {
name: "my-fake-lsp-adapter",
- capabilities: capabilities,
+ capabilities,
..FakeLspAdapter::default()
},
);
@@ -5046,21 +5049,21 @@ async fn test_references(
lsp_response_tx
.unbounded_send(Ok(Some(vec![
lsp::Location {
- uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
},
lsp::Location {
- uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
},
lsp::Location {
- uri: lsp::Url::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
},
])))
.unwrap();
- let references = references.await.unwrap();
+ let references = references.await.unwrap().unwrap();
executor.run_until_parked();
project_b.read_with(cx_b, |project, cx| {
// User is informed that a request is no longer pending.
@@ -5104,7 +5107,7 @@ async fn test_references(
lsp_response_tx
.unbounded_send(Err(anyhow!("can't find references")))
.unwrap();
- assert_eq!(references.await.unwrap(), []);
+ assert_eq!(references.await.unwrap().unwrap(), []);
// User is informed that the request is no longer pending.
executor.run_until_parked();
@@ -5505,7 +5508,8 @@ async fn test_lsp_hover(
// Request hover information as the guest.
let mut hovers = project_b
.update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
- .await;
+ .await
+ .unwrap();
assert_eq!(
hovers.len(),
2,
@@ -5621,7 +5625,7 @@ async fn test_project_symbols(
lsp::SymbolInformation {
name: "TWO".into(),
location: lsp::Location {
- uri: lsp::Url::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
},
kind: lsp::SymbolKind::CONSTANT,
@@ -5733,7 +5737,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
- lsp::Url::from_file_path(path!("/root/b.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/root/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
),
)))
@@ -5742,7 +5746,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
let definitions;
let buffer_b2;
- if rng.r#gen() {
+ if rng.random() {
cx_a.run_until_parked();
cx_b.run_until_parked();
definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
@@ -5764,7 +5768,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
}
- let definitions = definitions.await.unwrap();
+ let definitions = definitions.await.unwrap().unwrap();
assert_eq!(
definitions.len(),
1,
@@ -84,7 +84,7 @@ impl RandomizedTest for RandomChannelBufferTest {
}
loop {
- match rng.gen_range(0..100_u32) {
+ match rng.random_range(0..100_u32) {
0..=29 => {
let channel_name = client.channel_store().read_with(cx, |store, cx| {
store.ordered_channels().find_map(|(_, channel)| {
@@ -266,7 +266,7 @@ impl RandomizedTest for RandomChannelBufferTest {
"client {user_id} has different text than client {prev_user_id} for channel {channel_name}",
);
} else {
- prev_text = Some((user_id, text.clone()));
+ prev_text = Some((user_id, text));
}
// Assert that all clients and the server agree about who is present in the
@@ -17,7 +17,7 @@ use project::{
DEFAULT_COMPLETION_CONTEXT, Project, ProjectPath, search::SearchQuery, search::SearchResult,
};
use rand::{
- distributions::{Alphanumeric, DistString},
+ distr::{self, SampleString},
prelude::*,
};
use serde::{Deserialize, Serialize};
@@ -168,19 +168,19 @@ impl RandomizedTest for ProjectCollaborationTest {
) -> ClientOperation {
let call = cx.read(ActiveCall::global);
loop {
- match rng.gen_range(0..100_u32) {
+ match rng.random_range(0..100_u32) {
// Mutate the call
0..=29 => {
// Respond to an incoming call
if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
- break if rng.gen_bool(0.7) {
+ break if rng.random_bool(0.7) {
ClientOperation::AcceptIncomingCall
} else {
ClientOperation::RejectIncomingCall
};
}
- match rng.gen_range(0..100_u32) {
+ match rng.random_range(0..100_u32) {
// Invite a contact to the current call
0..=70 => {
let available_contacts =
@@ -212,7 +212,7 @@ impl RandomizedTest for ProjectCollaborationTest {
}
// Mutate projects
- 30..=59 => match rng.gen_range(0..100_u32) {
+ 30..=59 => match rng.random_range(0..100_u32) {
// Open a new project
0..=70 => {
// Open a remote project
@@ -270,7 +270,7 @@ impl RandomizedTest for ProjectCollaborationTest {
}
// Mutate project worktrees
- 81.. => match rng.gen_range(0..100_u32) {
+ 81.. => match rng.random_range(0..100_u32) {
// Add a worktree to a local project
0..=50 => {
let Some(project) = client.local_projects().choose(rng).cloned() else {
@@ -279,7 +279,7 @@ impl RandomizedTest for ProjectCollaborationTest {
let project_root_name = root_name_for_project(&project, cx);
let mut paths = client.fs().paths(false);
paths.remove(0);
- let new_root_path = if paths.is_empty() || rng.r#gen() {
+ let new_root_path = if paths.is_empty() || rng.random() {
Path::new(path!("/")).join(plan.next_root_dir_name())
} else {
paths.choose(rng).unwrap().clone()
@@ -304,12 +304,12 @@ impl RandomizedTest for ProjectCollaborationTest {
let worktree = worktree.read(cx);
worktree.is_visible()
&& worktree.entries(false, 0).any(|e| e.is_file())
- && worktree.root_entry().map_or(false, |e| e.is_dir())
+ && worktree.root_entry().is_some_and(|e| e.is_dir())
})
.choose(rng)
});
let Some(worktree) = worktree else { continue };
- let is_dir = rng.r#gen::<bool>();
+ let is_dir = rng.random::<bool>();
let mut full_path =
worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
full_path.push(gen_file_name(rng));
@@ -334,7 +334,7 @@ impl RandomizedTest for ProjectCollaborationTest {
let project_root_name = root_name_for_project(&project, cx);
let is_local = project.read_with(cx, |project, _| project.is_local());
- match rng.gen_range(0..100_u32) {
+ match rng.random_range(0..100_u32) {
// Manipulate an existing buffer
0..=70 => {
let Some(buffer) = client
@@ -349,7 +349,7 @@ impl RandomizedTest for ProjectCollaborationTest {
let full_path = buffer
.read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
- match rng.gen_range(0..100_u32) {
+ match rng.random_range(0..100_u32) {
// Close the buffer
0..=15 => {
break ClientOperation::CloseBuffer {
@@ -360,7 +360,7 @@ impl RandomizedTest for ProjectCollaborationTest {
}
// Save the buffer
16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
- let detach = rng.gen_bool(0.3);
+ let detach = rng.random_bool(0.3);
break ClientOperation::SaveBuffer {
project_root_name,
is_local,
@@ -383,17 +383,17 @@ impl RandomizedTest for ProjectCollaborationTest {
_ => {
let offset = buffer.read_with(cx, |buffer, _| {
buffer.clip_offset(
- rng.gen_range(0..=buffer.len()),
+ rng.random_range(0..=buffer.len()),
language::Bias::Left,
)
});
- let detach = rng.r#gen();
+ let detach = rng.random();
break ClientOperation::RequestLspDataInBuffer {
project_root_name,
full_path,
offset,
is_local,
- kind: match rng.gen_range(0..5_u32) {
+ kind: match rng.random_range(0..5_u32) {
0 => LspRequestKind::Rename,
1 => LspRequestKind::Highlights,
2 => LspRequestKind::Definition,
@@ -407,8 +407,8 @@ impl RandomizedTest for ProjectCollaborationTest {
}
71..=80 => {
- let query = rng.gen_range('a'..='z').to_string();
- let detach = rng.gen_bool(0.3);
+ let query = rng.random_range('a'..='z').to_string();
+ let detach = rng.random_bool(0.3);
break ClientOperation::SearchProject {
project_root_name,
is_local,
@@ -460,7 +460,7 @@ impl RandomizedTest for ProjectCollaborationTest {
// Create or update a file or directory
96.. => {
- let is_dir = rng.r#gen::<bool>();
+ let is_dir = rng.random::<bool>();
let content;
let mut path;
let dir_paths = client.fs().directories(false);
@@ -470,11 +470,11 @@ impl RandomizedTest for ProjectCollaborationTest {
path = dir_paths.choose(rng).unwrap().clone();
path.push(gen_file_name(rng));
} else {
- content = Alphanumeric.sample_string(rng, 16);
+ content = distr::Alphanumeric.sample_string(rng, 16);
// Create a new file or overwrite an existing file
let file_paths = client.fs().files();
- if file_paths.is_empty() || rng.gen_bool(0.5) {
+ if file_paths.is_empty() || rng.random_bool(0.5) {
path = dir_paths.choose(rng).unwrap().clone();
path.push(gen_file_name(rng));
path.set_extension("rs");
@@ -643,7 +643,7 @@ impl RandomizedTest for ProjectCollaborationTest {
);
let project = project.await?;
- client.dev_server_projects_mut().push(project.clone());
+ client.dev_server_projects_mut().push(project);
}
ClientOperation::CreateWorktreeEntry {
@@ -1090,7 +1090,7 @@ impl RandomizedTest for ProjectCollaborationTest {
move |_, cx| {
let background = cx.background_executor();
let mut rng = background.rng();
- let count = rng.gen_range::<usize, _>(1..3);
+ let count = rng.random_range::<usize, _>(1..3);
let files = fs.as_fake().files();
let files = (0..count)
.map(|_| files.choose(&mut rng).unwrap().clone())
@@ -1101,7 +1101,7 @@ impl RandomizedTest for ProjectCollaborationTest {
files
.into_iter()
.map(|file| lsp::Location {
- uri: lsp::Url::from_file_path(file).unwrap(),
+ uri: lsp::Uri::from_file_path(file).unwrap(),
range: Default::default(),
})
.collect(),
@@ -1117,12 +1117,12 @@ impl RandomizedTest for ProjectCollaborationTest {
let background = cx.background_executor();
let mut rng = background.rng();
- let highlight_count = rng.gen_range(1..=5);
+ let highlight_count = rng.random_range(1..=5);
for _ in 0..highlight_count {
- let start_row = rng.gen_range(0..100);
- let start_column = rng.gen_range(0..100);
- let end_row = rng.gen_range(0..100);
- let end_column = rng.gen_range(0..100);
+ let start_row = rng.random_range(0..100);
+ let start_column = rng.random_range(0..100);
+ let end_row = rng.random_range(0..100);
+ let end_column = rng.random_range(0..100);
let start = PointUtf16::new(start_row, start_column);
let end = PointUtf16::new(end_row, end_column);
let range =
@@ -1162,8 +1162,8 @@ impl RandomizedTest for ProjectCollaborationTest {
Some((project, cx))
});
- if !guest_project.is_disconnected(cx) {
- if let Some((host_project, host_cx)) = host_project {
+ if !guest_project.is_disconnected(cx)
+ && let Some((host_project, host_cx)) = host_project {
let host_worktree_snapshots =
host_project.read_with(host_cx, |host_project, cx| {
host_project
@@ -1219,8 +1219,8 @@ impl RandomizedTest for ProjectCollaborationTest {
guest_project.remote_id(),
);
assert_eq!(
- guest_snapshot.entries(false, 0).collect::<Vec<_>>(),
- host_snapshot.entries(false, 0).collect::<Vec<_>>(),
+ guest_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
+ host_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
client.username,
host_snapshot.abs_path(),
@@ -1235,7 +1235,6 @@ impl RandomizedTest for ProjectCollaborationTest {
);
}
}
- }
for buffer in guest_project.opened_buffers(cx) {
let buffer = buffer.read(cx);
@@ -1249,6 +1248,18 @@ impl RandomizedTest for ProjectCollaborationTest {
);
}
});
+
+ // A hack to work around a hack in
+ // https://github.com/zed-industries/zed/pull/16696 that wasn't
+ // detected until we upgraded the rng crate. This whole crate is
+ // going away with DeltaDB soon, so we hold our nose and
+ // continue.
+ fn null_out_entry_size(entry: &project::Entry) -> project::Entry {
+ project::Entry {
+ size: 0,
+ ..entry.clone()
+ }
+ }
}
let buffers = client.buffers().clone();
@@ -1423,7 +1434,7 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
.filter(|path| path.starts_with(repo_path))
.collect::<Vec<_>>();
- let count = rng.gen_range(0..=paths.len());
+ let count = rng.random_range(0..=paths.len());
paths.shuffle(rng);
paths.truncate(count);
@@ -1435,13 +1446,13 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
- match rng.gen_range(0..100_u32) {
+ match rng.random_range(0..100_u32) {
0..=25 => {
let file_paths = generate_file_paths(&repo_path, rng, client);
let contents = file_paths
.into_iter()
- .map(|path| (path, Alphanumeric.sample_string(rng, 16)))
+ .map(|path| (path, distr::Alphanumeric.sample_string(rng, 16)))
.collect();
GitOperation::WriteGitIndex {
@@ -1450,7 +1461,8 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
}
}
26..=63 => {
- let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));
+ let new_branch =
+ (rng.random_range(0..10) > 3).then(|| distr::Alphanumeric.sample_string(rng, 8));
GitOperation::WriteGitBranch {
repo_path,
@@ -1597,7 +1609,7 @@ fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Entity
fn gen_file_name(rng: &mut StdRng) -> String {
let mut name = String::new();
for _ in 0..10 {
- let letter = rng.gen_range('a'..='z');
+ let letter = rng.random_range('a'..='z');
name.push(letter);
}
name
@@ -1605,7 +1617,7 @@ fn gen_file_name(rng: &mut StdRng) -> String {
fn gen_status(rng: &mut StdRng) -> FileStatus {
fn gen_tracked_status(rng: &mut StdRng) -> TrackedStatus {
- match rng.gen_range(0..3) {
+ match rng.random_range(0..3) {
0 => TrackedStatus {
index_status: StatusCode::Unmodified,
worktree_status: StatusCode::Unmodified,
@@ -1627,7 +1639,7 @@ fn gen_status(rng: &mut StdRng) -> FileStatus {
}
fn gen_unmerged_status_code(rng: &mut StdRng) -> UnmergedStatusCode {
- match rng.gen_range(0..3) {
+ match rng.random_range(0..3) {
0 => UnmergedStatusCode::Updated,
1 => UnmergedStatusCode::Added,
2 => UnmergedStatusCode::Deleted,
@@ -1635,7 +1647,7 @@ fn gen_status(rng: &mut StdRng) -> FileStatus {
}
}
- match rng.gen_range(0..2) {
+ match rng.random_range(0..2) {
0 => FileStatus::Unmerged(UnmergedStatus {
first_head: gen_unmerged_status_code(rng),
second_head: gen_unmerged_status_code(rng),
@@ -198,19 +198,19 @@ pub async fn run_randomized_test<T: RandomizedTest>(
}
pub fn save_randomized_test_plan() {
- if let Some(serialize_plan) = LAST_PLAN.lock().take() {
- if let Some(path) = plan_save_path() {
- eprintln!("saved test plan to path {:?}", path);
- std::fs::write(path, serialize_plan()).unwrap();
- }
+ if let Some(serialize_plan) = LAST_PLAN.lock().take()
+ && let Some(path) = plan_save_path()
+ {
+ eprintln!("saved test plan to path {:?}", path);
+ std::fs::write(path, serialize_plan()).unwrap();
}
}
impl<T: RandomizedTest> TestPlan<T> {
pub async fn new(server: &mut TestServer, mut rng: StdRng) -> Arc<Mutex<Self>> {
- let allow_server_restarts = rng.gen_bool(0.7);
- let allow_client_reconnection = rng.gen_bool(0.7);
- let allow_client_disconnection = rng.gen_bool(0.1);
+ let allow_server_restarts = rng.random_bool(0.7);
+ let allow_client_reconnection = rng.random_bool(0.7);
+ let allow_client_disconnection = rng.random_bool(0.1);
let mut users = Vec::new();
for ix in 0..max_peers() {
@@ -290,10 +290,9 @@ impl<T: RandomizedTest> TestPlan<T> {
if let StoredOperation::Client {
user_id, batch_id, ..
} = operation
+ && batch_id == current_batch_id
{
- if batch_id == current_batch_id {
- return Some(user_id);
- }
+ return Some(user_id);
}
None
}));
@@ -366,10 +365,9 @@ impl<T: RandomizedTest> TestPlan<T> {
},
applied,
) = stored_operation
+ && user_id == ¤t_user_id
{
- if user_id == ¤t_user_id {
- return Some((operation.clone(), applied.clone()));
- }
+ return Some((operation.clone(), applied.clone()));
}
}
None
@@ -409,7 +407,7 @@ impl<T: RandomizedTest> TestPlan<T> {
}
Some(loop {
- break match self.rng.gen_range(0..100) {
+ break match self.rng.random_range(0..100) {
0..=29 if clients.len() < self.users.len() => {
let user = self
.users
@@ -423,13 +421,13 @@ impl<T: RandomizedTest> TestPlan<T> {
}
}
30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
- let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
+ let (client, cx) = &clients[self.rng.random_range(0..clients.len())];
let user_id = client.current_user_id(cx);
self.operation_ix += 1;
ServerOperation::RemoveConnection { user_id }
}
35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
- let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
+ let (client, cx) = &clients[self.rng.random_range(0..clients.len())];
let user_id = client.current_user_id(cx);
self.operation_ix += 1;
ServerOperation::BounceConnection { user_id }
@@ -441,12 +439,12 @@ impl<T: RandomizedTest> TestPlan<T> {
_ if !clients.is_empty() => {
let count = self
.rng
- .gen_range(1..10)
+ .random_range(1..10)
.min(self.max_operations - self.operation_ix);
let batch_id = util::post_inc(&mut self.next_batch_id);
let mut user_ids = (0..count)
.map(|_| {
- let ix = self.rng.gen_range(0..clients.len());
+ let ix = self.rng.random_range(0..clients.len());
let (client, cx) = &clients[ix];
client.current_user_id(cx)
})
@@ -455,7 +453,7 @@ impl<T: RandomizedTest> TestPlan<T> {
ServerOperation::MutateClients {
user_ids,
batch_id,
- quiesce: self.rng.gen_bool(0.7),
+ quiesce: self.rng.random_bool(0.7),
}
}
_ => continue,
@@ -550,11 +548,11 @@ impl<T: RandomizedTest> TestPlan<T> {
.unwrap();
let pool = server.connection_pool.lock();
for contact in contacts {
- if let db::Contact::Accepted { user_id, busy, .. } = contact {
- if user_id == removed_user_id {
- assert!(!pool.is_user_online(user_id));
- assert!(!busy);
- }
+ if let db::Contact::Accepted { user_id, busy, .. } = contact
+ && user_id == removed_user_id
+ {
+ assert!(!pool.is_user_online(user_id));
+ assert!(!busy);
}
}
}
@@ -26,7 +26,7 @@ use project::{
debugger::session::ThreadId,
lsp_store::{FormatTrigger, LspFormatTarget},
};
-use remote::SshRemoteClient;
+use remote::RemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
use rpc::proto;
use serde_json::json;
@@ -59,7 +59,7 @@ async fn test_sharing_an_ssh_remote_project(
.await;
// Set up project on remote FS
- let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -101,7 +101,7 @@ async fn test_sharing_an_ssh_remote_project(
)
});
- let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
.await;
@@ -235,7 +235,7 @@ async fn test_ssh_collaboration_git_branches(
.await;
// Set up project on remote FS
- let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree("/project", serde_json::json!({ ".git":{} }))
@@ -268,7 +268,7 @@ async fn test_ssh_collaboration_git_branches(
)
});
- let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, _) = client_a
.build_ssh_project("/project", client_ssh, cx_a)
.await;
@@ -420,7 +420,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
- let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
let buffer_text = "let one = \"two\"";
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
@@ -473,7 +473,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
)
});
- let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project(path!("/project"), client_ssh, cx_a)
.await;
@@ -602,7 +602,7 @@ async fn test_remote_server_debugger(
release_channel::init(SemanticVersion::default(), cx);
dap_adapters::init(cx);
});
- let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -633,7 +633,7 @@ async fn test_remote_server_debugger(
)
});
- let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let mut server = TestServer::start(server_cx.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
cx_a.update(|cx| {
@@ -711,7 +711,7 @@ async fn test_slow_adapter_startup_retries(
release_channel::init(SemanticVersion::default(), cx);
dap_adapters::init(cx);
});
- let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
+ let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -742,7 +742,7 @@ async fn test_slow_adapter_startup_retries(
)
});
- let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
+ let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let mut server = TestServer::start(server_cx.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
cx_a.update(|cx| {
@@ -1,123 +0,0 @@
-use std::sync::Arc;
-
-use pretty_assertions::assert_eq;
-
-use crate::stripe_billing::StripeBilling;
-use crate::stripe_client::{FakeStripeClient, StripePrice, StripePriceId, StripePriceRecurring};
-
-fn make_stripe_billing() -> (StripeBilling, Arc<FakeStripeClient>) {
- let stripe_client = Arc::new(FakeStripeClient::new());
- let stripe_billing = StripeBilling::test(stripe_client.clone());
-
- (stripe_billing, stripe_client)
-}
-
-#[gpui::test]
-async fn test_initialize() {
- let (stripe_billing, stripe_client) = make_stripe_billing();
-
- // Add test prices
- let price1 = StripePrice {
- id: StripePriceId("price_1".into()),
- unit_amount: Some(1_000),
- lookup_key: Some("zed-pro".to_string()),
- recurring: None,
- };
- let price2 = StripePrice {
- id: StripePriceId("price_2".into()),
- unit_amount: Some(0),
- lookup_key: Some("zed-free".to_string()),
- recurring: None,
- };
- let price3 = StripePrice {
- id: StripePriceId("price_3".into()),
- unit_amount: Some(500),
- lookup_key: None,
- recurring: Some(StripePriceRecurring {
- meter: Some("meter_1".to_string()),
- }),
- };
- stripe_client
- .prices
- .lock()
- .insert(price1.id.clone(), price1);
- stripe_client
- .prices
- .lock()
- .insert(price2.id.clone(), price2);
- stripe_client
- .prices
- .lock()
- .insert(price3.id.clone(), price3);
-
- // Initialize the billing system
- stripe_billing.initialize().await.unwrap();
-
- // Verify that prices can be found by lookup key
- let zed_pro_price_id = stripe_billing.zed_pro_price_id().await.unwrap();
- assert_eq!(zed_pro_price_id.to_string(), "price_1");
-
- let zed_free_price_id = stripe_billing.zed_free_price_id().await.unwrap();
- assert_eq!(zed_free_price_id.to_string(), "price_2");
-
- // Verify that a price can be found by lookup key
- let zed_pro_price = stripe_billing
- .find_price_by_lookup_key("zed-pro")
- .await
- .unwrap();
- assert_eq!(zed_pro_price.id.to_string(), "price_1");
- assert_eq!(zed_pro_price.unit_amount, Some(1_000));
-
- // Verify that finding a non-existent lookup key returns an error
- let result = stripe_billing
- .find_price_by_lookup_key("non-existent")
- .await;
- assert!(result.is_err());
-}
-
-#[gpui::test]
-async fn test_find_or_create_customer_by_email() {
- let (stripe_billing, stripe_client) = make_stripe_billing();
-
- // Create a customer with an email that doesn't yet correspond to a customer.
- {
- let email = "user@example.com";
-
- let customer_id = stripe_billing
- .find_or_create_customer_by_email(Some(email))
- .await
- .unwrap();
-
- let customer = stripe_client
- .customers
- .lock()
- .get(&customer_id)
- .unwrap()
- .clone();
- assert_eq!(customer.email.as_deref(), Some(email));
- }
-
- // Create a customer with an email that corresponds to an existing customer.
- {
- let email = "user2@example.com";
-
- let existing_customer_id = stripe_billing
- .find_or_create_customer_by_email(Some(email))
- .await
- .unwrap();
-
- let customer_id = stripe_billing
- .find_or_create_customer_by_email(Some(email))
- .await
- .unwrap();
- assert_eq!(customer_id, existing_customer_id);
-
- let customer = stripe_client
- .customers
- .lock()
- .get(&customer_id)
- .unwrap()
- .clone();
- assert_eq!(customer.email.as_deref(), Some(email));
- }
-}
@@ -1,4 +1,3 @@
-use crate::stripe_client::FakeStripeClient;
use crate::{
AppState, Config,
db::{NewUserParams, UserId, tests::TestDb},
@@ -27,7 +26,7 @@ use node_runtime::NodeRuntime;
use notifications::NotificationStore;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
-use remote::SshRemoteClient;
+use remote::RemoteClient;
use rpc::{
RECEIVE_TIMEOUT,
proto::{self, ChannelRole},
@@ -371,8 +370,8 @@ impl TestServer {
let client = TestClient {
app_state,
username: name.to_string(),
- channel_store: cx.read(ChannelStore::global).clone(),
- notification_store: cx.read(NotificationStore::global).clone(),
+ channel_store: cx.read(ChannelStore::global),
+ notification_store: cx.read(NotificationStore::global),
state: Default::default(),
};
client.wait_for_current_user(cx).await;
@@ -566,12 +565,8 @@ impl TestServer {
) -> Arc<AppState> {
Arc::new(AppState {
db: test_db.db().clone(),
- llm_db: None,
livekit_client: Some(Arc::new(livekit_test_server.create_api_client())),
blob_store_client: None,
- real_stripe_client: None,
- stripe_client: Some(Arc::new(FakeStripeClient::new())),
- stripe_billing: None,
executor,
kinesis_client: None,
config: Config {
@@ -608,7 +603,6 @@ impl TestServer {
auto_join_channel_id: None,
migrations_path: None,
seed_path: None,
- stripe_api_key: None,
supermaven_admin_api_key: None,
user_backfiller_github_access_token: None,
kinesis_region: None,
@@ -771,11 +765,11 @@ impl TestClient {
pub async fn build_ssh_project(
&self,
root_path: impl AsRef<Path>,
- ssh: Entity<SshRemoteClient>,
+ ssh: Entity<RemoteClient>,
cx: &mut TestAppContext,
) -> (Entity<Project>, WorktreeId) {
let project = cx.update(|cx| {
- Project::ssh(
+ Project::remote(
ssh,
self.client().clone(),
self.app_state.node_runtime.clone(),
@@ -903,7 +897,7 @@ impl TestClient {
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
let entity = window.root(cx).unwrap();
- let cx = VisualTestContext::from_window(*window.deref(), cx).as_mut();
+ let cx = VisualTestContext::from_window(*window.deref(), cx).into_mut();
// it might be nice to try and cleanup these at the end of each test.
(entity, cx)
}
@@ -130,17 +130,17 @@ impl UserBackfiller {
.and_then(|value| value.parse::<i64>().ok())
.and_then(|value| DateTime::from_timestamp(value, 0));
- if rate_limit_remaining == Some(0) {
- if let Some(reset_at) = rate_limit_reset {
- let now = Utc::now();
- if reset_at > now {
- let sleep_duration = reset_at - now;
- log::info!(
- "rate limit reached. Sleeping for {} seconds",
- sleep_duration.num_seconds()
- );
- self.executor.sleep(sleep_duration.to_std().unwrap()).await;
- }
+ if rate_limit_remaining == Some(0)
+ && let Some(reset_at) = rate_limit_reset
+ {
+ let now = Utc::now();
+ if reset_at > now {
+ let sleep_duration = reset_at - now;
+ log::info!(
+ "rate limit reached. Sleeping for {} seconds",
+ sleep_duration.num_seconds()
+ );
+ self.executor.sleep(sleep_duration.to_std().unwrap()).await;
}
}
@@ -37,18 +37,15 @@ client.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
-emojis.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
-language.workspace = true
log.workspace = true
menu.workspace = true
notifications.workspace = true
picker.workspace = true
project.workspace = true
release_channel.workspace = true
-rich_text.workspace = true
rpc.workspace = true
schemars.workspace = true
serde.workspace = true
@@ -66,7 +66,7 @@ impl ChannelView {
channel_id,
link_position,
pane.clone(),
- workspace.clone(),
+ workspace,
window,
cx,
);
@@ -107,43 +107,32 @@ impl ChannelView {
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
// If this channel buffer is already open in this pane, just return it.
- if let Some(existing_view) = existing_view.clone() {
- if existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
- {
- if let Some(link_position) = link_position {
- existing_view.update(cx, |channel_view, cx| {
- channel_view.focus_position_from_link(
- link_position,
- true,
- window,
- cx,
- )
- });
- }
- return existing_view;
+ if let Some(existing_view) = existing_view.clone()
+ && existing_view.read(cx).channel_buffer == channel_view.read(cx).channel_buffer
+ {
+ if let Some(link_position) = link_position {
+ existing_view.update(cx, |channel_view, cx| {
+ channel_view.focus_position_from_link(link_position, true, window, cx)
+ });
}
+ return existing_view;
}
// If the pane contained a disconnected view for this channel buffer,
// replace that.
- if let Some(existing_item) = existing_view {
- if let Some(ix) = pane.index_for_item(&existing_item) {
- pane.close_item_by_id(
- existing_item.entity_id(),
- SaveIntent::Skip,
- window,
- cx,
- )
+ if let Some(existing_item) = existing_view
+ && let Some(ix) = pane.index_for_item(&existing_item)
+ {
+ pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, window, cx)
.detach();
- pane.add_item(
- Box::new(channel_view.clone()),
- true,
- true,
- Some(ix),
- window,
- cx,
- );
- }
+ pane.add_item(
+ Box::new(channel_view.clone()),
+ true,
+ true,
+ Some(ix),
+ window,
+ cx,
+ );
}
if let Some(link_position) = link_position {
@@ -259,26 +248,21 @@ impl ChannelView {
.editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
- if let Some(outline) = snapshot.buffer_snapshot.outline(None) {
- if let Some(item) = outline
+ if let Some(outline) = snapshot.buffer_snapshot.outline(None)
+ && let Some(item) = outline
.items
.iter()
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
- {
- self.editor.update(cx, |editor, cx| {
- editor.change_selections(
- SelectionEffects::scroll(Autoscroll::focused()),
- window,
- cx,
- |s| {
- s.replace_cursors_with(|map| {
- vec![item.range.start.to_display_point(map)]
- })
- },
- )
- });
- return;
- }
+ {
+ self.editor.update(cx, |editor, cx| {
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::focused()),
+ window,
+ cx,
+ |s| s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)]),
+ )
+ });
+ return;
}
if !first_attempt {
@@ -1,1381 +0,0 @@
-use crate::{ChatPanelButton, ChatPanelSettings, collab_panel};
-use anyhow::Result;
-use call::{ActiveCall, room};
-use channel::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId, ChannelStore};
-use client::{ChannelId, Client};
-use collections::HashMap;
-use db::kvp::KEY_VALUE_STORE;
-use editor::{Editor, actions};
-use gpui::{
- Action, App, AsyncWindowContext, ClipboardItem, Context, CursorStyle, DismissEvent, ElementId,
- Entity, EventEmitter, FocusHandle, Focusable, FontWeight, HighlightStyle, ListOffset,
- ListScrollEvent, ListState, Render, Stateful, Subscription, Task, WeakEntity, Window, actions,
- div, list, prelude::*, px,
-};
-use language::LanguageRegistry;
-use menu::Confirm;
-use message_editor::MessageEditor;
-use project::Fs;
-use rich_text::{Highlight, RichText};
-use serde::{Deserialize, Serialize};
-use settings::Settings;
-use std::{sync::Arc, time::Duration};
-use time::{OffsetDateTime, UtcOffset};
-use ui::{
- Avatar, Button, ContextMenu, IconButton, IconName, KeyBinding, Label, PopoverMenu, Tab, TabBar,
- Tooltip, prelude::*,
-};
-use util::{ResultExt, TryFutureExt};
-use workspace::{
- Workspace,
- dock::{DockPosition, Panel, PanelEvent},
-};
-
-mod message_editor;
-
-const MESSAGE_LOADING_THRESHOLD: usize = 50;
-const CHAT_PANEL_KEY: &str = "ChatPanel";
-
-pub fn init(cx: &mut App) {
- cx.observe_new(|workspace: &mut Workspace, _, _| {
- workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
- workspace.toggle_panel_focus::<ChatPanel>(window, cx);
- });
- })
- .detach();
-}
-
-pub struct ChatPanel {
- client: Arc<Client>,
- channel_store: Entity<ChannelStore>,
- languages: Arc<LanguageRegistry>,
- message_list: ListState,
- active_chat: Option<(Entity<ChannelChat>, Subscription)>,
- message_editor: Entity<MessageEditor>,
- local_timezone: UtcOffset,
- fs: Arc<dyn Fs>,
- width: Option<Pixels>,
- active: bool,
- pending_serialization: Task<Option<()>>,
- subscriptions: Vec<gpui::Subscription>,
- is_scrolled_to_bottom: bool,
- markdown_data: HashMap<ChannelMessageId, RichText>,
- focus_handle: FocusHandle,
- open_context_menu: Option<(u64, Subscription)>,
- highlighted_message: Option<(u64, Task<()>)>,
- last_acknowledged_message_id: Option<u64>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedChatPanel {
- width: Option<Pixels>,
-}
-
-actions!(
- chat_panel,
- [
- /// Toggles focus on the chat panel.
- ToggleFocus
- ]
-);
-
-impl ChatPanel {
- pub fn new(
- workspace: &mut Workspace,
- window: &mut Window,
- cx: &mut Context<Workspace>,
- ) -> Entity<Self> {
- let fs = workspace.app_state().fs.clone();
- let client = workspace.app_state().client.clone();
- let channel_store = ChannelStore::global(cx);
- let user_store = workspace.app_state().user_store.clone();
- let languages = workspace.app_state().languages.clone();
-
- let input_editor = cx.new(|cx| {
- MessageEditor::new(
- languages.clone(),
- user_store.clone(),
- None,
- cx.new(|cx| Editor::auto_height(1, 4, window, cx)),
- window,
- cx,
- )
- });
-
- cx.new(|cx| {
- let message_list = ListState::new(0, gpui::ListAlignment::Bottom, px(1000.));
-
- message_list.set_scroll_handler(cx.listener(
- |this: &mut Self, event: &ListScrollEvent, _, cx| {
- if event.visible_range.start < MESSAGE_LOADING_THRESHOLD {
- this.load_more_messages(cx);
- }
- this.is_scrolled_to_bottom = !event.is_scrolled;
- },
- ));
-
- let local_offset = chrono::Local::now().offset().local_minus_utc();
- let mut this = Self {
- fs,
- client,
- channel_store,
- languages,
- message_list,
- active_chat: Default::default(),
- pending_serialization: Task::ready(None),
- message_editor: input_editor,
- local_timezone: UtcOffset::from_whole_seconds(local_offset).unwrap(),
- subscriptions: Vec::new(),
- is_scrolled_to_bottom: true,
- active: false,
- width: None,
- markdown_data: Default::default(),
- focus_handle: cx.focus_handle(),
- open_context_menu: None,
- highlighted_message: None,
- last_acknowledged_message_id: None,
- };
-
- if let Some(channel_id) = ActiveCall::global(cx)
- .read(cx)
- .room()
- .and_then(|room| room.read(cx).channel_id())
- {
- this.select_channel(channel_id, None, cx)
- .detach_and_log_err(cx);
- }
-
- this.subscriptions.push(cx.subscribe(
- &ActiveCall::global(cx),
- move |this: &mut Self, call, event: &room::Event, cx| match event {
- room::Event::RoomJoined { channel_id } => {
- if let Some(channel_id) = channel_id {
- this.select_channel(*channel_id, None, cx)
- .detach_and_log_err(cx);
-
- if call
- .read(cx)
- .room()
- .is_some_and(|room| room.read(cx).contains_guests())
- {
- cx.emit(PanelEvent::Activate)
- }
- }
- }
- room::Event::RoomLeft { channel_id } => {
- if channel_id == &this.channel_id(cx) {
- cx.emit(PanelEvent::Close)
- }
- }
- _ => {}
- },
- ));
-
- this
- })
- }
-
- pub fn channel_id(&self, cx: &App) -> Option<ChannelId> {
- self.active_chat
- .as_ref()
- .map(|(chat, _)| chat.read(cx).channel_id)
- }
-
- pub fn is_scrolled_to_bottom(&self) -> bool {
- self.is_scrolled_to_bottom
- }
-
- pub fn active_chat(&self) -> Option<Entity<ChannelChat>> {
- self.active_chat.as_ref().map(|(chat, _)| chat.clone())
- }
-
- pub fn load(
- workspace: WeakEntity<Workspace>,
- cx: AsyncWindowContext,
- ) -> Task<Result<Entity<Self>>> {
- cx.spawn(async move |cx| {
- let serialized_panel = if let Some(panel) = cx
- .background_spawn(async move { KEY_VALUE_STORE.read_kvp(CHAT_PANEL_KEY) })
- .await
- .log_err()
- .flatten()
- {
- Some(serde_json::from_str::<SerializedChatPanel>(&panel)?)
- } else {
- None
- };
-
- workspace.update_in(cx, |workspace, window, cx| {
- let panel = Self::new(workspace, window, cx);
- if let Some(serialized_panel) = serialized_panel {
- panel.update(cx, |panel, cx| {
- panel.width = serialized_panel.width.map(|r| r.round());
- cx.notify();
- });
- }
- panel
- })
- })
- }
-
- fn serialize(&mut self, cx: &mut Context<Self>) {
- let width = self.width;
- self.pending_serialization = cx.background_spawn(
- async move {
- KEY_VALUE_STORE
- .write_kvp(
- CHAT_PANEL_KEY.into(),
- serde_json::to_string(&SerializedChatPanel { width })?,
- )
- .await?;
- anyhow::Ok(())
- }
- .log_err(),
- );
- }
-
- fn set_active_chat(&mut self, chat: Entity<ChannelChat>, cx: &mut Context<Self>) {
- if self.active_chat.as_ref().map(|e| &e.0) != Some(&chat) {
- self.markdown_data.clear();
- self.message_list.reset(chat.read(cx).message_count());
- self.message_editor.update(cx, |editor, cx| {
- editor.set_channel_chat(chat.clone(), cx);
- editor.clear_reply_to_message_id();
- });
- let subscription = cx.subscribe(&chat, Self::channel_did_change);
- self.active_chat = Some((chat, subscription));
- self.acknowledge_last_message(cx);
- cx.notify();
- }
- }
-
- fn channel_did_change(
- &mut self,
- _: Entity<ChannelChat>,
- event: &ChannelChatEvent,
- cx: &mut Context<Self>,
- ) {
- match event {
- ChannelChatEvent::MessagesUpdated {
- old_range,
- new_count,
- } => {
- self.message_list.splice(old_range.clone(), *new_count);
- if self.active {
- self.acknowledge_last_message(cx);
- }
- }
- ChannelChatEvent::UpdateMessage {
- message_id,
- message_ix,
- } => {
- self.message_list.splice(*message_ix..*message_ix + 1, 1);
- self.markdown_data.remove(message_id);
- }
- ChannelChatEvent::NewMessage {
- channel_id,
- message_id,
- } => {
- if !self.active {
- self.channel_store.update(cx, |store, cx| {
- store.update_latest_message_id(*channel_id, *message_id, cx)
- })
- }
- }
- }
- cx.notify();
- }
-
- fn acknowledge_last_message(&mut self, cx: &mut Context<Self>) {
- if self.active && self.is_scrolled_to_bottom {
- if let Some((chat, _)) = &self.active_chat {
- if let Some(channel_id) = self.channel_id(cx) {
- self.last_acknowledged_message_id = self
- .channel_store
- .read(cx)
- .last_acknowledge_message_id(channel_id);
- }
-
- chat.update(cx, |chat, cx| {
- chat.acknowledge_last_message(cx);
- });
- }
- }
- }
-
- fn render_replied_to_message(
- &mut self,
- message_id: Option<ChannelMessageId>,
- reply_to_message: &Option<ChannelMessage>,
- cx: &mut Context<Self>,
- ) -> impl IntoElement {
- let reply_to_message = match reply_to_message {
- None => {
- return div().child(
- h_flex()
- .text_ui_xs(cx)
- .my_0p5()
- .px_0p5()
- .gap_x_1()
- .rounded_sm()
- .child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))
- .when(reply_to_message.is_none(), |el| {
- el.child(
- Label::new("Message has been deleted...")
- .size(LabelSize::XSmall)
- .color(Color::Muted),
- )
- }),
- );
- }
- Some(val) => val,
- };
-
- let user_being_replied_to = reply_to_message.sender.clone();
- let message_being_replied_to = reply_to_message.clone();
-
- let message_element_id: ElementId = match message_id {
- Some(ChannelMessageId::Saved(id)) => ("reply-to-saved-message-container", id).into(),
- Some(ChannelMessageId::Pending(id)) => {
- ("reply-to-pending-message-container", id).into()
- } // This should never happen
- None => ("composing-reply-container").into(),
- };
-
- let current_channel_id = self.channel_id(cx);
- let reply_to_message_id = reply_to_message.id;
-
- div().child(
- h_flex()
- .id(message_element_id)
- .text_ui_xs(cx)
- .my_0p5()
- .px_0p5()
- .gap_x_1()
- .rounded_sm()
- .overflow_hidden()
- .hover(|style| style.bg(cx.theme().colors().element_background))
- .child(Icon::new(IconName::ReplyArrowRight).color(Color::Muted))
- .child(Avatar::new(user_being_replied_to.avatar_uri.clone()).size(rems(0.7)))
- .child(
- Label::new(format!("@{}", user_being_replied_to.github_login))
- .size(LabelSize::XSmall)
- .weight(FontWeight::SEMIBOLD)
- .color(Color::Muted),
- )
- .child(
- div().overflow_y_hidden().child(
- Label::new(message_being_replied_to.body.replace('\n', " "))
- .size(LabelSize::XSmall)
- .color(Color::Default),
- ),
- )
- .cursor(CursorStyle::PointingHand)
- .tooltip(Tooltip::text("Go to message"))
- .on_click(cx.listener(move |chat_panel, _, _, cx| {
- if let Some(channel_id) = current_channel_id {
- chat_panel
- .select_channel(channel_id, reply_to_message_id.into(), cx)
- .detach_and_log_err(cx)
- }
- })),
- )
- }
-
- fn render_message(
- &mut self,
- ix: usize,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> AnyElement {
- let active_chat = &self.active_chat.as_ref().unwrap().0;
- let (message, is_continuation_from_previous, is_admin) =
- active_chat.update(cx, |active_chat, cx| {
- let is_admin = self
- .channel_store
- .read(cx)
- .is_channel_admin(active_chat.channel_id);
-
- let last_message = active_chat.message(ix.saturating_sub(1));
- let this_message = active_chat.message(ix).clone();
-
- let duration_since_last_message = this_message.timestamp - last_message.timestamp;
- let is_continuation_from_previous = last_message.sender.id
- == this_message.sender.id
- && last_message.id != this_message.id
- && duration_since_last_message < Duration::from_secs(5 * 60);
-
- if let ChannelMessageId::Saved(id) = this_message.id {
- if this_message
- .mentions
- .iter()
- .any(|(_, user_id)| Some(*user_id) == self.client.user_id())
- {
- active_chat.acknowledge_message(id);
- }
- }
-
- (this_message, is_continuation_from_previous, is_admin)
- });
-
- let _is_pending = message.is_pending();
-
- let belongs_to_user = Some(message.sender.id) == self.client.user_id();
- let can_delete_message = belongs_to_user || is_admin;
- let can_edit_message = belongs_to_user;
-
- let element_id: ElementId = match message.id {
- ChannelMessageId::Saved(id) => ("saved-message", id).into(),
- ChannelMessageId::Pending(id) => ("pending-message", id).into(),
- };
-
- let mentioning_you = message
- .mentions
- .iter()
- .any(|m| Some(m.1) == self.client.user_id());
-
- let message_id = match message.id {
- ChannelMessageId::Saved(id) => Some(id),
- ChannelMessageId::Pending(_) => None,
- };
-
- let reply_to_message = message
- .reply_to_message_id
- .and_then(|id| active_chat.read(cx).find_loaded_message(id))
- .cloned();
-
- let replied_to_you =
- reply_to_message.as_ref().map(|m| m.sender.id) == self.client.user_id();
-
- let is_highlighted_message = self
- .highlighted_message
- .as_ref()
- .is_some_and(|(id, _)| Some(id) == message_id.as_ref());
- let background = if is_highlighted_message {
- cx.theme().status().info_background
- } else if mentioning_you || replied_to_you {
- cx.theme().colors().background
- } else {
- cx.theme().colors().panel_background
- };
-
- let reply_to_message_id = self.message_editor.read(cx).reply_to_message_id();
-
- v_flex()
- .w_full()
- .relative()
- .group("")
- .when(!is_continuation_from_previous, |this| this.pt_2())
- .child(
- div()
- .group("")
- .bg(background)
- .rounded_sm()
- .overflow_hidden()
- .px_1p5()
- .py_0p5()
- .when_some(reply_to_message_id, |el, reply_id| {
- el.when_some(message_id, |el, message_id| {
- el.when(reply_id == message_id, |el| {
- el.bg(cx.theme().colors().element_selected)
- })
- })
- })
- .when(!self.has_open_menu(message_id), |this| {
- this.hover(|style| style.bg(cx.theme().colors().element_hover))
- })
- .when(message.reply_to_message_id.is_some(), |el| {
- el.child(self.render_replied_to_message(
- Some(message.id),
- &reply_to_message,
- cx,
- ))
- .when(is_continuation_from_previous, |this| this.mt_2())
- })
- .when(
- !is_continuation_from_previous || message.reply_to_message_id.is_some(),
- |this| {
- this.child(
- h_flex()
- .gap_2()
- .text_ui_sm(cx)
- .child(
- Avatar::new(message.sender.avatar_uri.clone())
- .size(rems(1.)),
- )
- .child(
- Label::new(message.sender.github_login.clone())
- .size(LabelSize::Small)
- .weight(FontWeight::BOLD),
- )
- .child(
- Label::new(time_format::format_localized_timestamp(
- message.timestamp,
- OffsetDateTime::now_utc(),
- self.local_timezone,
- time_format::TimestampFormat::EnhancedAbsolute,
- ))
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- },
- )
- .when(mentioning_you || replied_to_you, |this| this.my_0p5())
- .map(|el| {
- let text = self.markdown_data.entry(message.id).or_insert_with(|| {
- Self::render_markdown_with_mentions(
- &self.languages,
- self.client.id(),
- &message,
- self.local_timezone,
- cx,
- )
- });
- el.child(
- v_flex()
- .w_full()
- .text_ui_sm(cx)
- .id(element_id)
- .child(text.element("body".into(), window, cx)),
- )
- .when(self.has_open_menu(message_id), |el| {
- el.bg(cx.theme().colors().element_selected)
- })
- }),
- )
- .when(
- self.last_acknowledged_message_id
- .is_some_and(|l| Some(l) == message_id),
- |this| {
- this.child(
- h_flex()
- .py_2()
- .gap_1()
- .items_center()
- .child(div().w_full().h_0p5().bg(cx.theme().colors().border))
- .child(
- div()
- .px_1()
- .rounded_sm()
- .text_ui_xs(cx)
- .bg(cx.theme().colors().background)
- .child("New messages"),
- )
- .child(div().w_full().h_0p5().bg(cx.theme().colors().border)),
- )
- },
- )
- .child(
- self.render_popover_buttons(message_id, can_delete_message, can_edit_message, cx)
- .mt_neg_2p5(),
- )
- .into_any_element()
- }
-
- fn has_open_menu(&self, message_id: Option<u64>) -> bool {
- match self.open_context_menu.as_ref() {
- Some((id, _)) => Some(*id) == message_id,
- None => false,
- }
- }
-
- fn render_popover_button(&self, cx: &mut Context<Self>, child: Stateful<Div>) -> Div {
- div()
- .w_6()
- .bg(cx.theme().colors().element_background)
- .hover(|style| style.bg(cx.theme().colors().element_hover).rounded_sm())
- .child(child)
- }
-
- fn render_popover_buttons(
- &self,
- message_id: Option<u64>,
- can_delete_message: bool,
- can_edit_message: bool,
- cx: &mut Context<Self>,
- ) -> Div {
- h_flex()
- .absolute()
- .right_2()
- .overflow_hidden()
- .rounded_sm()
- .border_color(cx.theme().colors().element_selected)
- .border_1()
- .when(!self.has_open_menu(message_id), |el| {
- el.visible_on_hover("")
- })
- .bg(cx.theme().colors().element_background)
- .when_some(message_id, |el, message_id| {
- el.child(
- self.render_popover_button(
- cx,
- div()
- .id("reply")
- .child(
- IconButton::new(("reply", message_id), IconName::ReplyArrowRight)
- .on_click(cx.listener(move |this, _, window, cx| {
- this.cancel_edit_message(cx);
-
- this.message_editor.update(cx, |editor, cx| {
- editor.set_reply_to_message_id(message_id);
- window.focus(&editor.focus_handle(cx));
- })
- })),
- )
- .tooltip(Tooltip::text("Reply")),
- ),
- )
- })
- .when_some(message_id, |el, message_id| {
- el.when(can_edit_message, |el| {
- el.child(
- self.render_popover_button(
- cx,
- div()
- .id("edit")
- .child(
- IconButton::new(("edit", message_id), IconName::Pencil)
- .on_click(cx.listener(move |this, _, window, cx| {
- this.message_editor.update(cx, |editor, cx| {
- editor.clear_reply_to_message_id();
-
- let message = this
- .active_chat()
- .and_then(|active_chat| {
- active_chat
- .read(cx)
- .find_loaded_message(message_id)
- })
- .cloned();
-
- if let Some(message) = message {
- let buffer = editor
- .editor
- .read(cx)
- .buffer()
- .read(cx)
- .as_singleton()
- .expect("message editor must be singleton");
-
- buffer.update(cx, |buffer, cx| {
- buffer.set_text(message.body.clone(), cx)
- });
-
- editor.set_edit_message_id(message_id);
- editor.focus_handle(cx).focus(window);
- }
- })
- })),
- )
- .tooltip(Tooltip::text("Edit")),
- ),
- )
- })
- })
- .when_some(message_id, |el, message_id| {
- let this = cx.entity().clone();
-
- el.child(
- self.render_popover_button(
- cx,
- div()
- .child(
- PopoverMenu::new(("menu", message_id))
- .trigger(IconButton::new(
- ("trigger", message_id),
- IconName::Ellipsis,
- ))
- .menu(move |window, cx| {
- Some(Self::render_message_menu(
- &this,
- message_id,
- can_delete_message,
- window,
- cx,
- ))
- }),
- )
- .id("more")
- .tooltip(Tooltip::text("More")),
- ),
- )
- })
- }
-
- fn render_message_menu(
- this: &Entity<Self>,
- message_id: u64,
- can_delete_message: bool,
- window: &mut Window,
- cx: &mut App,
- ) -> Entity<ContextMenu> {
- let menu = {
- ContextMenu::build(window, cx, move |menu, window, _| {
- menu.entry(
- "Copy message text",
- None,
- window.handler_for(this, move |this, _, cx| {
- if let Some(message) = this.active_chat().and_then(|active_chat| {
- active_chat.read(cx).find_loaded_message(message_id)
- }) {
- let text = message.body.clone();
- cx.write_to_clipboard(ClipboardItem::new_string(text))
- }
- }),
- )
- .when(can_delete_message, |menu| {
- menu.entry(
- "Delete message",
- None,
- window.handler_for(this, move |this, _, cx| {
- this.remove_message(message_id, cx)
- }),
- )
- })
- })
- };
- this.update(cx, |this, cx| {
- let subscription = cx.subscribe_in(
- &menu,
- window,
- |this: &mut Self, _, _: &DismissEvent, _, _| {
- this.open_context_menu = None;
- },
- );
- this.open_context_menu = Some((message_id, subscription));
- });
- menu
- }
-
- fn render_markdown_with_mentions(
- language_registry: &Arc<LanguageRegistry>,
- current_user_id: u64,
- message: &channel::ChannelMessage,
- local_timezone: UtcOffset,
- cx: &App,
- ) -> RichText {
- let mentions = message
- .mentions
- .iter()
- .map(|(range, user_id)| rich_text::Mention {
- range: range.clone(),
- is_self_mention: *user_id == current_user_id,
- })
- .collect::<Vec<_>>();
-
- const MESSAGE_EDITED: &str = " (edited)";
-
- let mut body = message.body.clone();
-
- if message.edited_at.is_some() {
- body.push_str(MESSAGE_EDITED);
- }
-
- let mut rich_text = RichText::new(body, &mentions, language_registry);
-
- if message.edited_at.is_some() {
- let range = (rich_text.text.len() - MESSAGE_EDITED.len())..rich_text.text.len();
- rich_text.highlights.push((
- range.clone(),
- Highlight::Highlight(HighlightStyle {
- color: Some(cx.theme().colors().text_muted),
- ..Default::default()
- }),
- ));
-
- if let Some(edit_timestamp) = message.edited_at {
- let edit_timestamp_text = time_format::format_localized_timestamp(
- edit_timestamp,
- OffsetDateTime::now_utc(),
- local_timezone,
- time_format::TimestampFormat::Absolute,
- );
-
- rich_text.custom_ranges.push(range);
- rich_text.set_tooltip_builder_for_custom_ranges(move |_, _, _, cx| {
- Some(Tooltip::simple(edit_timestamp_text.clone(), cx))
- })
- }
- }
- rich_text
- }
-
- fn send(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
- if let Some((chat, _)) = self.active_chat.as_ref() {
- let message = self
- .message_editor
- .update(cx, |editor, cx| editor.take_message(window, cx));
-
- if let Some(id) = self.message_editor.read(cx).edit_message_id() {
- self.message_editor.update(cx, |editor, _| {
- editor.clear_edit_message_id();
- });
-
- if let Some(task) = chat
- .update(cx, |chat, cx| chat.update_message(id, message, cx))
- .log_err()
- {
- task.detach();
- }
- } else if let Some(task) = chat
- .update(cx, |chat, cx| chat.send_message(message, cx))
- .log_err()
- {
- task.detach();
- }
- }
- }
-
- fn remove_message(&mut self, id: u64, cx: &mut Context<Self>) {
- if let Some((chat, _)) = self.active_chat.as_ref() {
- chat.update(cx, |chat, cx| chat.remove_message(id, cx).detach())
- }
- }
-
- fn load_more_messages(&mut self, cx: &mut Context<Self>) {
- if let Some((chat, _)) = self.active_chat.as_ref() {
- chat.update(cx, |channel, cx| {
- if let Some(task) = channel.load_more_messages(cx) {
- task.detach();
- }
- })
- }
- }
-
- pub fn select_channel(
- &mut self,
- selected_channel_id: ChannelId,
- scroll_to_message_id: Option<u64>,
- cx: &mut Context<ChatPanel>,
- ) -> Task<Result<()>> {
- let open_chat = self
- .active_chat
- .as_ref()
- .and_then(|(chat, _)| {
- (chat.read(cx).channel_id == selected_channel_id)
- .then(|| Task::ready(anyhow::Ok(chat.clone())))
- })
- .unwrap_or_else(|| {
- self.channel_store.update(cx, |store, cx| {
- store.open_channel_chat(selected_channel_id, cx)
- })
- });
-
- cx.spawn(async move |this, cx| {
- let chat = open_chat.await?;
- let highlight_message_id = scroll_to_message_id;
- let scroll_to_message_id = this.update(cx, |this, cx| {
- this.set_active_chat(chat.clone(), cx);
-
- scroll_to_message_id.or(this.last_acknowledged_message_id)
- })?;
-
- if let Some(message_id) = scroll_to_message_id {
- if let Some(item_ix) =
- ChannelChat::load_history_since_message(chat.clone(), message_id, cx.clone())
- .await
- {
- this.update(cx, |this, cx| {
- if let Some(highlight_message_id) = highlight_message_id {
- let task = cx.spawn(async move |this, cx| {
- cx.background_executor().timer(Duration::from_secs(2)).await;
- this.update(cx, |this, cx| {
- this.highlighted_message.take();
- cx.notify();
- })
- .ok();
- });
-
- this.highlighted_message = Some((highlight_message_id, task));
- }
-
- if this.active_chat.as_ref().map_or(false, |(c, _)| *c == chat) {
- this.message_list.scroll_to(ListOffset {
- item_ix,
- offset_in_item: px(0.0),
- });
- cx.notify();
- }
- })?;
- }
- }
-
- Ok(())
- })
- }
-
- fn close_reply_preview(&mut self, cx: &mut Context<Self>) {
- self.message_editor
- .update(cx, |editor, _| editor.clear_reply_to_message_id());
- }
-
- fn cancel_edit_message(&mut self, cx: &mut Context<Self>) {
- self.message_editor.update(cx, |editor, cx| {
- // only clear the editor input if we were editing a message
- if editor.edit_message_id().is_none() {
- return;
- }
-
- editor.clear_edit_message_id();
-
- let buffer = editor
- .editor
- .read(cx)
- .buffer()
- .read(cx)
- .as_singleton()
- .expect("message editor must be singleton");
-
- buffer.update(cx, |buffer, cx| buffer.set_text("", cx));
- });
- }
-}
-
-impl Render for ChatPanel {
- fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let channel_id = self
- .active_chat
- .as_ref()
- .map(|(c, _)| c.read(cx).channel_id);
- let message_editor = self.message_editor.read(cx);
-
- let reply_to_message_id = message_editor.reply_to_message_id();
- let edit_message_id = message_editor.edit_message_id();
-
- v_flex()
- .key_context("ChatPanel")
- .track_focus(&self.focus_handle)
- .size_full()
- .on_action(cx.listener(Self::send))
- .child(
- h_flex().child(
- TabBar::new("chat_header").child(
- h_flex()
- .w_full()
- .h(Tab::container_height(cx))
- .px_2()
- .child(Label::new(
- self.active_chat
- .as_ref()
- .and_then(|c| {
- Some(format!("#{}", c.0.read(cx).channel(cx)?.name))
- })
- .unwrap_or("Chat".to_string()),
- )),
- ),
- ),
- )
- .child(div().flex_grow().px_2().map(|this| {
- if self.active_chat.is_some() {
- this.child(
- list(
- self.message_list.clone(),
- cx.processor(Self::render_message),
- )
- .size_full(),
- )
- } else {
- this.child(
- div()
- .size_full()
- .p_4()
- .child(
- Label::new("Select a channel to chat in.")
- .size(LabelSize::Small)
- .color(Color::Muted),
- )
- .child(
- div().pt_1().w_full().items_center().child(
- Button::new("toggle-collab", "Open")
- .full_width()
- .key_binding(KeyBinding::for_action(
- &collab_panel::ToggleFocus,
- window,
- cx,
- ))
- .on_click(|_, window, cx| {
- window.dispatch_action(
- collab_panel::ToggleFocus.boxed_clone(),
- cx,
- )
- }),
- ),
- ),
- )
- }
- }))
- .when(!self.is_scrolled_to_bottom, |el| {
- el.child(div().border_t_1().border_color(cx.theme().colors().border))
- })
- .when_some(edit_message_id, |el, _| {
- el.child(
- h_flex()
- .px_2()
- .text_ui_xs(cx)
- .justify_between()
- .border_t_1()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background)
- .child("Editing message")
- .child(
- IconButton::new("cancel-edit-message", IconName::Close)
- .shape(ui::IconButtonShape::Square)
- .tooltip(Tooltip::text("Cancel edit message"))
- .on_click(cx.listener(move |this, _, _, cx| {
- this.cancel_edit_message(cx);
- })),
- ),
- )
- })
- .when_some(reply_to_message_id, |el, reply_to_message_id| {
- let reply_message = self
- .active_chat()
- .and_then(|active_chat| {
- active_chat
- .read(cx)
- .find_loaded_message(reply_to_message_id)
- })
- .cloned();
-
- el.when_some(reply_message, |el, reply_message| {
- let user_being_replied_to = reply_message.sender.clone();
-
- el.child(
- h_flex()
- .when(!self.is_scrolled_to_bottom, |el| {
- el.border_t_1().border_color(cx.theme().colors().border)
- })
- .justify_between()
- .overflow_hidden()
- .items_start()
- .py_1()
- .px_2()
- .bg(cx.theme().colors().background)
- .child(
- div().flex_shrink().overflow_hidden().child(
- h_flex()
- .id(("reply-preview", reply_to_message_id))
- .child(Label::new("Replying to ").size(LabelSize::Small))
- .child(
- Label::new(format!(
- "@{}",
- user_being_replied_to.github_login
- ))
- .size(LabelSize::Small)
- .weight(FontWeight::BOLD),
- )
- .when_some(channel_id, |this, channel_id| {
- this.cursor_pointer().on_click(cx.listener(
- move |chat_panel, _, _, cx| {
- chat_panel
- .select_channel(
- channel_id,
- reply_to_message_id.into(),
- cx,
- )
- .detach_and_log_err(cx)
- },
- ))
- }),
- ),
- )
- .child(
- IconButton::new("close-reply-preview", IconName::Close)
- .shape(ui::IconButtonShape::Square)
- .tooltip(Tooltip::text("Close reply"))
- .on_click(cx.listener(move |this, _, _, cx| {
- this.close_reply_preview(cx);
- })),
- ),
- )
- })
- })
- .children(
- Some(
- h_flex()
- .p_2()
- .on_action(cx.listener(|this, _: &actions::Cancel, _, cx| {
- this.cancel_edit_message(cx);
- this.close_reply_preview(cx);
- }))
- .map(|el| el.child(self.message_editor.clone())),
- )
- .filter(|_| self.active_chat.is_some()),
- )
- .into_any()
- }
-}
-
-impl Focusable for ChatPanel {
- fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
- if self.active_chat.is_some() {
- self.message_editor.read(cx).focus_handle(cx)
- } else {
- self.focus_handle.clone()
- }
- }
-}
-
-impl Panel for ChatPanel {
- fn position(&self, _: &Window, cx: &App) -> DockPosition {
- ChatPanelSettings::get_global(cx).dock
- }
-
- fn position_is_valid(&self, position: DockPosition) -> bool {
- matches!(position, DockPosition::Left | DockPosition::Right)
- }
-
- fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
- settings::update_settings_file::<ChatPanelSettings>(
- self.fs.clone(),
- cx,
- move |settings, _| settings.dock = Some(position),
- );
- }
-
- fn size(&self, _: &Window, cx: &App) -> Pixels {
- self.width
- .unwrap_or_else(|| ChatPanelSettings::get_global(cx).default_width)
- }
-
- fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
- self.width = size;
- self.serialize(cx);
- cx.notify();
- }
-
- fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut Context<Self>) {
- self.active = active;
- if active {
- self.acknowledge_last_message(cx);
- }
- }
-
- fn persistent_name() -> &'static str {
- "ChatPanel"
- }
-
- fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {
- self.enabled(cx).then(|| ui::IconName::Chat)
- }
-
- fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> {
- Some("Chat Panel")
- }
-
- fn toggle_action(&self) -> Box<dyn gpui::Action> {
- Box::new(ToggleFocus)
- }
-
- fn starts_open(&self, _: &Window, cx: &App) -> bool {
- ActiveCall::global(cx)
- .read(cx)
- .room()
- .is_some_and(|room| room.read(cx).contains_guests())
- }
-
- fn activation_priority(&self) -> u32 {
- 7
- }
-
- fn enabled(&self, cx: &App) -> bool {
- match ChatPanelSettings::get_global(cx).button {
- ChatPanelButton::Never => false,
- ChatPanelButton::Always => true,
- ChatPanelButton::WhenInCall => {
- let is_in_call = ActiveCall::global(cx)
- .read(cx)
- .room()
- .map_or(false, |room| room.read(cx).contains_guests());
-
- self.active || is_in_call
- }
- }
- }
-}
-
-impl EventEmitter<PanelEvent> for ChatPanel {}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use gpui::HighlightStyle;
- use pretty_assertions::assert_eq;
- use rich_text::Highlight;
- use time::OffsetDateTime;
- use util::test::marked_text_ranges;
-
- #[gpui::test]
- fn test_render_markdown_with_mentions(cx: &mut App) {
- let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
- let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
- let message = channel::ChannelMessage {
- id: ChannelMessageId::Saved(0),
- body,
- timestamp: OffsetDateTime::now_utc(),
- sender: Arc::new(client::User {
- github_login: "fgh".into(),
- avatar_uri: "avatar_fgh".into(),
- id: 103,
- name: None,
- }),
- nonce: 5,
- mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
- reply_to_message_id: None,
- edited_at: None,
- };
-
- let message = ChatPanel::render_markdown_with_mentions(
- &language_registry,
- 102,
- &message,
- UtcOffset::UTC,
- cx,
- );
-
- // Note that the "'" was replaced with ’ due to smart punctuation.
- let (body, ranges) = marked_text_ranges("«hi», «@abc», let’s «call» «@fgh»", false);
- assert_eq!(message.text, body);
- assert_eq!(
- message.highlights,
- vec![
- (
- ranges[0].clone(),
- HighlightStyle {
- font_style: Some(gpui::FontStyle::Italic),
- ..Default::default()
- }
- .into()
- ),
- (ranges[1].clone(), Highlight::Mention),
- (
- ranges[2].clone(),
- HighlightStyle {
- font_weight: Some(gpui::FontWeight::BOLD),
- ..Default::default()
- }
- .into()
- ),
- (ranges[3].clone(), Highlight::SelfMention)
- ]
- );
- }
-
- #[gpui::test]
- fn test_render_markdown_with_auto_detect_links(cx: &mut App) {
- let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
- let message = channel::ChannelMessage {
- id: ChannelMessageId::Saved(0),
- body: "Here is a link https://zed.dev to zeds website".to_string(),
- timestamp: OffsetDateTime::now_utc(),
- sender: Arc::new(client::User {
- github_login: "fgh".into(),
- avatar_uri: "avatar_fgh".into(),
- id: 103,
- name: None,
- }),
- nonce: 5,
- mentions: Vec::new(),
- reply_to_message_id: None,
- edited_at: None,
- };
-
- let message = ChatPanel::render_markdown_with_mentions(
- &language_registry,
- 102,
- &message,
- UtcOffset::UTC,
- cx,
- );
-
- // Note that the "'" was replaced with ’ due to smart punctuation.
- let (body, ranges) =
- marked_text_ranges("Here is a link «https://zed.dev» to zeds website", false);
- assert_eq!(message.text, body);
- assert_eq!(1, ranges.len());
- assert_eq!(
- message.highlights,
- vec![(
- ranges[0].clone(),
- HighlightStyle {
- underline: Some(gpui::UnderlineStyle {
- thickness: 1.0.into(),
- ..Default::default()
- }),
- ..Default::default()
- }
- .into()
- ),]
- );
- }
-
- #[gpui::test]
- fn test_render_markdown_with_auto_detect_links_and_additional_formatting(cx: &mut App) {
- let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
- let message = channel::ChannelMessage {
- id: ChannelMessageId::Saved(0),
- body: "**Here is a link https://zed.dev to zeds website**".to_string(),
- timestamp: OffsetDateTime::now_utc(),
- sender: Arc::new(client::User {
- github_login: "fgh".into(),
- avatar_uri: "avatar_fgh".into(),
- id: 103,
- name: None,
- }),
- nonce: 5,
- mentions: Vec::new(),
- reply_to_message_id: None,
- edited_at: None,
- };
-
- let message = ChatPanel::render_markdown_with_mentions(
- &language_registry,
- 102,
- &message,
- UtcOffset::UTC,
- cx,
- );
-
- // Note that the "'" was replaced with ’ due to smart punctuation.
- let (body, ranges) = marked_text_ranges(
- "«Here is a link »«https://zed.dev»« to zeds website»",
- false,
- );
- assert_eq!(message.text, body);
- assert_eq!(3, ranges.len());
- assert_eq!(
- message.highlights,
- vec![
- (
- ranges[0].clone(),
- HighlightStyle {
- font_weight: Some(gpui::FontWeight::BOLD),
- ..Default::default()
- }
- .into()
- ),
- (
- ranges[1].clone(),
- HighlightStyle {
- font_weight: Some(gpui::FontWeight::BOLD),
- underline: Some(gpui::UnderlineStyle {
- thickness: 1.0.into(),
- ..Default::default()
- }),
- ..Default::default()
- }
- .into()
- ),
- (
- ranges[2].clone(),
- HighlightStyle {
- font_weight: Some(gpui::FontWeight::BOLD),
- ..Default::default()
- }
- .into()
- ),
- ]
- );
- }
-}
@@ -1,548 +0,0 @@
-use anyhow::{Context as _, Result};
-use channel::{ChannelChat, ChannelStore, MessageParams};
-use client::{UserId, UserStore};
-use collections::HashSet;
-use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
-use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::{
- AsyncApp, AsyncWindowContext, Context, Entity, Focusable, FontStyle, FontWeight,
- HighlightStyle, IntoElement, Render, Task, TextStyle, WeakEntity, Window,
-};
-use language::{
- Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, ToOffset,
- language_settings::SoftWrap,
-};
-use project::{Completion, CompletionResponse, CompletionSource, search::SearchQuery};
-use settings::Settings;
-use std::{
- ops::Range,
- rc::Rc,
- sync::{Arc, LazyLock},
- time::Duration,
-};
-use theme::ThemeSettings;
-use ui::{TextSize, prelude::*};
-
-use crate::panel_settings::MessageEditorSettings;
-
-const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
-
-static MENTIONS_SEARCH: LazyLock<SearchQuery> = LazyLock::new(|| {
- SearchQuery::regex(
- "@[-_\\w]+",
- false,
- false,
- false,
- false,
- Default::default(),
- Default::default(),
- false,
- None,
- )
- .unwrap()
-});
-
-pub struct MessageEditor {
- pub editor: Entity<Editor>,
- user_store: Entity<UserStore>,
- channel_chat: Option<Entity<ChannelChat>>,
- mentions: Vec<UserId>,
- mentions_task: Option<Task<()>>,
- reply_to_message_id: Option<u64>,
- edit_message_id: Option<u64>,
-}
-
-struct MessageEditorCompletionProvider(WeakEntity<MessageEditor>);
-
-impl CompletionProvider for MessageEditorCompletionProvider {
- fn completions(
- &self,
- _excerpt_id: ExcerptId,
- buffer: &Entity<Buffer>,
- buffer_position: language::Anchor,
- _: editor::CompletionContext,
- _window: &mut Window,
- cx: &mut Context<Editor>,
- ) -> Task<Result<Vec<CompletionResponse>>> {
- let Some(handle) = self.0.upgrade() else {
- return Task::ready(Ok(Vec::new()));
- };
- handle.update(cx, |message_editor, cx| {
- message_editor.completions(buffer, buffer_position, cx)
- })
- }
-
- fn is_completion_trigger(
- &self,
- _buffer: &Entity<Buffer>,
- _position: language::Anchor,
- text: &str,
- _trigger_in_words: bool,
- _menu_is_open: bool,
- _cx: &mut Context<Editor>,
- ) -> bool {
- text == "@"
- }
-}
-
-impl MessageEditor {
- pub fn new(
- language_registry: Arc<LanguageRegistry>,
- user_store: Entity<UserStore>,
- channel_chat: Option<Entity<ChannelChat>>,
- editor: Entity<Editor>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let this = cx.entity().downgrade();
- editor.update(cx, |editor, cx| {
- editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
- editor.set_offset_content(false, cx);
- editor.set_use_autoclose(false);
- editor.set_show_gutter(false, cx);
- editor.set_show_wrap_guides(false, cx);
- editor.set_show_indent_guides(false, cx);
- editor.set_completion_provider(Some(Rc::new(MessageEditorCompletionProvider(this))));
- editor.set_auto_replace_emoji_shortcode(
- MessageEditorSettings::get_global(cx)
- .auto_replace_emoji_shortcode
- .unwrap_or_default(),
- );
- });
-
- let buffer = editor
- .read(cx)
- .buffer()
- .read(cx)
- .as_singleton()
- .expect("message editor must be singleton");
-
- cx.subscribe_in(&buffer, window, Self::on_buffer_event)
- .detach();
- cx.observe_global::<settings::SettingsStore>(|this, cx| {
- this.editor.update(cx, |editor, cx| {
- editor.set_auto_replace_emoji_shortcode(
- MessageEditorSettings::get_global(cx)
- .auto_replace_emoji_shortcode
- .unwrap_or_default(),
- )
- })
- })
- .detach();
-
- let markdown = language_registry.language_for_name("Markdown");
- cx.spawn_in(window, async move |_, cx| {
- let markdown = markdown.await.context("failed to load Markdown language")?;
- buffer.update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx))
- })
- .detach_and_log_err(cx);
-
- Self {
- editor,
- user_store,
- channel_chat,
- mentions: Vec::new(),
- mentions_task: None,
- reply_to_message_id: None,
- edit_message_id: None,
- }
- }
-
- pub fn reply_to_message_id(&self) -> Option<u64> {
- self.reply_to_message_id
- }
-
- pub fn set_reply_to_message_id(&mut self, reply_to_message_id: u64) {
- self.reply_to_message_id = Some(reply_to_message_id);
- }
-
- pub fn clear_reply_to_message_id(&mut self) {
- self.reply_to_message_id = None;
- }
-
- pub fn edit_message_id(&self) -> Option<u64> {
- self.edit_message_id
- }
-
- pub fn set_edit_message_id(&mut self, edit_message_id: u64) {
- self.edit_message_id = Some(edit_message_id);
- }
-
- pub fn clear_edit_message_id(&mut self) {
- self.edit_message_id = None;
- }
-
- pub fn set_channel_chat(&mut self, chat: Entity<ChannelChat>, cx: &mut Context<Self>) {
- let channel_id = chat.read(cx).channel_id;
- self.channel_chat = Some(chat);
- let channel_name = ChannelStore::global(cx)
- .read(cx)
- .channel_for_id(channel_id)
- .map(|channel| channel.name.clone());
- self.editor.update(cx, |editor, cx| {
- if let Some(channel_name) = channel_name {
- editor.set_placeholder_text(format!("Message #{channel_name}"), cx);
- } else {
- editor.set_placeholder_text("Message Channel", cx);
- }
- });
- }
-
- pub fn take_message(&mut self, window: &mut Window, cx: &mut Context<Self>) -> MessageParams {
- self.editor.update(cx, |editor, cx| {
- let highlights = editor.text_highlights::<Self>(cx);
- let text = editor.text(cx);
- let snapshot = editor.buffer().read(cx).snapshot(cx);
- let mentions = if let Some((_, ranges)) = highlights {
- ranges
- .iter()
- .map(|range| range.to_offset(&snapshot))
- .zip(self.mentions.iter().copied())
- .collect()
- } else {
- Vec::new()
- };
-
- editor.clear(window, cx);
- self.mentions.clear();
- let reply_to_message_id = std::mem::take(&mut self.reply_to_message_id);
-
- MessageParams {
- text,
- mentions,
- reply_to_message_id,
- }
- })
- }
-
- fn on_buffer_event(
- &mut self,
- buffer: &Entity<Buffer>,
- event: &language::BufferEvent,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
- let buffer = buffer.read(cx).snapshot();
- self.mentions_task = Some(cx.spawn_in(window, async move |this, cx| {
- cx.background_executor()
- .timer(MENTIONS_DEBOUNCE_INTERVAL)
- .await;
- Self::find_mentions(this, buffer, cx).await;
- }));
- }
- }
-
- fn completions(
- &mut self,
- buffer: &Entity<Buffer>,
- end_anchor: Anchor,
- cx: &mut Context<Self>,
- ) -> Task<Result<Vec<CompletionResponse>>> {
- if let Some((start_anchor, query, candidates)) =
- self.collect_mention_candidates(buffer, end_anchor, cx)
- {
- if !candidates.is_empty() {
- return cx.spawn(async move |_, cx| {
- let completion_response = Self::completions_for_candidates(
- &cx,
- query.as_str(),
- &candidates,
- start_anchor..end_anchor,
- Self::completion_for_mention,
- )
- .await;
- Ok(vec![completion_response])
- });
- }
- }
-
- if let Some((start_anchor, query, candidates)) =
- self.collect_emoji_candidates(buffer, end_anchor, cx)
- {
- if !candidates.is_empty() {
- return cx.spawn(async move |_, cx| {
- let completion_response = Self::completions_for_candidates(
- &cx,
- query.as_str(),
- candidates,
- start_anchor..end_anchor,
- Self::completion_for_emoji,
- )
- .await;
- Ok(vec![completion_response])
- });
- }
- }
-
- Task::ready(Ok(vec![CompletionResponse {
- completions: Vec::new(),
- is_incomplete: false,
- }]))
- }
-
- async fn completions_for_candidates(
- cx: &AsyncApp,
- query: &str,
- candidates: &[StringMatchCandidate],
- range: Range<Anchor>,
- completion_fn: impl Fn(&StringMatch) -> (String, CodeLabel),
- ) -> CompletionResponse {
- const LIMIT: usize = 10;
- let matches = fuzzy::match_strings(
- candidates,
- query,
- true,
- true,
- LIMIT,
- &Default::default(),
- cx.background_executor().clone(),
- )
- .await;
-
- let completions = matches
- .into_iter()
- .map(|mat| {
- let (new_text, label) = completion_fn(&mat);
- Completion {
- replace_range: range.clone(),
- new_text,
- label,
- icon_path: None,
- confirm: None,
- documentation: None,
- insert_text_mode: None,
- source: CompletionSource::Custom,
- }
- })
- .collect::<Vec<_>>();
-
- CompletionResponse {
- is_incomplete: completions.len() >= LIMIT,
- completions,
- }
- }
-
- fn completion_for_mention(mat: &StringMatch) -> (String, CodeLabel) {
- let label = CodeLabel {
- filter_range: 1..mat.string.len() + 1,
- text: format!("@{}", mat.string),
- runs: Vec::new(),
- };
- (mat.string.clone(), label)
- }
-
- fn completion_for_emoji(mat: &StringMatch) -> (String, CodeLabel) {
- let emoji = emojis::get_by_shortcode(&mat.string).unwrap();
- let label = CodeLabel {
- filter_range: 1..mat.string.len() + 1,
- text: format!(":{}: {}", mat.string, emoji),
- runs: Vec::new(),
- };
- (emoji.to_string(), label)
- }
-
- fn collect_mention_candidates(
- &mut self,
- buffer: &Entity<Buffer>,
- end_anchor: Anchor,
- cx: &mut Context<Self>,
- ) -> Option<(Anchor, String, Vec<StringMatchCandidate>)> {
- let end_offset = end_anchor.to_offset(buffer.read(cx));
-
- let query = buffer.read_with(cx, |buffer, _| {
- let mut query = String::new();
- for ch in buffer.reversed_chars_at(end_offset).take(100) {
- if ch == '@' {
- return Some(query.chars().rev().collect::<String>());
- }
- if ch.is_whitespace() || !ch.is_ascii() {
- break;
- }
- query.push(ch);
- }
- None
- })?;
-
- let start_offset = end_offset - query.len();
- let start_anchor = buffer.read(cx).anchor_before(start_offset);
-
- let mut names = HashSet::default();
- if let Some(chat) = self.channel_chat.as_ref() {
- let chat = chat.read(cx);
- for participant in ChannelStore::global(cx)
- .read(cx)
- .channel_participants(chat.channel_id)
- {
- names.insert(participant.github_login.clone());
- }
- for message in chat
- .messages_in_range(chat.message_count().saturating_sub(100)..chat.message_count())
- {
- names.insert(message.sender.github_login.clone());
- }
- }
-
- let candidates = names
- .into_iter()
- .map(|user| StringMatchCandidate::new(0, &user))
- .collect::<Vec<_>>();
-
- Some((start_anchor, query, candidates))
- }
-
- fn collect_emoji_candidates(
- &mut self,
- buffer: &Entity<Buffer>,
- end_anchor: Anchor,
- cx: &mut Context<Self>,
- ) -> Option<(Anchor, String, &'static [StringMatchCandidate])> {
- static EMOJI_FUZZY_MATCH_CANDIDATES: LazyLock<Vec<StringMatchCandidate>> =
- LazyLock::new(|| {
- let emojis = emojis::iter()
- .flat_map(|s| s.shortcodes())
- .map(|emoji| StringMatchCandidate::new(0, emoji))
- .collect::<Vec<_>>();
- emojis
- });
-
- let end_offset = end_anchor.to_offset(buffer.read(cx));
-
- let query = buffer.read_with(cx, |buffer, _| {
- let mut query = String::new();
- for ch in buffer.reversed_chars_at(end_offset).take(100) {
- if ch == ':' {
- let next_char = buffer
- .reversed_chars_at(end_offset - query.len() - 1)
- .next();
- // Ensure we are at the start of the message or that the previous character is a whitespace
- if next_char.is_none() || next_char.unwrap().is_whitespace() {
- return Some(query.chars().rev().collect::<String>());
- }
-
- // If the previous character is not a whitespace, we are in the middle of a word
- // and we only want to complete the shortcode if the word is made up of other emojis
- let mut containing_word = String::new();
- for ch in buffer
- .reversed_chars_at(end_offset - query.len() - 1)
- .take(100)
- {
- if ch.is_whitespace() {
- break;
- }
- containing_word.push(ch);
- }
- let containing_word = containing_word.chars().rev().collect::<String>();
- if util::word_consists_of_emojis(containing_word.as_str()) {
- return Some(query.chars().rev().collect::<String>());
- }
- break;
- }
- if ch.is_whitespace() || !ch.is_ascii() {
- break;
- }
- query.push(ch);
- }
- None
- })?;
-
- let start_offset = end_offset - query.len() - 1;
- let start_anchor = buffer.read(cx).anchor_before(start_offset);
-
- Some((start_anchor, query, &EMOJI_FUZZY_MATCH_CANDIDATES))
- }
-
- async fn find_mentions(
- this: WeakEntity<MessageEditor>,
- buffer: BufferSnapshot,
- cx: &mut AsyncWindowContext,
- ) {
- let (buffer, ranges) = cx
- .background_spawn(async move {
- let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
- (buffer, ranges)
- })
- .await;
-
- this.update(cx, |this, cx| {
- let mut anchor_ranges = Vec::new();
- let mut mentioned_user_ids = Vec::new();
- let mut text = String::new();
-
- this.editor.update(cx, |editor, cx| {
- let multi_buffer = editor.buffer().read(cx).snapshot(cx);
- for range in ranges {
- text.clear();
- text.extend(buffer.text_for_range(range.clone()));
- if let Some(username) = text.strip_prefix('@') {
- if let Some(user) = this
- .user_store
- .read(cx)
- .cached_user_by_github_login(username)
- {
- let start = multi_buffer.anchor_after(range.start);
- let end = multi_buffer.anchor_after(range.end);
-
- mentioned_user_ids.push(user.id);
- anchor_ranges.push(start..end);
- }
- }
- }
-
- editor.clear_highlights::<Self>(cx);
- editor.highlight_text::<Self>(
- anchor_ranges,
- HighlightStyle {
- font_weight: Some(FontWeight::BOLD),
- ..Default::default()
- },
- cx,
- )
- });
-
- this.mentions = mentioned_user_ids;
- this.mentions_task.take();
- })
- .ok();
- }
-
- pub(crate) fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle {
- self.editor.read(cx).focus_handle(cx)
- }
-}
-
-impl Render for MessageEditor {
- fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let settings = ThemeSettings::get_global(cx);
- let text_style = TextStyle {
- color: if self.editor.read(cx).read_only(cx) {
- cx.theme().colors().text_disabled
- } else {
- cx.theme().colors().text
- },
- font_family: settings.ui_font.family.clone(),
- font_features: settings.ui_font.features.clone(),
- font_fallbacks: settings.ui_font.fallbacks.clone(),
- font_size: TextSize::Small.rems(cx).into(),
- font_weight: settings.ui_font.weight,
- font_style: FontStyle::Normal,
- line_height: relative(1.3),
- ..Default::default()
- };
-
- div()
- .w_full()
- .px_2()
- .py_1()
- .bg(cx.theme().colors().editor_background)
- .rounded_sm()
- .child(EditorElement::new(
- &self.editor,
- EditorStyle {
- local_player: cx.theme().players().local(),
- text: text_style,
- ..Default::default()
- },
- ))
- }
-}
@@ -2,7 +2,7 @@ mod channel_modal;
mod contact_finder;
use self::channel_modal::ChannelModal;
-use crate::{CollaborationPanelSettings, channel_view::ChannelView, chat_panel::ChatPanel};
+use crate::{CollaborationPanelSettings, channel_view::ChannelView};
use anyhow::Context as _;
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelStore};
@@ -38,7 +38,7 @@ use util::{ResultExt, TryFutureExt, maybe};
use workspace::{
Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace,
dock::{DockPosition, Panel, PanelEvent},
- notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
+ notifications::{DetachAndPromptErr, NotifyResultExt},
};
actions!(
@@ -95,7 +95,7 @@ pub fn init(cx: &mut App) {
.and_then(|room| room.read(cx).channel_id());
if let Some(channel_id) = channel_id {
- let workspace = cx.entity().clone();
+ let workspace = cx.entity();
window.defer(cx, move |window, cx| {
ChannelView::open(channel_id, None, workspace, window, cx)
.detach_and_log_err(cx)
@@ -261,9 +261,6 @@ enum ListEntry {
ChannelNotes {
channel_id: ChannelId,
},
- ChannelChat {
- channel_id: ChannelId,
- },
ChannelEditor {
depth: usize,
},
@@ -283,7 +280,7 @@ impl CollabPanel {
cx.new(|cx| {
let filter_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("Filter...", cx);
+ editor.set_placeholder_text("Filter...", window, cx);
editor
});
@@ -311,10 +308,10 @@ impl CollabPanel {
window,
|this: &mut Self, _, event, window, cx| {
if let editor::EditorEvent::Blurred = event {
- if let Some(state) = &this.channel_editing_state {
- if state.pending_name().is_some() {
- return;
- }
+ if let Some(state) = &this.channel_editing_state
+ && state.pending_name().is_some()
+ {
+ return;
}
this.take_editing_state(window, cx);
this.update_entries(false, cx);
@@ -491,11 +488,10 @@ impl CollabPanel {
if !self.collapsed_sections.contains(&Section::ActiveCall) {
let room = room.read(cx);
- if query.is_empty() {
- if let Some(channel_id) = room.channel_id() {
- self.entries.push(ListEntry::ChannelNotes { channel_id });
- self.entries.push(ListEntry::ChannelChat { channel_id });
- }
+ if query.is_empty()
+ && let Some(channel_id) = room.channel_id()
+ {
+ self.entries.push(ListEntry::ChannelNotes { channel_id });
}
// Populate the active user.
@@ -639,10 +635,10 @@ impl CollabPanel {
&Default::default(),
executor.clone(),
));
- if let Some(state) = &self.channel_editing_state {
- if matches!(state, ChannelEditingState::Create { location: None, .. }) {
- self.entries.push(ListEntry::ChannelEditor { depth: 0 });
- }
+ if let Some(state) = &self.channel_editing_state
+ && matches!(state, ChannelEditingState::Create { location: None, .. })
+ {
+ self.entries.push(ListEntry::ChannelEditor { depth: 0 });
}
let mut collapse_depth = None;
for mat in matches {
@@ -664,9 +660,7 @@ impl CollabPanel {
let has_children = channel_store
.channel_at_index(mat.candidate_id + 1)
- .map_or(false, |next_channel| {
- next_channel.parent_path.ends_with(&[channel.id])
- });
+ .is_some_and(|next_channel| next_channel.parent_path.ends_with(&[channel.id]));
match &self.channel_editing_state {
Some(ChannelEditingState::Create {
@@ -1091,41 +1085,8 @@ impl CollabPanel {
.tooltip(Tooltip::text("Open Channel Notes"))
}
- fn render_channel_chat(
- &self,
- channel_id: ChannelId,
- is_selected: bool,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> impl IntoElement {
- let channel_store = self.channel_store.read(cx);
- let has_messages_notification = channel_store.has_new_messages(channel_id);
- ListItem::new("channel-chat")
- .toggle_state(is_selected)
- .on_click(cx.listener(move |this, _, window, cx| {
- this.join_channel_chat(channel_id, window, cx);
- }))
- .start_slot(
- h_flex()
- .relative()
- .gap_1()
- .child(render_tree_branch(false, false, window, cx))
- .child(IconButton::new(0, IconName::Chat))
- .children(has_messages_notification.then(|| {
- div()
- .w_1p5()
- .absolute()
- .right(px(2.))
- .top(px(4.))
- .child(Indicator::dot().color(Color::Info))
- })),
- )
- .child(Label::new("chat"))
- .tooltip(Tooltip::text("Open Chat"))
- }
-
fn has_subchannels(&self, ix: usize) -> bool {
- self.entries.get(ix).map_or(false, |entry| {
+ self.entries.get(ix).is_some_and(|entry| {
if let ListEntry::Channel { has_children, .. } = entry {
*has_children
} else {
@@ -1142,7 +1103,7 @@ impl CollabPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let this = cx.entity().clone();
+ let this = cx.entity();
if !(role == proto::ChannelRole::Guest
|| role == proto::ChannelRole::Talker
|| role == proto::ChannelRole::Member)
@@ -1272,7 +1233,7 @@ impl CollabPanel {
.channel_for_id(clipboard.channel_id)
.map(|channel| channel.name.clone())
});
- let this = cx.entity().clone();
+ let this = cx.entity();
let context_menu = ContextMenu::build(window, cx, |mut context_menu, window, cx| {
if self.has_subchannels(ix) {
@@ -1298,13 +1259,6 @@ impl CollabPanel {
this.open_channel_notes(channel_id, window, cx)
}),
)
- .entry(
- "Open Chat",
- None,
- window.handler_for(&this, move |this, window, cx| {
- this.join_channel_chat(channel_id, window, cx)
- }),
- )
.entry(
"Copy Channel Link",
None,
@@ -1439,7 +1393,7 @@ impl CollabPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let this = cx.entity().clone();
+ let this = cx.entity();
let in_room = ActiveCall::global(cx).read(cx).room().is_some();
let context_menu = ContextMenu::build(window, cx, |mut context_menu, _, _| {
@@ -1552,98 +1506,90 @@ impl CollabPanel {
return;
}
- if let Some(selection) = self.selection {
- if let Some(entry) = self.entries.get(selection) {
- match entry {
- ListEntry::Header(section) => match section {
- Section::ActiveCall => Self::leave_call(window, cx),
- Section::Channels => self.new_root_channel(window, cx),
- Section::Contacts => self.toggle_contact_finder(window, cx),
- Section::ContactRequests
- | Section::Online
- | Section::Offline
- | Section::ChannelInvites => {
- self.toggle_section_expanded(*section, cx);
- }
- },
- ListEntry::Contact { contact, calling } => {
- if contact.online && !contact.busy && !calling {
- self.call(contact.user.id, window, cx);
- }
+ if let Some(selection) = self.selection
+ && let Some(entry) = self.entries.get(selection)
+ {
+ match entry {
+ ListEntry::Header(section) => match section {
+ Section::ActiveCall => Self::leave_call(window, cx),
+ Section::Channels => self.new_root_channel(window, cx),
+ Section::Contacts => self.toggle_contact_finder(window, cx),
+ Section::ContactRequests
+ | Section::Online
+ | Section::Offline
+ | Section::ChannelInvites => {
+ self.toggle_section_expanded(*section, cx);
}
- ListEntry::ParticipantProject {
- project_id,
- host_user_id,
- ..
- } => {
- if let Some(workspace) = self.workspace.upgrade() {
- let app_state = workspace.read(cx).app_state().clone();
- workspace::join_in_room_project(
- *project_id,
- *host_user_id,
- app_state,
- cx,
- )
+ },
+ ListEntry::Contact { contact, calling } => {
+ if contact.online && !contact.busy && !calling {
+ self.call(contact.user.id, window, cx);
+ }
+ }
+ ListEntry::ParticipantProject {
+ project_id,
+ host_user_id,
+ ..
+ } => {
+ if let Some(workspace) = self.workspace.upgrade() {
+ let app_state = workspace.read(cx).app_state().clone();
+ workspace::join_in_room_project(*project_id, *host_user_id, app_state, cx)
.detach_and_prompt_err(
"Failed to join project",
window,
cx,
|_, _, _| None,
);
- }
}
- ListEntry::ParticipantScreen { peer_id, .. } => {
- let Some(peer_id) = peer_id else {
- return;
- };
- if let Some(workspace) = self.workspace.upgrade() {
- workspace.update(cx, |workspace, cx| {
- workspace.open_shared_screen(*peer_id, window, cx)
- });
- }
- }
- ListEntry::Channel { channel, .. } => {
- let is_active = maybe!({
- let call_channel = ActiveCall::global(cx)
- .read(cx)
- .room()?
- .read(cx)
- .channel_id()?;
-
- Some(call_channel == channel.id)
- })
- .unwrap_or(false);
- if is_active {
- self.open_channel_notes(channel.id, window, cx)
- } else {
- self.join_channel(channel.id, window, cx)
- }
- }
- ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx),
- ListEntry::CallParticipant { user, peer_id, .. } => {
- if Some(user) == self.user_store.read(cx).current_user().as_ref() {
- Self::leave_call(window, cx);
- } else if let Some(peer_id) = peer_id {
- self.workspace
- .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx))
- .ok();
- }
- }
- ListEntry::IncomingRequest(user) => {
- self.respond_to_contact_request(user.id, true, window, cx)
- }
- ListEntry::ChannelInvite(channel) => {
- self.respond_to_channel_invite(channel.id, true, cx)
+ }
+ ListEntry::ParticipantScreen { peer_id, .. } => {
+ let Some(peer_id) = peer_id else {
+ return;
+ };
+ if let Some(workspace) = self.workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ workspace.open_shared_screen(*peer_id, window, cx)
+ });
}
- ListEntry::ChannelNotes { channel_id } => {
- self.open_channel_notes(*channel_id, window, cx)
+ }
+ ListEntry::Channel { channel, .. } => {
+ let is_active = maybe!({
+ let call_channel = ActiveCall::global(cx)
+ .read(cx)
+ .room()?
+ .read(cx)
+ .channel_id()?;
+
+ Some(call_channel == channel.id)
+ })
+ .unwrap_or(false);
+ if is_active {
+ self.open_channel_notes(channel.id, window, cx)
+ } else {
+ self.join_channel(channel.id, window, cx)
}
- ListEntry::ChannelChat { channel_id } => {
- self.join_channel_chat(*channel_id, window, cx)
+ }
+ ListEntry::ContactPlaceholder => self.toggle_contact_finder(window, cx),
+ ListEntry::CallParticipant { user, peer_id, .. } => {
+ if Some(user) == self.user_store.read(cx).current_user().as_ref() {
+ Self::leave_call(window, cx);
+ } else if let Some(peer_id) = peer_id {
+ self.workspace
+ .update(cx, |workspace, cx| workspace.follow(*peer_id, window, cx))
+ .ok();
}
- ListEntry::OutgoingRequest(_) => {}
- ListEntry::ChannelEditor { .. } => {}
}
+ ListEntry::IncomingRequest(user) => {
+ self.respond_to_contact_request(user.id, true, window, cx)
+ }
+ ListEntry::ChannelInvite(channel) => {
+ self.respond_to_channel_invite(channel.id, true, cx)
+ }
+ ListEntry::ChannelNotes { channel_id } => {
+ self.open_channel_notes(*channel_id, window, cx)
+ }
+ ListEntry::OutgoingRequest(_) => {}
+ ListEntry::ChannelEditor { .. } => {}
}
}
}
@@ -1828,10 +1774,10 @@ impl CollabPanel {
}
fn select_channel_editor(&mut self) {
- self.selection = self.entries.iter().position(|entry| match entry {
- ListEntry::ChannelEditor { .. } => true,
- _ => false,
- });
+ self.selection = self
+ .entries
+ .iter()
+ .position(|entry| matches!(entry, ListEntry::ChannelEditor { .. }));
}
fn new_subchannel(
@@ -2265,28 +2211,6 @@ impl CollabPanel {
.detach_and_prompt_err("Failed to join channel", window, cx, |_, _, _| None)
}
- fn join_channel_chat(
- &mut self,
- channel_id: ChannelId,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let Some(workspace) = self.workspace.upgrade() else {
- return;
- };
- window.defer(cx, move |window, cx| {
- workspace.update(cx, |workspace, cx| {
- if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) {
- panel.update(cx, |panel, cx| {
- panel
- .select_channel(channel_id, None, cx)
- .detach_and_notify_err(window, cx);
- });
- }
- });
- });
- }
-
fn copy_channel_link(&mut self, channel_id: ChannelId, cx: &mut Context<Self>) {
let channel_store = self.channel_store.read(cx);
let Some(channel) = channel_store.channel_for_id(channel_id) else {
@@ -2317,7 +2241,7 @@ impl CollabPanel {
let client = this.client.clone();
cx.spawn_in(window, async move |_, cx| {
client
- .connect(true, &cx)
+ .connect(true, cx)
.await
.into_response()
.notify_async_err(cx);
@@ -2405,9 +2329,6 @@ impl CollabPanel {
ListEntry::ChannelNotes { channel_id } => self
.render_channel_notes(*channel_id, is_selected, window, cx)
.into_any_element(),
- ListEntry::ChannelChat { channel_id } => self
- .render_channel_chat(*channel_id, is_selected, window, cx)
- .into_any_element(),
}
}
@@ -2514,7 +2435,7 @@ impl CollabPanel {
let button = match section {
Section::ActiveCall => channel_link.map(|channel_link| {
- let channel_link_copy = channel_link.clone();
+ let channel_link_copy = channel_link;
IconButton::new("channel-link", IconName::Copy)
.icon_size(IconSize::Small)
.size(ButtonSize::None)
@@ -2698,7 +2619,7 @@ impl CollabPanel {
h_flex()
.w_full()
.justify_between()
- .child(Label::new(github_login.clone()))
+ .child(Label::new(github_login))
.child(h_flex().children(controls)),
)
.start_slot(Avatar::new(user.avatar_uri.clone()))
@@ -2788,7 +2709,6 @@ impl CollabPanel {
let disclosed =
has_children.then(|| self.collapsed_channels.binary_search(&channel.id).is_err());
- let has_messages_notification = channel_store.has_new_messages(channel_id);
let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
const FACEPILE_LIMIT: usize = 3;
@@ -2912,24 +2832,10 @@ impl CollabPanel {
h_flex().absolute().right(rems(0.)).h_full().child(
h_flex()
.h_full()
+ .bg(cx.theme().colors().background)
+ .rounded_l_sm()
.gap_1()
.px_1()
- .child(
- IconButton::new("channel_chat", IconName::Chat)
- .style(ButtonStyle::Filled)
- .shape(ui::IconButtonShape::Square)
- .icon_size(IconSize::Small)
- .icon_color(if has_messages_notification {
- Color::Default
- } else {
- Color::Muted
- })
- .on_click(cx.listener(move |this, _, window, cx| {
- this.join_channel_chat(channel_id, window, cx)
- }))
- .tooltip(Tooltip::text("Open channel chat"))
- .visible_on_hover(""),
- )
.child(
IconButton::new("channel_notes", IconName::Reader)
.style(ButtonStyle::Filled)
@@ -2943,9 +2849,9 @@ impl CollabPanel {
.on_click(cx.listener(move |this, _, window, cx| {
this.open_channel_notes(channel_id, window, cx)
}))
- .tooltip(Tooltip::text("Open channel notes"))
- .visible_on_hover(""),
- ),
+ .tooltip(Tooltip::text("Open channel notes")),
+ )
+ .visible_on_hover(""),
),
)
.tooltip({
@@ -3053,7 +2959,7 @@ impl Render for CollabPanel {
.on_action(cx.listener(CollabPanel::move_channel_down))
.track_focus(&self.focus_handle)
.size_full()
- .child(if !self.client.status().borrow().is_connected() {
+ .child(if !self.client.status().borrow().is_or_was_connected() {
self.render_signed_out(cx)
} else {
self.render_signed_in(window, cx)
@@ -3132,7 +3038,7 @@ impl Panel for CollabPanel {
impl Focusable for CollabPanel {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
- self.filter_editor.focus_handle(cx).clone()
+ self.filter_editor.focus_handle(cx)
}
}
@@ -3189,14 +3095,6 @@ impl PartialEq for ListEntry {
return channel_id == other_id;
}
}
- ListEntry::ChannelChat { channel_id } => {
- if let ListEntry::ChannelChat {
- channel_id: other_id,
- } = other
- {
- return channel_id == other_id;
- }
- }
ListEntry::ChannelInvite(channel_1) => {
if let ListEntry::ChannelInvite(channel_2) = other {
return channel_1.id == channel_2.id;
@@ -586,7 +586,7 @@ impl ChannelModalDelegate {
return;
};
let user_id = membership.user.id;
- let picker = cx.entity().clone();
+ let picker = cx.entity();
let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
let role = membership.role;
@@ -148,7 +148,7 @@ impl PickerDelegate for ContactFinderDelegate {
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let user = &self.potential_contacts[ix];
+ let user = &self.potential_contacts.get(ix)?;
let request_status = self.user_store.read(cx).contact_request_status(user);
let icon_path = match request_status {
@@ -1,5 +1,4 @@
pub mod channel_view;
-pub mod chat_panel;
pub mod collab_panel;
pub mod notification_panel;
pub mod notifications;
@@ -13,9 +12,7 @@ use gpui::{
WindowDecorations, WindowKind, WindowOptions, point,
};
use panel_settings::MessageEditorSettings;
-pub use panel_settings::{
- ChatPanelButton, ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
-};
+pub use panel_settings::{CollaborationPanelSettings, NotificationPanelSettings};
use release_channel::ReleaseChannel;
use settings::Settings;
use ui::px;
@@ -23,12 +20,10 @@ use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut App) {
CollaborationPanelSettings::register(cx);
- ChatPanelSettings::register(cx);
NotificationPanelSettings::register(cx);
MessageEditorSettings::register(cx);
channel_view::init(cx);
- chat_panel::init(cx);
collab_panel::init(cx);
notification_panel::init(cx);
notifications::init(app_state, cx);
@@ -66,5 +61,7 @@ fn notification_window_options(
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
+ tabbing_identifier: None,
+ ..Default::default()
}
}
@@ -1,4 +1,4 @@
-use crate::{NotificationPanelSettings, chat_panel::ChatPanel};
+use crate::NotificationPanelSettings;
use anyhow::Result;
use channel::ChannelStore;
use client::{ChannelId, Client, Notification, User, UserStore};
@@ -6,8 +6,8 @@ use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use futures::StreamExt;
use gpui::{
- AnyElement, App, AsyncWindowContext, ClickEvent, Context, CursorStyle, DismissEvent, Element,
- Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment,
+ AnyElement, App, AsyncWindowContext, ClickEvent, Context, DismissEvent, Element, Entity,
+ EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment,
ListScrollEvent, ListState, ParentElement, Render, StatefulInteractiveElement, Styled, Task,
WeakEntity, Window, actions, div, img, list, px,
};
@@ -71,7 +71,6 @@ pub struct NotificationPresenter {
pub text: String,
pub icon: &'static str,
pub needs_response: bool,
- pub can_navigate: bool,
}
actions!(
@@ -121,13 +120,12 @@ impl NotificationPanel {
let notification_list = ListState::new(0, ListAlignment::Top, px(1000.));
notification_list.set_scroll_handler(cx.listener(
|this, event: &ListScrollEvent, _, cx| {
- if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD {
- if let Some(task) = this
+ if event.count.saturating_sub(event.visible_range.end) < LOADING_THRESHOLD
+ && let Some(task) = this
.notification_store
.update(cx, |store, cx| store.load_more_notifications(false, cx))
- {
- task.detach();
- }
+ {
+ task.detach();
}
},
));
@@ -235,7 +233,6 @@ impl NotificationPanel {
actor,
text,
needs_response,
- can_navigate,
..
} = self.present_notification(entry, cx)?;
@@ -270,14 +267,6 @@ impl NotificationPanel {
.py_1()
.gap_2()
.hover(|style| style.bg(cx.theme().colors().element_hover))
- .when(can_navigate, |el| {
- el.cursor(CursorStyle::PointingHand).on_click({
- let notification = notification.clone();
- cx.listener(move |this, _, window, cx| {
- this.did_click_notification(¬ification, window, cx)
- })
- })
- })
.children(actor.map(|actor| {
img(actor.avatar_uri.clone())
.flex_none()
@@ -290,7 +279,7 @@ impl NotificationPanel {
.gap_1()
.size_full()
.overflow_hidden()
- .child(Label::new(text.clone()))
+ .child(Label::new(text))
.child(
h_flex()
.child(
@@ -321,7 +310,7 @@ impl NotificationPanel {
.justify_end()
.child(Button::new("decline", "Decline").on_click({
let notification = notification.clone();
- let entity = cx.entity().clone();
+ let entity = cx.entity();
move |_, _, cx| {
entity.update(cx, |this, cx| {
this.respond_to_notification(
@@ -334,7 +323,7 @@ impl NotificationPanel {
}))
.child(Button::new("accept", "Accept").on_click({
let notification = notification.clone();
- let entity = cx.entity().clone();
+ let entity = cx.entity();
move |_, _, cx| {
entity.update(cx, |this, cx| {
this.respond_to_notification(
@@ -370,7 +359,6 @@ impl NotificationPanel {
text: format!("{} wants to add you as a contact", requester.github_login),
needs_response: user_store.has_incoming_contact_request(requester.id),
actor: Some(requester),
- can_navigate: false,
})
}
Notification::ContactRequestAccepted { responder_id } => {
@@ -380,7 +368,6 @@ impl NotificationPanel {
text: format!("{} accepted your contact invite", responder.github_login),
needs_response: false,
actor: Some(responder),
- can_navigate: false,
})
}
Notification::ChannelInvitation {
@@ -397,29 +384,6 @@ impl NotificationPanel {
),
needs_response: channel_store.has_channel_invitation(ChannelId(channel_id)),
actor: Some(inviter),
- can_navigate: false,
- })
- }
- Notification::ChannelMessageMention {
- sender_id,
- channel_id,
- message_id,
- } => {
- let sender = user_store.get_cached_user(sender_id)?;
- let channel = channel_store.channel_for_id(ChannelId(channel_id))?;
- let message = self
- .notification_store
- .read(cx)
- .channel_message_for_id(message_id)?;
- Some(NotificationPresenter {
- icon: "icons/conversations.svg",
- text: format!(
- "{} mentioned you in #{}:\n{}",
- sender.github_login, channel.name, message.body,
- ),
- needs_response: false,
- actor: Some(sender),
- can_navigate: true,
})
}
}
@@ -434,9 +398,7 @@ impl NotificationPanel {
) {
let should_mark_as_read = match notification {
Notification::ContactRequestAccepted { .. } => true,
- Notification::ContactRequest { .. }
- | Notification::ChannelInvitation { .. }
- | Notification::ChannelMessageMention { .. } => false,
+ Notification::ContactRequest { .. } | Notification::ChannelInvitation { .. } => false,
};
if should_mark_as_read {
@@ -458,56 +420,6 @@ impl NotificationPanel {
}
}
- fn did_click_notification(
- &mut self,
- notification: &Notification,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if let Notification::ChannelMessageMention {
- message_id,
- channel_id,
- ..
- } = notification.clone()
- {
- if let Some(workspace) = self.workspace.upgrade() {
- window.defer(cx, move |window, cx| {
- workspace.update(cx, |workspace, cx| {
- if let Some(panel) = workspace.focus_panel::<ChatPanel>(window, cx) {
- panel.update(cx, |panel, cx| {
- panel
- .select_channel(ChannelId(channel_id), Some(message_id), cx)
- .detach_and_log_err(cx);
- });
- }
- });
- });
- }
- }
- }
-
- fn is_showing_notification(&self, notification: &Notification, cx: &mut Context<Self>) -> bool {
- if !self.active {
- return false;
- }
-
- if let Notification::ChannelMessageMention { channel_id, .. } = ¬ification {
- if let Some(workspace) = self.workspace.upgrade() {
- return if let Some(panel) = workspace.read(cx).panel::<ChatPanel>(cx) {
- let panel = panel.read(cx);
- panel.is_scrolled_to_bottom()
- && panel
- .active_chat()
- .map_or(false, |chat| chat.read(cx).channel_id.0 == *channel_id)
- } else {
- false
- };
- }
- }
-
- false
- }
-
fn on_notification_event(
&mut self,
_: &Entity<NotificationStore>,
@@ -517,9 +429,7 @@ impl NotificationPanel {
) {
match event {
NotificationEvent::NewNotification { entry } => {
- if !self.is_showing_notification(&entry.notification, cx) {
- self.unseen_notifications.push(entry.clone());
- }
+ self.unseen_notifications.push(entry.clone());
self.add_toast(entry, window, cx);
}
NotificationEvent::NotificationRemoved { entry }
@@ -543,10 +453,6 @@ impl NotificationPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
- if self.is_showing_notification(&entry.notification, cx) {
- return;
- }
-
let Some(NotificationPresenter { actor, text, .. }) = self.present_notification(entry, cx)
else {
return;
@@ -570,7 +476,6 @@ impl NotificationPanel {
workspace.show_notification(id, cx, |cx| {
let workspace = cx.entity().downgrade();
cx.new(|cx| NotificationToast {
- notification_id,
actor,
text,
workspace,
@@ -582,16 +487,16 @@ impl NotificationPanel {
}
fn remove_toast(&mut self, notification_id: u64, cx: &mut Context<Self>) {
- if let Some((current_id, _)) = &self.current_notification_toast {
- if *current_id == notification_id {
- self.current_notification_toast.take();
- self.workspace
- .update(cx, |workspace, cx| {
- let id = NotificationId::unique::<NotificationToast>();
- workspace.dismiss_notification(&id, cx)
- })
- .ok();
- }
+ if let Some((current_id, _)) = &self.current_notification_toast
+ && *current_id == notification_id
+ {
+ self.current_notification_toast.take();
+ self.workspace
+ .update(cx, |workspace, cx| {
+ let id = NotificationId::unique::<NotificationToast>();
+ workspace.dismiss_notification(&id, cx)
+ })
+ .ok();
}
}
@@ -643,7 +548,7 @@ impl Render for NotificationPanel {
let client = client.clone();
window
.spawn(cx, async move |cx| {
- match client.connect(true, &cx).await {
+ match client.connect(true, cx).await {
util::ConnectionResult::Timeout => {
log::error!("Connection timeout");
}
@@ -783,7 +688,6 @@ impl Panel for NotificationPanel {
}
pub struct NotificationToast {
- notification_id: u64,
actor: Option<Arc<User>>,
text: String,
workspace: WeakEntity<Workspace>,
@@ -801,22 +705,10 @@ impl WorkspaceNotification for NotificationToast {}
impl NotificationToast {
fn focus_notification_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
let workspace = self.workspace.clone();
- let notification_id = self.notification_id;
window.defer(cx, move |window, cx| {
workspace
.update(cx, |workspace, cx| {
- if let Some(panel) = workspace.focus_panel::<NotificationPanel>(window, cx) {
- panel.update(cx, |panel, cx| {
- let store = panel.notification_store.read(cx);
- if let Some(entry) = store.notification_for_id(notification_id) {
- panel.did_click_notification(
- &entry.clone().notification,
- window,
- cx,
- );
- }
- });
- }
+ workspace.focus_panel::<NotificationPanel>(window, cx)
})
.ok();
})
@@ -1,7 +1,7 @@
use gpui::Pixels;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use workspace::dock::DockPosition;
#[derive(Deserialize, Debug)]
@@ -11,31 +11,16 @@ pub struct CollaborationPanelSettings {
pub default_width: Pixels,
}
-#[derive(Clone, Copy, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[serde(rename_all = "snake_case")]
-pub enum ChatPanelButton {
- Never,
- Always,
- #[default]
- WhenInCall,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct ChatPanelSettings {
- pub button: ChatPanelButton,
- pub dock: DockPosition,
- pub default_width: Pixels,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-pub struct ChatPanelSettingsContent {
- /// When to show the panel button in the status bar.
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "collaboration_panel")]
+pub struct PanelSettingsContent {
+ /// Whether to show the panel button in the status bar.
///
- /// Default: only when in a call
- pub button: Option<ChatPanelButton>,
+ /// Default: true
+ pub button: Option<bool>,
/// Where to dock the panel.
///
- /// Default: right
+ /// Default: left
pub dock: Option<DockPosition>,
/// Default width of the panel in pixels.
///
@@ -50,23 +35,25 @@ pub struct NotificationPanelSettings {
pub default_width: Pixels,
}
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-pub struct PanelSettingsContent {
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "notification_panel")]
+pub struct NotificationPanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Where to dock the panel.
///
- /// Default: left
+ /// Default: right
pub dock: Option<DockPosition>,
/// Default width of the panel in pixels.
///
- /// Default: 240
+ /// Default: 300
pub default_width: Option<f32>,
}
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "message_editor")]
pub struct MessageEditorSettings {
/// Whether to automatically replace emoji shortcodes with emoji characters.
/// For example: typing `:wave:` gets replaced with `👋`.
@@ -76,8 +63,6 @@ pub struct MessageEditorSettings {
}
impl Settings for CollaborationPanelSettings {
- const KEY: Option<&'static str> = Some("collaboration_panel");
-
type FileContent = PanelSettingsContent;
fn load(
@@ -90,25 +75,8 @@ impl Settings for CollaborationPanelSettings {
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
-impl Settings for ChatPanelSettings {
- const KEY: Option<&'static str> = Some("chat_panel");
-
- type FileContent = ChatPanelSettingsContent;
-
- fn load(
- sources: SettingsSources<Self::FileContent>,
- _: &mut gpui::App,
- ) -> anyhow::Result<Self> {
- sources.json_merge()
- }
-
- fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
-}
-
impl Settings for NotificationPanelSettings {
- const KEY: Option<&'static str> = Some("notification_panel");
-
- type FileContent = PanelSettingsContent;
+ type FileContent = NotificationPanelSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
@@ -121,8 +89,6 @@ impl Settings for NotificationPanelSettings {
}
impl Settings for MessageEditorSettings {
- const KEY: Option<&'static str> = Some("message_editor");
-
type FileContent = MessageEditorSettings;
fn load(
@@ -206,7 +206,7 @@ impl CommandPaletteDelegate {
if parse_zed_link(&query, cx).is_some() {
intercept_results = vec![CommandInterceptResult {
action: OpenZedUrl { url: query.clone() }.boxed_clone(),
- string: query.clone(),
+ string: query,
positions: vec![],
}]
}
@@ -1,7 +1,10 @@
use anyhow::Result;
use db::{
- define_connection, query,
- sqlez::{bindable::Column, statement::Statement},
+ query,
+ sqlez::{
+ bindable::Column, domain::Domain, statement::Statement,
+ thread_safe_connection::ThreadSafeConnection,
+ },
sqlez_macros::sql,
};
use serde::{Deserialize, Serialize};
@@ -50,8 +53,11 @@ impl Column for SerializedCommandInvocation {
}
}
-define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
- &[sql!(
+pub struct CommandPaletteDB(ThreadSafeConnection);
+
+impl Domain for CommandPaletteDB {
+ const NAME: &str = stringify!(CommandPaletteDB);
+ const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE IF NOT EXISTS command_invocations(
id INTEGER PRIMARY KEY AUTOINCREMENT,
command_name TEXT NOT NULL,
@@ -59,7 +65,9 @@ define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()>
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
) STRICT;
)];
-);
+}
+
+db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
impl CommandPaletteDB {
pub async fn write_command_invocation(
@@ -76,7 +76,7 @@ impl CommandPaletteFilter {
}
/// Hides all actions with the given types.
- pub fn hide_action_types(&mut self, action_types: &[TypeId]) {
+ pub fn hide_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
for action_type in action_types {
self.hidden_action_types.insert(*action_type);
self.shown_action_types.remove(action_type);
@@ -84,7 +84,7 @@ impl CommandPaletteFilter {
}
/// Shows all actions with the given types.
- pub fn show_action_types<'a>(&mut self, action_types: impl Iterator<Item = &'a TypeId>) {
+ pub fn show_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
for action_type in action_types {
self.shown_action_types.insert(*action_type);
self.hidden_action_types.remove(action_type);
@@ -20,5 +20,8 @@ strum.workspace = true
theme.workspace = true
workspace-hack.workspace = true
+[dev-dependencies]
+documented.workspace = true
+
[features]
default = []
@@ -227,6 +227,8 @@ pub trait Component {
/// Example:
///
/// ```
+ /// use documented::Documented;
+ ///
/// /// This is a doc comment.
/// #[derive(Documented)]
/// struct MyComponent;
@@ -42,7 +42,7 @@ impl RenderOnce for ComponentExample {
div()
.text_size(rems(0.875))
.text_color(cx.theme().colors().text_muted)
- .child(description.clone()),
+ .child(description),
)
}),
)
@@ -25,7 +25,7 @@ use crate::{
};
const JSON_RPC_VERSION: &str = "2.0";
-const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
+const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
// Standard JSON-RPC error codes
pub const PARSE_ERROR: i32 = -32700;
@@ -60,6 +60,7 @@ pub(crate) struct Client {
executor: BackgroundExecutor,
#[allow(dead_code)]
transport: Arc<dyn Transport>,
+ request_timeout: Option<Duration>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -67,11 +68,7 @@ pub(crate) struct Client {
pub(crate) struct ContextServerId(pub Arc<str>);
fn is_null_value<T: Serialize>(value: &T) -> bool {
- if let Ok(Value::Null) = serde_json::to_value(value) {
- true
- } else {
- false
- }
+ matches!(serde_json::to_value(value), Ok(Value::Null))
}
#[derive(Serialize, Deserialize)]
@@ -147,6 +144,7 @@ pub struct ModelContextServerBinary {
pub executable: PathBuf,
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
+ pub timeout: Option<u64>,
}
impl Client {
@@ -161,7 +159,7 @@ impl Client {
working_directory: &Option<PathBuf>,
cx: AsyncApp,
) -> Result<Self> {
- log::info!(
+ log::debug!(
"starting context server (executable={:?}, args={:?})",
binary.executable,
&binary.args
@@ -173,8 +171,9 @@ impl Client {
.map(|name| name.to_string_lossy().to_string())
.unwrap_or_else(String::new);
+ let timeout = binary.timeout.map(Duration::from_millis);
let transport = Arc::new(StdioTransport::new(binary, working_directory, &cx)?);
- Self::new(server_id, server_name.into(), transport, cx)
+ Self::new(server_id, server_name.into(), transport, timeout, cx)
}
/// Creates a new Client instance for a context server.
@@ -182,6 +181,7 @@ impl Client {
server_id: ContextServerId,
server_name: Arc<str>,
transport: Arc<dyn Transport>,
+ request_timeout: Option<Duration>,
cx: AsyncApp,
) -> Result<Self> {
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
@@ -241,6 +241,7 @@ impl Client {
io_tasks: Mutex::new(Some((input_task, output_task))),
output_done_rx: Mutex::new(Some(output_done_rx)),
transport,
+ request_timeout,
})
}
@@ -271,10 +272,10 @@ impl Client {
);
}
} else if let Ok(response) = serde_json::from_str::<AnyResponse>(&message) {
- if let Some(handlers) = response_handlers.lock().as_mut() {
- if let Some(handler) = handlers.remove(&response.id) {
- handler(Ok(message.to_string()));
- }
+ if let Some(handlers) = response_handlers.lock().as_mut()
+ && let Some(handler) = handlers.remove(&response.id)
+ {
+ handler(Ok(message.to_string()));
}
} else if let Ok(notification) = serde_json::from_str::<AnyNotification>(&message) {
let mut notification_handlers = notification_handlers.lock();
@@ -295,7 +296,7 @@ impl Client {
/// Continuously reads and logs any error messages from the server.
async fn handle_err(transport: Arc<dyn Transport>) -> anyhow::Result<()> {
while let Some(err) = transport.receive_err().next().await {
- log::warn!("context server stderr: {}", err.trim());
+ log::debug!("context server stderr: {}", err.trim());
}
Ok(())
@@ -331,8 +332,13 @@ impl Client {
method: &str,
params: impl Serialize,
) -> Result<T> {
- self.request_with(method, params, None, Some(REQUEST_TIMEOUT))
- .await
+ self.request_with(
+ method,
+ params,
+ None,
+ self.request_timeout.or(Some(DEFAULT_REQUEST_TIMEOUT)),
+ )
+ .await
}
pub async fn request_with<T: DeserializeOwned>(
@@ -34,6 +34,8 @@ pub struct ContextServerCommand {
pub path: PathBuf,
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
+ /// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified.
+ pub timeout: Option<u64>,
}
impl std::fmt::Debug for ContextServerCommand {
@@ -123,6 +125,7 @@ impl ContextServer {
executable: Path::new(&command.path).to_path_buf(),
args: command.args.clone(),
env: command.env.clone(),
+ timeout: command.timeout,
},
working_directory,
cx.clone(),
@@ -131,13 +134,14 @@ impl ContextServer {
client::ContextServerId(self.id.0.clone()),
self.id().0,
transport.clone(),
+ None,
cx.clone(),
)?,
})
}
async fn initialize(&self, client: Client) -> Result<()> {
- log::info!("starting context server {}", self.id);
+ log::debug!("starting context server {}", self.id);
let protocol = crate::protocol::ModelContextProtocol::new(client);
let client_info = types::Implementation {
name: "Zed".to_string(),
@@ -14,6 +14,7 @@ use serde::de::DeserializeOwned;
use serde_json::{json, value::RawValue};
use smol::stream::StreamExt;
use std::{
+ any::TypeId,
cell::RefCell,
path::{Path, PathBuf},
rc::Rc,
@@ -77,7 +78,7 @@ impl McpServer {
socket_path,
_server_task: server_task,
tools,
- handlers: handlers,
+ handlers,
})
})
}
@@ -87,23 +88,30 @@ impl McpServer {
settings.inline_subschemas = true;
let mut generator = settings.into_generator();
- let output_schema = generator.root_schema_for::<T::Output>();
- let unit_schema = generator.root_schema_for::<T::Output>();
+ let input_schema = generator.root_schema_for::<T::Input>();
+
+ let description = input_schema
+ .get("description")
+ .and_then(|desc| desc.as_str())
+ .map(|desc| desc.to_string());
+ debug_assert!(
+ description.is_some(),
+ "Input schema struct must include a doc comment for the tool description"
+ );
let registered_tool = RegisteredTool {
tool: Tool {
name: T::NAME.into(),
- description: Some(tool.description().into()),
- input_schema: generator.root_schema_for::<T::Input>().into(),
- output_schema: if output_schema == unit_schema {
+ description,
+ input_schema: input_schema.into(),
+ output_schema: if TypeId::of::<T::Output>() == TypeId::of::<()>() {
None
} else {
- Some(output_schema.into())
+ Some(generator.root_schema_for::<T::Output>().into())
},
annotations: Some(tool.annotations()),
},
handler: Box::new({
- let tool = tool.clone();
move |input_value, cx| {
let input = match input_value {
Some(input) => serde_json::from_value(input),
@@ -315,12 +323,12 @@ impl McpServer {
Self::send_err(
request_id,
format!("Tool not found: {}", params.name),
- &outgoing_tx,
+ outgoing_tx,
);
}
}
Err(err) => {
- Self::send_err(request_id, err.to_string(), &outgoing_tx);
+ Self::send_err(request_id, err.to_string(), outgoing_tx);
}
}
}
@@ -399,8 +407,6 @@ pub trait McpServerTool {
const NAME: &'static str;
- fn description(&self) -> &'static str;
-
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations {
title: None,
@@ -418,6 +424,7 @@ pub trait McpServerTool {
) -> impl Future<Output = Result<ToolResponse<Self::Output>>>;
}
+#[derive(Debug)]
pub struct ToolResponse<T> {
pub content: Vec<ToolResponseContent>,
pub structured_content: T,
@@ -1,6 +1,6 @@
use anyhow::Context as _;
use collections::HashMap;
-use futures::{Stream, StreamExt as _, lock::Mutex};
+use futures::{FutureExt, Stream, StreamExt as _, future::BoxFuture, lock::Mutex};
use gpui::BackgroundExecutor;
use std::{pin::Pin, sync::Arc};
@@ -14,9 +14,12 @@ pub fn create_fake_transport(
executor: BackgroundExecutor,
) -> FakeTransport {
let name = name.into();
- FakeTransport::new(executor).on_request::<crate::types::requests::Initialize>(move |_params| {
- create_initialize_response(name.clone())
- })
+ FakeTransport::new(executor).on_request::<crate::types::requests::Initialize, _>(
+ move |_params| {
+ let name = name.clone();
+ async move { create_initialize_response(name.clone()) }
+ },
+ )
}
fn create_initialize_response(server_name: String) -> InitializeResponse {
@@ -32,8 +35,10 @@ fn create_initialize_response(server_name: String) -> InitializeResponse {
}
pub struct FakeTransport {
- request_handlers:
- HashMap<&'static str, Arc<dyn Fn(serde_json::Value) -> serde_json::Value + Send + Sync>>,
+ request_handlers: HashMap<
+ &'static str,
+ Arc<dyn Send + Sync + Fn(serde_json::Value) -> BoxFuture<'static, serde_json::Value>>,
+ >,
tx: futures::channel::mpsc::UnboundedSender<String>,
rx: Arc<Mutex<futures::channel::mpsc::UnboundedReceiver<String>>>,
executor: BackgroundExecutor,
@@ -50,18 +55,25 @@ impl FakeTransport {
}
}
- pub fn on_request<T: crate::types::Request>(
+ pub fn on_request<T, Fut>(
mut self,
- handler: impl Fn(T::Params) -> T::Response + Send + Sync + 'static,
- ) -> Self {
+ handler: impl 'static + Send + Sync + Fn(T::Params) -> Fut,
+ ) -> Self
+ where
+ T: crate::types::Request,
+ Fut: 'static + Send + Future<Output = T::Response>,
+ {
self.request_handlers.insert(
T::METHOD,
Arc::new(move |value| {
- let params = value.get("params").expect("Missing parameters").clone();
+ let params = value
+ .get("params")
+ .cloned()
+ .unwrap_or(serde_json::Value::Null);
let params: T::Params =
serde_json::from_value(params).expect("Invalid parameters received");
let response = handler(params);
- serde_json::to_value(response).unwrap()
+ async move { serde_json::to_value(response.await).unwrap() }.boxed()
}),
);
self
@@ -77,7 +89,7 @@ impl Transport for FakeTransport {
if let Some(method) = msg.get("method") {
let method = method.as_str().expect("Invalid method received");
if let Some(handler) = self.request_handlers.get(method) {
- let payload = handler(msg);
+ let payload = handler(msg).await;
let response = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
@@ -691,7 +691,7 @@ impl CallToolResponse {
let mut text = String::new();
for chunk in &self.content {
if let ToolResponseContent::Text { text: chunk } = chunk {
- text.push_str(&chunk)
+ text.push_str(chunk)
};
}
text
@@ -711,6 +711,16 @@ pub enum ToolResponseContent {
Resource { resource: ResourceContents },
}
+impl ToolResponseContent {
+ pub fn text(&self) -> Option<&str> {
+ if let ToolResponseContent::Text { text } = self {
+ Some(text)
+ } else {
+ None
+ }
+ }
+}
+
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListToolsResponse {
@@ -21,7 +21,7 @@ use language::{
point_from_lsp, point_to_lsp,
};
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
-use node_runtime::{NodeRuntime, VersionCheck};
+use node_runtime::{NodeRuntime, VersionStrategy};
use parking_lot::Mutex;
use project::DisableAiSettings;
use request::StatusNotification;
@@ -81,10 +81,7 @@ pub fn init(
};
copilot_chat::init(fs.clone(), http.clone(), configuration, cx);
- let copilot = cx.new({
- let node_runtime = node_runtime.clone();
- move |cx| Copilot::start(new_server_id, fs, node_runtime, cx)
- });
+ let copilot = cx.new(move |cx| Copilot::start(new_server_id, fs, node_runtime, cx));
Copilot::set_global(copilot.clone(), cx);
cx.observe(&copilot, |copilot, cx| {
copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx));
@@ -129,7 +126,7 @@ impl CopilotServer {
fn as_authenticated(&mut self) -> Result<&mut RunningCopilotServer> {
let server = self.as_running()?;
anyhow::ensure!(
- matches!(server.sign_in_status, SignInStatus::Authorized { .. }),
+ matches!(server.sign_in_status, SignInStatus::Authorized),
"must sign in before using copilot"
);
Ok(server)
@@ -200,7 +197,7 @@ impl Status {
}
struct RegisteredBuffer {
- uri: lsp::Url,
+ uri: lsp::Uri,
language_id: String,
snapshot: BufferSnapshot,
snapshot_version: i32,
@@ -349,7 +346,11 @@ impl Copilot {
this.start_copilot(true, false, cx);
cx.observe_global::<SettingsStore>(move |this, cx| {
this.start_copilot(true, false, cx);
- this.send_configuration_update(cx);
+ if let Ok(server) = this.server.as_running() {
+ notify_did_change_config_to_server(&server.lsp, cx)
+ .context("copilot setting change: did change configuration")
+ .log_err();
+ }
})
.detach();
this
@@ -438,43 +439,6 @@ impl Copilot {
if env.is_empty() { None } else { Some(env) }
}
- fn send_configuration_update(&mut self, cx: &mut Context<Self>) {
- let copilot_settings = all_language_settings(None, cx)
- .edit_predictions
- .copilot
- .clone();
-
- let settings = json!({
- "http": {
- "proxy": copilot_settings.proxy,
- "proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false)
- },
- "github-enterprise": {
- "uri": copilot_settings.enterprise_uri
- }
- });
-
- if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) {
- copilot_chat.update(cx, |chat, cx| {
- chat.set_configuration(
- copilot_chat::CopilotChatConfiguration {
- enterprise_uri: copilot_settings.enterprise_uri.clone(),
- },
- cx,
- );
- });
- }
-
- if let Ok(server) = self.server.as_running() {
- server
- .lsp
- .notify::<lsp::notification::DidChangeConfiguration>(
- &lsp::DidChangeConfigurationParams { settings },
- )
- .log_err();
- }
- }
-
#[cfg(any(test, feature = "test-support"))]
pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
use fs::FakeFs;
@@ -573,6 +537,9 @@ impl Copilot {
})?
.await?;
+ this.update(cx, |_, cx| notify_did_change_config_to_server(&server, cx))?
+ .context("copilot: did change configuration")?;
+
let status = server
.request::<request::CheckStatus>(request::CheckStatusParams {
local_checks_only: false,
@@ -598,8 +565,6 @@ impl Copilot {
});
cx.emit(Event::CopilotLanguageServerStarted);
this.update_sign_in_status(status, cx);
- // Send configuration now that the LSP is fully started
- this.send_configuration_update(cx);
}
Err(error) => {
this.server = CopilotServer::Error(error.to_string().into());
@@ -613,12 +578,12 @@ impl Copilot {
pub(crate) fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
if let CopilotServer::Running(server) = &mut self.server {
let task = match &server.sign_in_status {
- SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(),
+ SignInStatus::Authorized => Task::ready(Ok(())).shared(),
SignInStatus::SigningIn { task, .. } => {
cx.notify();
task.clone()
}
- SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized { .. } => {
+ SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized => {
let lsp = server.lsp.clone();
let task = cx
.spawn(async move |this, cx| {
@@ -640,15 +605,13 @@ impl Copilot {
sign_in_status: status,
..
}) = &mut this.server
- {
- if let SignInStatus::SigningIn {
+ && let SignInStatus::SigningIn {
prompt: prompt_flow,
..
} = status
- {
- *prompt_flow = Some(flow.clone());
- cx.notify();
- }
+ {
+ *prompt_flow = Some(flow.clone());
+ cx.notify();
}
})?;
let response = lsp
@@ -764,7 +727,7 @@ impl Copilot {
..
}) = &mut self.server
{
- if !matches!(status, SignInStatus::Authorized { .. }) {
+ if !matches!(status, SignInStatus::Authorized) {
return;
}
@@ -814,59 +777,58 @@ impl Copilot {
event: &language::BufferEvent,
cx: &mut Context<Self>,
) -> Result<()> {
- if let Ok(server) = self.server.as_running() {
- if let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id())
- {
- match event {
- language::BufferEvent::Edited => {
- drop(registered_buffer.report_changes(&buffer, cx));
- }
- language::BufferEvent::Saved => {
+ if let Ok(server) = self.server.as_running()
+ && let Some(registered_buffer) = server.registered_buffers.get_mut(&buffer.entity_id())
+ {
+ match event {
+ language::BufferEvent::Edited => {
+ drop(registered_buffer.report_changes(&buffer, cx));
+ }
+ language::BufferEvent::Saved => {
+ server
+ .lsp
+ .notify::<lsp::notification::DidSaveTextDocument>(
+ &lsp::DidSaveTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(
+ registered_buffer.uri.clone(),
+ ),
+ text: None,
+ },
+ )?;
+ }
+ language::BufferEvent::FileHandleChanged
+ | language::BufferEvent::LanguageChanged => {
+ let new_language_id = id_for_language(buffer.read(cx).language());
+ let Ok(new_uri) = uri_for_buffer(&buffer, cx) else {
+ return Ok(());
+ };
+ if new_uri != registered_buffer.uri
+ || new_language_id != registered_buffer.language_id
+ {
+ let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
+ registered_buffer.language_id = new_language_id;
server
.lsp
- .notify::<lsp::notification::DidSaveTextDocument>(
- &lsp::DidSaveTextDocumentParams {
- text_document: lsp::TextDocumentIdentifier::new(
+ .notify::<lsp::notification::DidCloseTextDocument>(
+ &lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(old_uri),
+ },
+ )?;
+ server
+ .lsp
+ .notify::<lsp::notification::DidOpenTextDocument>(
+ &lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
registered_buffer.uri.clone(),
+ registered_buffer.language_id.clone(),
+ registered_buffer.snapshot_version,
+ registered_buffer.snapshot.text(),
),
- text: None,
},
)?;
}
- language::BufferEvent::FileHandleChanged
- | language::BufferEvent::LanguageChanged => {
- let new_language_id = id_for_language(buffer.read(cx).language());
- let Ok(new_uri) = uri_for_buffer(&buffer, cx) else {
- return Ok(());
- };
- if new_uri != registered_buffer.uri
- || new_language_id != registered_buffer.language_id
- {
- let old_uri = mem::replace(&mut registered_buffer.uri, new_uri);
- registered_buffer.language_id = new_language_id;
- server
- .lsp
- .notify::<lsp::notification::DidCloseTextDocument>(
- &lsp::DidCloseTextDocumentParams {
- text_document: lsp::TextDocumentIdentifier::new(old_uri),
- },
- )?;
- server
- .lsp
- .notify::<lsp::notification::DidOpenTextDocument>(
- &lsp::DidOpenTextDocumentParams {
- text_document: lsp::TextDocumentItem::new(
- registered_buffer.uri.clone(),
- registered_buffer.language_id.clone(),
- registered_buffer.snapshot_version,
- registered_buffer.snapshot.text(),
- ),
- },
- )?;
- }
- }
- _ => {}
}
+ _ => {}
}
}
@@ -874,17 +836,17 @@ impl Copilot {
}
fn unregister_buffer(&mut self, buffer: &WeakEntity<Buffer>) {
- if let Ok(server) = self.server.as_running() {
- if let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id()) {
- server
- .lsp
- .notify::<lsp::notification::DidCloseTextDocument>(
- &lsp::DidCloseTextDocumentParams {
- text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
- },
- )
- .ok();
- }
+ if let Ok(server) = self.server.as_running()
+ && let Some(buffer) = server.registered_buffers.remove(&buffer.entity_id())
+ {
+ server
+ .lsp
+ .notify::<lsp::notification::DidCloseTextDocument>(
+ &lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
+ },
+ )
+ .ok();
}
}
@@ -1047,8 +1009,8 @@ impl Copilot {
CopilotServer::Error(error) => Status::Error(error.clone()),
CopilotServer::Running(RunningCopilotServer { sign_in_status, .. }) => {
match sign_in_status {
- SignInStatus::Authorized { .. } => Status::Authorized,
- SignInStatus::Unauthorized { .. } => Status::Unauthorized,
+ SignInStatus::Authorized => Status::Authorized,
+ SignInStatus::Unauthorized => Status::Unauthorized,
SignInStatus::SigningIn { prompt, .. } => Status::SigningIn {
prompt: prompt.clone(),
},
@@ -1133,7 +1095,7 @@ impl Copilot {
_ => {
filter.hide_action_types(&signed_in_actions);
filter.hide_action_types(&auth_actions);
- filter.show_action_types(no_auth_actions.iter());
+ filter.show_action_types(&no_auth_actions);
}
}
}
@@ -1146,9 +1108,9 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
.unwrap_or_else(|| "plaintext".to_string())
}
-fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> {
+fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Uri, ()> {
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
- lsp::Url::from_file_path(file.abs_path(cx))
+ lsp::Uri::from_file_path(file.abs_path(cx))
} else {
format!("buffer://{}", buffer.entity_id())
.parse()
@@ -1156,6 +1118,41 @@ fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> {
}
}
+fn notify_did_change_config_to_server(
+ server: &Arc<LanguageServer>,
+ cx: &mut Context<Copilot>,
+) -> std::result::Result<(), anyhow::Error> {
+ let copilot_settings = all_language_settings(None, cx)
+ .edit_predictions
+ .copilot
+ .clone();
+
+ if let Some(copilot_chat) = copilot_chat::CopilotChat::global(cx) {
+ copilot_chat.update(cx, |chat, cx| {
+ chat.set_configuration(
+ copilot_chat::CopilotChatConfiguration {
+ enterprise_uri: copilot_settings.enterprise_uri.clone(),
+ },
+ cx,
+ );
+ });
+ }
+
+ let settings = json!({
+ "http": {
+ "proxy": copilot_settings.proxy,
+ "proxyStrictSSL": !copilot_settings.proxy_no_verify.unwrap_or(false)
+ },
+ "github-enterprise": {
+ "uri": copilot_settings.enterprise_uri
+ }
+ });
+
+ server.notify::<lsp::notification::DidChangeConfiguration>(&lsp::DidChangeConfigurationParams {
+ settings,
+ })
+}
+
async fn clear_copilot_dir() {
remove_matching(paths::copilot_dir(), |_| true).await
}
@@ -1169,8 +1166,9 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
const SERVER_PATH: &str =
"node_modules/@github/copilot-language-server/dist/language-server.js";
- // pinning it: https://github.com/zed-industries/zed/issues/36093
- const PINNED_VERSION: &str = "1.354";
+ let latest_version = node_runtime
+ .npm_package_latest_version(PACKAGE_NAME)
+ .await?;
let server_path = paths::copilot_dir().join(SERVER_PATH);
fs.create_dir(paths::copilot_dir()).await?;
@@ -1180,13 +1178,12 @@ async fn get_copilot_lsp(fs: Arc<dyn Fs>, node_runtime: NodeRuntime) -> anyhow::
PACKAGE_NAME,
&server_path,
paths::copilot_dir(),
- &PINNED_VERSION,
- VersionCheck::VersionMismatch,
+ VersionStrategy::Latest(&latest_version),
)
.await;
if should_install {
node_runtime
- .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &PINNED_VERSION)])
+ .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)])
.await?;
}
@@ -1204,7 +1201,7 @@ mod tests {
let (copilot, mut lsp) = Copilot::fake(cx);
let buffer_1 = cx.new(|cx| Buffer::local("Hello", cx));
- let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
+ let buffer_1_uri: lsp::Uri = format!("buffer://{}", buffer_1.entity_id().as_u64())
.parse()
.unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
@@ -1222,7 +1219,7 @@ mod tests {
);
let buffer_2 = cx.new(|cx| Buffer::local("Goodbye", cx));
- let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
+ let buffer_2_uri: lsp::Uri = format!("buffer://{}", buffer_2.entity_id().as_u64())
.parse()
.unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
@@ -1273,7 +1270,7 @@ mod tests {
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
}
);
- let buffer_1_uri = lsp::Url::from_file_path(path!("/root/child/buffer-1")).unwrap();
+ let buffer_1_uri = lsp::Uri::from_file_path(path!("/root/child/buffer-1")).unwrap();
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await,
@@ -62,12 +62,6 @@ impl CopilotChatConfiguration {
}
}
-// Copilot's base model; defined by Microsoft in premium requests table
-// This will be moved to the front of the Copilot model list, and will be used for
-// 'fast' requests (e.g. title generation)
-// https://docs.github.com/en/copilot/managing-copilot/monitoring-usage-and-entitlements/about-premium-requests
-const DEFAULT_MODEL_ID: &str = "gpt-4.1";
-
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
@@ -101,22 +95,41 @@ where
Ok(models)
}
-#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Model {
+ billing: ModelBilling,
capabilities: ModelCapabilities,
id: String,
name: String,
policy: Option<ModelPolicy>,
vendor: ModelVendor,
+ is_chat_default: bool,
+ // The model with this value true is selected by VSCode copilot if a premium request limit is
+ // reached. Zed does not currently implement this behaviour
+ is_chat_fallback: bool,
model_picker_enabled: bool,
}
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
+struct ModelBilling {
+ is_premium: bool,
+ multiplier: f64,
+ // List of plans a model is restricted to
+ // Field is not present if a model is available for all plans
+ #[serde(default)]
+ restricted_to: Option<Vec<String>>,
+}
+
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ModelCapabilities {
family: String,
#[serde(default)]
limits: ModelLimits,
supports: ModelSupportedFeatures,
+ #[serde(rename = "type")]
+ model_type: String,
+ #[serde(default)]
+ tokenizer: Option<String>,
}
#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -153,6 +166,11 @@ pub enum ModelVendor {
OpenAI,
Google,
Anthropic,
+ #[serde(rename = "xAI")]
+ XAI,
+ /// Unknown vendor that we don't explicitly support yet
+ #[serde(other)]
+ Unknown,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
@@ -201,6 +219,10 @@ impl Model {
pub fn supports_parallel_tool_calls(&self) -> bool {
self.capabilities.supports.parallel_tool_calls
}
+
+ pub fn tokenizer(&self) -> Option<&str> {
+ self.capabilities.tokenizer.as_deref()
+ }
}
#[derive(Serialize, Deserialize)]
@@ -484,7 +506,7 @@ impl CopilotChat {
};
if this.oauth_token.is_some() {
- cx.spawn(async move |this, mut cx| Self::update_models(&this, &mut cx).await)
+ cx.spawn(async move |this, cx| Self::update_models(&this, cx).await)
.detach_and_log_err(cx);
}
@@ -602,6 +624,7 @@ async fn get_models(
.into_iter()
.filter(|model| {
model.model_picker_enabled
+ && model.capabilities.model_type.as_str() == "chat"
&& model
.policy
.as_ref()
@@ -610,9 +633,7 @@ async fn get_models(
.dedup_by(|a, b| a.capabilities.family == b.capabilities.family)
.collect();
- if let Some(default_model_position) =
- models.iter().position(|model| model.id == DEFAULT_MODEL_ID)
- {
+ if let Some(default_model_position) = models.iter().position(|model| model.is_chat_default) {
let default_model = models.remove(default_model_position);
models.insert(0, default_model);
}
@@ -630,7 +651,9 @@ async fn request_models(
.uri(models_url.as_ref())
.header("Authorization", format!("Bearer {}", api_token))
.header("Content-Type", "application/json")
- .header("Copilot-Integration-Id", "vscode-chat");
+ .header("Copilot-Integration-Id", "vscode-chat")
+ .header("Editor-Version", "vscode/1.103.2")
+ .header("x-github-api-version", "2025-05-01");
let request = request_builder.body(AsyncBody::empty())?;
@@ -801,6 +824,10 @@ mod tests {
let json = r#"{
"data": [
{
+ "billing": {
+ "is_premium": false,
+ "multiplier": 0
+ },
"capabilities": {
"family": "gpt-4",
"limits": {
@@ -814,6 +841,8 @@ mod tests {
"type": "chat"
},
"id": "gpt-4",
+ "is_chat_default": false,
+ "is_chat_fallback": false,
"model_picker_enabled": false,
"name": "GPT 4",
"object": "model",
@@ -825,6 +854,16 @@ mod tests {
"some-unknown-field": 123
},
{
+ "billing": {
+ "is_premium": true,
+ "multiplier": 1,
+ "restricted_to": [
+ "pro",
+ "pro_plus",
+ "business",
+ "enterprise"
+ ]
+ },
"capabilities": {
"family": "claude-3.7-sonnet",
"limits": {
@@ -848,6 +887,8 @@ mod tests {
"type": "chat"
},
"id": "claude-3.7-sonnet",
+ "is_chat_default": false,
+ "is_chat_fallback": false,
"model_picker_enabled": true,
"name": "Claude 3.7 Sonnet",
"object": "model",
@@ -863,10 +904,51 @@ mod tests {
"object": "list"
}"#;
- let schema: ModelSchema = serde_json::from_str(&json).unwrap();
+ let schema: ModelSchema = serde_json::from_str(json).unwrap();
assert_eq!(schema.data.len(), 2);
assert_eq!(schema.data[0].id, "gpt-4");
assert_eq!(schema.data[1].id, "claude-3.7-sonnet");
}
+
+ #[test]
+ fn test_unknown_vendor_resilience() {
+ let json = r#"{
+ "data": [
+ {
+ "billing": {
+ "is_premium": false,
+ "multiplier": 1
+ },
+ "capabilities": {
+ "family": "future-model",
+ "limits": {
+ "max_context_window_tokens": 128000,
+ "max_output_tokens": 8192,
+ "max_prompt_tokens": 120000
+ },
+ "object": "model_capabilities",
+ "supports": { "streaming": true, "tool_calls": true },
+ "type": "chat"
+ },
+ "id": "future-model-v1",
+ "is_chat_default": false,
+ "is_chat_fallback": false,
+ "model_picker_enabled": true,
+ "name": "Future Model v1",
+ "object": "model",
+ "preview": false,
+ "vendor": "SomeNewVendor",
+ "version": "v1.0"
+ }
+ ],
+ "object": "list"
+ }"#;
+
+ let schema: ModelSchema = serde_json::from_str(json).unwrap();
+
+ assert_eq!(schema.data.len(), 1);
+ assert_eq!(schema.data[0].id, "future-model-v1");
+ assert_eq!(schema.data[0].vendor, ModelVendor::Unknown);
+ }
}
@@ -301,6 +301,7 @@ mod tests {
init_test(cx, |settings| {
settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled,
+ words_min_length: 0,
lsp: true,
lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert,
@@ -533,6 +534,7 @@ mod tests {
init_test(cx, |settings| {
settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Disabled,
+ words_min_length: 0,
lsp: true,
lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert,
@@ -1083,7 +1085,7 @@ mod tests {
let replace_range_marker: TextRangeMarker = ('<', '>').into();
let (_, mut marked_ranges) = marked_text_ranges_by(
marked_string,
- vec![complete_from_marker.clone(), replace_range_marker.clone()],
+ vec![complete_from_marker, replace_range_marker.clone()],
);
let replace_range =
@@ -102,7 +102,7 @@ pub struct GetCompletionsDocument {
pub tab_size: u32,
pub indent_size: u32,
pub insert_spaces: bool,
- pub uri: lsp::Url,
+ pub uri: lsp::Uri,
pub relative_path: String,
pub position: lsp::Position,
pub version: usize,
@@ -6,13 +6,21 @@ edition.workspace = true
license = "GPL-3.0-or-later"
[dependencies]
+bincode.workspace = true
crash-handler.workspace = true
log.workspace = true
minidumper.workspace = true
paths.workspace = true
release_channel.workspace = true
smol.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+system_specs.workspace = true
workspace-hack.workspace = true
+zstd.workspace = true
+
+[target.'cfg(target_os = "macos")'.dependencies]
+mach2.workspace = true
[lints]
workspace = true
@@ -2,15 +2,19 @@ use crash_handler::CrashHandler;
use log::info;
use minidumper::{Client, LoopAction, MinidumpBinary};
use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
+use serde::{Deserialize, Serialize};
+#[cfg(target_os = "macos")]
+use std::sync::atomic::AtomicU32;
use std::{
env,
- fs::File,
+ fs::{self, File},
io,
+ panic::Location,
path::{Path, PathBuf},
process::{self, Command},
sync::{
- LazyLock, OnceLock,
+ Arc, OnceLock,
atomic::{AtomicBool, Ordering},
},
thread,
@@ -18,19 +22,20 @@ use std::{
};
// set once the crash handler has initialized and the client has connected to it
-pub static CRASH_HANDLER: AtomicBool = AtomicBool::new(false);
+pub static CRASH_HANDLER: OnceLock<Arc<Client>> = OnceLock::new();
// set when the first minidump request is made to avoid generating duplicate crash reports
pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false);
-const CRASH_HANDLER_TIMEOUT: Duration = Duration::from_secs(60);
+const CRASH_HANDLER_PING_TIMEOUT: Duration = Duration::from_secs(60);
+const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
-pub static GENERATE_MINIDUMPS: LazyLock<bool> = LazyLock::new(|| {
- *RELEASE_CHANNEL != ReleaseChannel::Dev || env::var("ZED_GENERATE_MINIDUMPS").is_ok()
-});
+#[cfg(target_os = "macos")]
+static PANIC_THREAD_ID: AtomicU32 = AtomicU32::new(0);
-pub async fn init(id: String) {
- if !*GENERATE_MINIDUMPS {
+pub async fn init(crash_init: InitCrashHandler) {
+ if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() {
return;
}
+
let exe = env::current_exe().expect("unable to find ourselves");
let zed_pid = process::id();
// TODO: we should be able to get away with using 1 crash-handler process per machine,
@@ -61,9 +66,11 @@ pub async fn init(id: String) {
smol::Timer::after(retry_frequency).await;
}
let client = maybe_client.unwrap();
- client.send_message(1, id).unwrap(); // set session id on the server
+ client
+ .send_message(1, serde_json::to_vec(&crash_init).unwrap())
+ .unwrap();
- let client = std::sync::Arc::new(client);
+ let client = Arc::new(client);
let handler = crash_handler::CrashHandler::attach(unsafe {
let client = client.clone();
crash_handler::make_crash_event(move |crash_context: &crash_handler::CrashContext| {
@@ -72,7 +79,9 @@ pub async fn init(id: String) {
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
- client.send_message(2, "mistakes were made").unwrap();
+ #[cfg(target_os = "macos")]
+ suspend_all_other_threads();
+
client.ping().unwrap();
client.request_dump(crash_context).is_ok()
} else {
@@ -87,7 +96,7 @@ pub async fn init(id: String) {
{
handler.set_ptracer(Some(server_pid));
}
- CRASH_HANDLER.store(true, Ordering::Release);
+ CRASH_HANDLER.set(client.clone()).ok();
std::mem::forget(handler);
info!("crash handler registered");
@@ -97,64 +106,181 @@ pub async fn init(id: String) {
}
}
+#[cfg(target_os = "macos")]
+unsafe fn suspend_all_other_threads() {
+ let task = unsafe { mach2::traps::current_task() };
+ let mut threads: mach2::mach_types::thread_act_array_t = std::ptr::null_mut();
+ let mut count = 0;
+ unsafe {
+ mach2::task::task_threads(task, &raw mut threads, &raw mut count);
+ }
+ let current = unsafe { mach2::mach_init::mach_thread_self() };
+ let panic_thread = PANIC_THREAD_ID.load(Ordering::SeqCst);
+ for i in 0..count {
+ let t = unsafe { *threads.add(i as usize) };
+ if t != current && t != panic_thread {
+ unsafe { mach2::thread_act::thread_suspend(t) };
+ }
+ }
+}
+
pub struct CrashServer {
- session_id: OnceLock<String>,
+ initialization_params: OnceLock<InitCrashHandler>,
+ panic_info: OnceLock<CrashPanic>,
+ active_gpu: OnceLock<system_specs::GpuSpecs>,
+ has_connection: Arc<AtomicBool>,
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct CrashInfo {
+ pub init: InitCrashHandler,
+ pub panic: Option<CrashPanic>,
+ pub minidump_error: Option<String>,
+ pub gpus: Vec<system_specs::GpuInfo>,
+ pub active_gpu: Option<system_specs::GpuSpecs>,
+}
+
+#[derive(Debug, Deserialize, Serialize, Clone)]
+pub struct InitCrashHandler {
+ pub session_id: String,
+ pub zed_version: String,
+ pub release_channel: String,
+ pub commit_sha: String,
+}
+
+#[derive(Deserialize, Serialize, Debug, Clone)]
+pub struct CrashPanic {
+ pub message: String,
+ pub span: String,
}
impl minidumper::ServerHandler for CrashServer {
fn create_minidump_file(&self) -> Result<(File, PathBuf), io::Error> {
- let err_message = "Need to send a message with the ID upon starting the crash handler";
+ let err_message = "Missing initialization data";
let dump_path = paths::logs_dir()
- .join(self.session_id.get().expect(err_message))
+ .join(
+ &self
+ .initialization_params
+ .get()
+ .expect(err_message)
+ .session_id,
+ )
.with_extension("dmp");
let file = File::create(&dump_path)?;
Ok((file, dump_path))
}
fn on_minidump_created(&self, result: Result<MinidumpBinary, minidumper::Error>) -> LoopAction {
- match result {
- Ok(mut md_bin) => {
+ let minidump_error = match result {
+ Ok(MinidumpBinary { mut file, path, .. }) => {
use io::Write;
- let _ = md_bin.file.flush();
- info!("wrote minidump to disk {:?}", md_bin.path);
+ file.flush().ok();
+ // TODO: clean this up once https://github.com/EmbarkStudios/crash-handling/issues/101 is addressed
+ drop(file);
+ let original_file = File::open(&path).unwrap();
+ let compressed_path = path.with_extension("zstd");
+ let compressed_file = File::create(&compressed_path).unwrap();
+ zstd::stream::copy_encode(original_file, compressed_file, 0).ok();
+ fs::rename(&compressed_path, path).unwrap();
+ None
}
- Err(e) => {
- info!("failed to write minidump: {:#}", e);
+ Err(e) => Some(format!("{e:?}")),
+ };
+
+ #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
+ let gpus = vec![];
+
+ #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+ let gpus = match system_specs::read_gpu_info_from_sys_class_drm() {
+ Ok(gpus) => gpus,
+ Err(err) => {
+ log::warn!("Failed to collect GPU information for crash report: {err}");
+ vec![]
}
- }
+ };
+
+ let crash_info = CrashInfo {
+ init: self
+ .initialization_params
+ .get()
+ .expect("not initialized")
+ .clone(),
+ panic: self.panic_info.get().cloned(),
+ minidump_error,
+ active_gpu: self.active_gpu.get().cloned(),
+ gpus,
+ };
+
+ let crash_data_path = paths::logs_dir()
+ .join(&crash_info.init.session_id)
+ .with_extension("json");
+
+ fs::write(crash_data_path, serde_json::to_vec(&crash_info).unwrap()).ok();
+
LoopAction::Exit
}
fn on_message(&self, kind: u32, buffer: Vec<u8>) {
- let message = String::from_utf8(buffer).expect("invalid utf-8");
- info!("kind: {kind}, message: {message}",);
- if kind == 1 {
- self.session_id
- .set(message)
- .expect("session id already initialized");
+ match kind {
+ 1 => {
+ let init_data =
+ serde_json::from_slice::<InitCrashHandler>(&buffer).expect("invalid init data");
+ self.initialization_params
+ .set(init_data)
+ .expect("already initialized");
+ }
+ 2 => {
+ let panic_data =
+ serde_json::from_slice::<CrashPanic>(&buffer).expect("invalid panic data");
+ self.panic_info.set(panic_data).expect("already panicked");
+ }
+ 3 => {
+ let gpu_specs: system_specs::GpuSpecs =
+ bincode::deserialize(&buffer).expect("gpu specs");
+ self.active_gpu
+ .set(gpu_specs)
+ .expect("already set active gpu");
+ }
+ _ => {
+ panic!("invalid message kind");
+ }
}
}
- fn on_client_disconnected(&self, clients: usize) -> LoopAction {
- info!("client disconnected, {clients} remaining");
- if clients == 0 {
- LoopAction::Exit
- } else {
- LoopAction::Continue
- }
+ fn on_client_disconnected(&self, _clients: usize) -> LoopAction {
+ LoopAction::Exit
}
-}
-pub fn handle_panic() {
- if !*GENERATE_MINIDUMPS {
- return;
+ fn on_client_connected(&self, _clients: usize) -> LoopAction {
+ self.has_connection.store(true, Ordering::SeqCst);
+ LoopAction::Continue
}
+}
+
+pub fn handle_panic(message: String, span: Option<&Location>) {
+ let span = span
+ .map(|loc| format!("{}:{}", loc.file(), loc.line()))
+ .unwrap_or_default();
+
// wait 500ms for the crash handler process to start up
// if it's still not there just write panic info and no minidump
let retry_frequency = Duration::from_millis(100);
for _ in 0..5 {
- if CRASH_HANDLER.load(Ordering::Acquire) {
+ if let Some(client) = CRASH_HANDLER.get() {
+ client
+ .send_message(
+ 2,
+ serde_json::to_vec(&CrashPanic { message, span }).unwrap(),
+ )
+ .ok();
log::error!("triggering a crash to generate a minidump...");
+
+ #[cfg(target_os = "macos")]
+ PANIC_THREAD_ID.store(
+ unsafe { mach2::mach_init::mach_thread_self() },
+ Ordering::SeqCst,
+ );
+
#[cfg(target_os = "linux")]
CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32);
#[cfg(not(target_os = "linux"))]
@@ -170,14 +296,31 @@ pub fn crash_server(socket: &Path) {
log::info!("Couldn't create socket, there may already be a running crash server");
return;
};
- let ab = AtomicBool::new(false);
+
+ let shutdown = Arc::new(AtomicBool::new(false));
+ let has_connection = Arc::new(AtomicBool::new(false));
+
+ std::thread::spawn({
+ let shutdown = shutdown.clone();
+ let has_connection = has_connection.clone();
+ move || {
+ std::thread::sleep(CRASH_HANDLER_CONNECT_TIMEOUT);
+ if !has_connection.load(Ordering::SeqCst) {
+ shutdown.store(true, Ordering::SeqCst);
+ }
+ }
+ });
+
server
.run(
Box::new(CrashServer {
- session_id: OnceLock::new(),
+ initialization_params: OnceLock::new(),
+ panic_info: OnceLock::new(),
+ has_connection,
+ active_gpu: OnceLock::new(),
}),
- &ab,
- Some(CRASH_HANDLER_TIMEOUT),
+ &shutdown,
+ Some(CRASH_HANDLER_PING_TIMEOUT),
)
.expect("failed to run server");
}
@@ -19,7 +19,7 @@ use release_channel::ReleaseChannel;
/// Only works in development. Setting this environment variable in other
/// release channels is a no-op.
static ZED_DEVELOPMENT_USE_KEYCHAIN: LazyLock<bool> = LazyLock::new(|| {
- std::env::var("ZED_DEVELOPMENT_USE_KEYCHAIN").map_or(false, |value| !value.is_empty())
+ std::env::var("ZED_DEVELOPMENT_USE_KEYCHAIN").is_ok_and(|value| !value.is_empty())
});
/// A provider for credentials.
@@ -285,7 +285,7 @@ pub async fn download_adapter_from_github(
}
if !adapter_path.exists() {
- fs.create_dir(&adapter_path.as_path())
+ fs.create_dir(adapter_path.as_path())
.await
.context("Failed creating adapter path")?;
}
@@ -23,7 +23,7 @@ impl SessionId {
Self(client_id as u32)
}
- pub fn to_proto(&self) -> u64 {
+ pub fn to_proto(self) -> u64 {
self.0 as u64
}
}
@@ -2,9 +2,9 @@ use dap_types::SteppingGranularity;
use gpui::{App, Global};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum DebugPanelDockPosition {
Left,
@@ -12,12 +12,17 @@ pub enum DebugPanelDockPosition {
Right,
}
-#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)]
+#[derive(Serialize, Deserialize, JsonSchema, Clone, SettingsUi, SettingsKey)]
#[serde(default)]
+// todo(settings_ui) @ben: I'm pretty sure not having the fields be optional here is a bug,
+// it means the defaults will override previously set values if a single key is missing
+#[settings_ui(group = "Debugger")]
+#[settings_key(key = "debugger")]
pub struct DebuggerSettings {
/// Determines the stepping granularity.
///
/// Default: line
+ #[settings_ui(skip)]
pub stepping_granularity: SteppingGranularity,
/// Whether the breakpoints should be reused across Zed sessions.
///
@@ -60,8 +65,6 @@ impl Default for DebuggerSettings {
}
impl Settings for DebuggerSettings {
- const KEY: Option<&'static str> = Some("debugger");
-
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
@@ -385,7 +385,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
&& let Some(source_languages) = config.get("sourceLanguages").filter(|value| {
value
.as_array()
- .map_or(false, |array| array.iter().all(Value::is_string))
+ .is_some_and(|array| array.iter().all(Value::is_string))
})
{
let ret = vec![
@@ -36,7 +36,7 @@ impl GoDebugAdapter {
delegate: &Arc<dyn DapDelegate>,
) -> Result<AdapterVersion> {
let release = latest_github_release(
- &"zed-industries/delve-shim-dap",
+ "zed-industries/delve-shim-dap",
true,
false,
delegate.http_client(),
@@ -99,10 +99,10 @@ impl JsDebugAdapter {
}
}
- if let Some(env) = configuration.get("env").cloned() {
- if let Ok(env) = serde_json::from_value(env) {
- envs = env;
- }
+ if let Some(env) = configuration.get("env").cloned()
+ && let Ok(env) = serde_json::from_value(env)
+ {
+ envs = env;
}
configuration
@@ -514,7 +514,7 @@ impl DebugAdapter for JsDebugAdapter {
}
}
- self.get_installed_binary(delegate, &config, user_installed_path, user_args, cx)
+ self.get_installed_binary(delegate, config, user_installed_path, user_args, cx)
.await
}
@@ -24,6 +24,7 @@ use util::{ResultExt, maybe};
#[derive(Default)]
pub(crate) struct PythonDebugAdapter {
+ base_venv_path: OnceCell<Result<Arc<Path>, String>>,
debugpy_whl_base_path: OnceCell<Result<Arc<Path>, String>>,
}
@@ -91,14 +92,12 @@ impl PythonDebugAdapter {
})
}
- async fn fetch_wheel(delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
- let system_python = Self::system_python_name(delegate)
- .await
- .ok_or_else(|| String::from("Could not find a Python installation"))?;
- let command: &OsStr = system_python.as_ref();
+ async fn fetch_wheel(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels");
std::fs::create_dir_all(&download_dir).map_err(|e| e.to_string())?;
- let installation_succeeded = util::command::new_smol_command(command)
+ let system_python = self.base_venv_path(delegate).await?;
+
+ let installation_succeeded = util::command::new_smol_command(system_python.as_ref())
.args([
"-m",
"pip",
@@ -114,7 +113,7 @@ impl PythonDebugAdapter {
.status
.success();
if !installation_succeeded {
- return Err("debugpy installation failed".into());
+ return Err("debugpy installation failed (could not fetch Debugpy's wheel)".into());
}
let wheel_path = std::fs::read_dir(&download_dir)
@@ -139,7 +138,7 @@ impl PythonDebugAdapter {
Ok(Arc::from(wheel_path.path()))
}
- async fn maybe_fetch_new_wheel(delegate: &Arc<dyn DapDelegate>) {
+ async fn maybe_fetch_new_wheel(&self, delegate: &Arc<dyn DapDelegate>) {
let latest_release = delegate
.http_client()
.get(
@@ -191,7 +190,7 @@ impl PythonDebugAdapter {
)
.await
.ok()?;
- Self::fetch_wheel(delegate).await.ok()?;
+ self.fetch_wheel(delegate).await.ok()?;
}
Some(())
})
@@ -204,7 +203,7 @@ impl PythonDebugAdapter {
) -> Result<Arc<Path>, String> {
self.debugpy_whl_base_path
.get_or_init(|| async move {
- Self::maybe_fetch_new_wheel(delegate).await;
+ self.maybe_fetch_new_wheel(delegate).await;
Ok(Arc::from(
debug_adapters_dir()
.join(Self::ADAPTER_NAME)
@@ -217,6 +216,46 @@ impl PythonDebugAdapter {
.clone()
}
+ async fn base_venv_path(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
+ self.base_venv_path
+ .get_or_init(|| async {
+ let base_python = Self::system_python_name(delegate)
+ .await
+ .ok_or_else(|| String::from("Could not find a Python installation"))?;
+
+ let did_succeed = util::command::new_smol_command(base_python)
+ .args(["-m", "venv", "zed_base_venv"])
+ .current_dir(
+ paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref()),
+ )
+ .spawn()
+ .map_err(|e| format!("{e:#?}"))?
+ .status()
+ .await
+ .map_err(|e| format!("{e:#?}"))?
+ .success();
+
+ if !did_succeed {
+ return Err("Failed to create base virtual environment".into());
+ }
+
+ const DIR: &str = if cfg!(target_os = "windows") {
+ "Scripts"
+ } else {
+ "bin"
+ };
+ Ok(Arc::from(
+ paths::debug_adapters_dir()
+ .join(Self::DEBUG_ADAPTER_NAME.as_ref())
+ .join("zed_base_venv")
+ .join(DIR)
+ .join("python3")
+ .as_ref(),
+ ))
+ })
+ .await
+ .clone()
+ }
async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
let mut name = None;
@@ -679,7 +718,7 @@ impl DebugAdapter for PythonDebugAdapter {
local_path.display()
);
return self
- .get_installed_binary(delegate, &config, Some(local_path.clone()), user_args, None)
+ .get_installed_binary(delegate, config, Some(local_path.clone()), user_args, None)
.await;
}
@@ -716,7 +755,7 @@ impl DebugAdapter for PythonDebugAdapter {
return self
.get_installed_binary(
delegate,
- &config,
+ config,
None,
user_args,
Some(toolchain.path.to_string()),
@@ -724,7 +763,7 @@ impl DebugAdapter for PythonDebugAdapter {
.await;
}
- self.get_installed_binary(delegate, &config, None, user_args, None)
+ self.get_installed_binary(delegate, config, None, user_args, None)
.await
}
@@ -27,6 +27,7 @@ sqlez.workspace = true
sqlez_macros.workspace = true
util.workspace = true
workspace-hack.workspace = true
+zed_env_vars.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
@@ -17,9 +17,10 @@ use sqlez::thread_safe_connection::ThreadSafeConnection;
use sqlez_macros::sql;
use std::future::Future;
use std::path::Path;
+use std::sync::atomic::AtomicBool;
use std::sync::{LazyLock, atomic::Ordering};
-use std::{env, sync::atomic::AtomicBool};
use util::{ResultExt, maybe};
+use zed_env_vars::ZED_STATELESS;
const CONNECTION_INITIALIZE_QUERY: &str = sql!(
PRAGMA foreign_keys=TRUE;
@@ -36,9 +37,6 @@ const FALLBACK_DB_NAME: &str = "FALLBACK_MEMORY_DB";
const DB_FILE_NAME: &str = "db.sqlite";
-pub static ZED_STATELESS: LazyLock<bool> =
- LazyLock::new(|| env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
-
pub static ALL_FILE_DB_FAILED: LazyLock<AtomicBool> = LazyLock::new(|| AtomicBool::new(false));
/// Open or create a database at the given directory path.
@@ -74,7 +72,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> Threa
}
async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection> {
- log::info!("Opening database {}", db_path.display());
+ log::trace!("Opening database {}", db_path.display());
ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true)
.with_db_initialization_query(DB_INITIALIZE_QUERY)
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
@@ -110,11 +108,14 @@ pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection {
}
/// Implements a basic DB wrapper for a given domain
+///
+/// Arguments:
+/// - static variable name for connection
+/// - type of connection wrapper
+/// - dependencies, whose migrations should be run prior to this domain's migrations
#[macro_export]
-macro_rules! define_connection {
- (pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => {
- pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
-
+macro_rules! static_connection {
+ ($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => {
impl ::std::ops::Deref for $t {
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
@@ -123,16 +124,6 @@ macro_rules! define_connection {
}
}
- impl $crate::sqlez::domain::Domain for $t {
- fn name() -> &'static str {
- stringify!($t)
- }
-
- fn migrations() -> &'static [&'static str] {
- $migrations
- }
- }
-
impl $t {
#[cfg(any(test, feature = "test-support"))]
pub async fn open_test_db(name: &'static str) -> Self {
@@ -142,7 +133,8 @@ macro_rules! define_connection {
#[cfg(any(test, feature = "test-support"))]
pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
- $t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id))))
+ #[allow(unused_parens)]
+ $t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id))))
});
#[cfg(not(any(test, feature = "test-support")))]
@@ -153,46 +145,10 @@ macro_rules! define_connection {
} else {
$crate::RELEASE_CHANNEL.dev_name()
};
- $t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope)))
+ #[allow(unused_parens)]
+ $t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope)))
});
- };
- (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => {
- pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection);
-
- impl ::std::ops::Deref for $t {
- type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection;
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
- }
-
- impl $crate::sqlez::domain::Domain for $t {
- fn name() -> &'static str {
- stringify!($t)
- }
-
- fn migrations() -> &'static [&'static str] {
- $migrations
- }
- }
-
- #[cfg(any(test, feature = "test-support"))]
- pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
- $t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id))))
- });
-
- #[cfg(not(any(test, feature = "test-support")))]
- pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| {
- let db_dir = $crate::database_dir();
- let scope = if false $(|| stringify!($global) == "global")? {
- "global"
- } else {
- $crate::RELEASE_CHANNEL.dev_name()
- };
- $t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope)))
- });
- };
+ }
}
pub fn write_and_log<F>(cx: &App, db_write: impl FnOnce() -> F + Send + 'static)
@@ -219,17 +175,12 @@ mod tests {
enum BadDB {}
impl Domain for BadDB {
- fn name() -> &'static str {
- "db_tests"
- }
-
- fn migrations() -> &'static [&'static str] {
- &[
- sql!(CREATE TABLE test(value);),
- // failure because test already exists
- sql!(CREATE TABLE test(value);),
- ]
- }
+ const NAME: &str = "db_tests";
+ const MIGRATIONS: &[&str] = &[
+ sql!(CREATE TABLE test(value);),
+ // failure because test already exists
+ sql!(CREATE TABLE test(value);),
+ ];
}
let tempdir = tempfile::Builder::new()
@@ -238,7 +189,7 @@ mod tests {
.unwrap();
let _bad_db = open_db::<BadDB>(
tempdir.path(),
- &release_channel::ReleaseChannel::Dev.dev_name(),
+ release_channel::ReleaseChannel::Dev.dev_name(),
)
.await;
}
@@ -251,25 +202,15 @@ mod tests {
enum CorruptedDB {}
impl Domain for CorruptedDB {
- fn name() -> &'static str {
- "db_tests"
- }
-
- fn migrations() -> &'static [&'static str] {
- &[sql!(CREATE TABLE test(value);)]
- }
+ const NAME: &str = "db_tests";
+ const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
}
enum GoodDB {}
impl Domain for GoodDB {
- fn name() -> &'static str {
- "db_tests" //Notice same name
- }
-
- fn migrations() -> &'static [&'static str] {
- &[sql!(CREATE TABLE test2(value);)] //But different migration
- }
+ const NAME: &str = "db_tests"; //Notice same name
+ const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)];
}
let tempdir = tempfile::Builder::new()
@@ -279,7 +220,7 @@ mod tests {
{
let corrupt_db = open_db::<CorruptedDB>(
tempdir.path(),
- &release_channel::ReleaseChannel::Dev.dev_name(),
+ release_channel::ReleaseChannel::Dev.dev_name(),
)
.await;
assert!(corrupt_db.persistent());
@@ -287,7 +228,7 @@ mod tests {
let good_db = open_db::<GoodDB>(
tempdir.path(),
- &release_channel::ReleaseChannel::Dev.dev_name(),
+ release_channel::ReleaseChannel::Dev.dev_name(),
)
.await;
assert!(
@@ -305,25 +246,16 @@ mod tests {
enum CorruptedDB {}
impl Domain for CorruptedDB {
- fn name() -> &'static str {
- "db_tests"
- }
+ const NAME: &str = "db_tests";
- fn migrations() -> &'static [&'static str] {
- &[sql!(CREATE TABLE test(value);)]
- }
+ const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)];
}
enum GoodDB {}
impl Domain for GoodDB {
- fn name() -> &'static str {
- "db_tests" //Notice same name
- }
-
- fn migrations() -> &'static [&'static str] {
- &[sql!(CREATE TABLE test2(value);)] //But different migration
- }
+ const NAME: &str = "db_tests"; //Notice same name
+ const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration
}
let tempdir = tempfile::Builder::new()
@@ -334,7 +266,7 @@ mod tests {
// Setup the bad database
let corrupt_db = open_db::<CorruptedDB>(
tempdir.path(),
- &release_channel::ReleaseChannel::Dev.dev_name(),
+ release_channel::ReleaseChannel::Dev.dev_name(),
)
.await;
assert!(corrupt_db.persistent());
@@ -347,7 +279,7 @@ mod tests {
let guard = thread::spawn(move || {
let good_db = smol::block_on(open_db::<GoodDB>(
tmp_path.as_path(),
- &release_channel::ReleaseChannel::Dev.dev_name(),
+ release_channel::ReleaseChannel::Dev.dev_name(),
));
assert!(
good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
@@ -2,16 +2,26 @@ use gpui::App;
use sqlez_macros::sql;
use util::ResultExt as _;
-use crate::{define_connection, query, write_and_log};
+use crate::{
+ query,
+ sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
+ write_and_log,
+};
-define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
- &[sql!(
+pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection);
+
+impl Domain for KeyValueStore {
+ const NAME: &str = stringify!(KeyValueStore);
+
+ const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE IF NOT EXISTS kv_store(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
)];
-);
+}
+
+crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []);
pub trait Dismissable {
const KEY: &'static str;
@@ -20,7 +30,7 @@ pub trait Dismissable {
KEY_VALUE_STORE
.read_kvp(Self::KEY)
.log_err()
- .map_or(false, |s| s.is_some())
+ .is_some_and(|s| s.is_some())
}
fn set_dismissed(is_dismissed: bool, cx: &mut App) {
@@ -91,15 +101,19 @@ mod tests {
}
}
-define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> =
- &[sql!(
+pub struct GlobalKeyValueStore(ThreadSafeConnection);
+
+impl Domain for GlobalKeyValueStore {
+ const NAME: &str = stringify!(GlobalKeyValueStore);
+ const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE IF NOT EXISTS kv_store(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
)];
- global
-);
+}
+
+crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global);
impl GlobalKeyValueStore {
query! {
@@ -392,7 +392,7 @@ impl LogStore {
session.label(),
session
.adapter_client()
- .map_or(false, |client| client.has_adapter_logs()),
+ .is_some_and(|client| client.has_adapter_logs()),
)
});
@@ -485,7 +485,7 @@ impl LogStore {
&mut self,
id: &LogStoreEntryIdentifier<'_>,
) -> Option<&Vec<SharedString>> {
- self.get_debug_adapter_state(&id)
+ self.get_debug_adapter_state(id)
.map(|state| &state.rpc_messages.initialization_sequence)
}
}
@@ -536,11 +536,11 @@ impl Render for DapLogToolbarItemView {
})
.unwrap_or_else(|| "No adapter selected".into()),
))
- .menu(move |mut window, cx| {
+ .menu(move |window, cx| {
let log_view = log_view.clone();
let menu_rows = menu_rows.clone();
let project = project.clone();
- ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
+ ContextMenu::build(window, cx, move |mut menu, window, _cx| {
for row in menu_rows.into_iter() {
menu = menu.custom_row(move |_window, _cx| {
div()
@@ -661,11 +661,11 @@ impl ToolbarItemView for DapLogToolbarItemView {
_window: &mut Window,
cx: &mut Context<Self>,
) -> workspace::ToolbarItemLocation {
- if let Some(item) = active_pane_item {
- if let Some(log_view) = item.downcast::<DapLogView>() {
- self.log_view = Some(log_view.clone());
- return workspace::ToolbarItemLocation::PrimaryLeft;
- }
+ if let Some(item) = active_pane_item
+ && let Some(log_view) = item.downcast::<DapLogView>()
+ {
+ self.log_view = Some(log_view);
+ return workspace::ToolbarItemLocation::PrimaryLeft;
}
self.log_view = None;
@@ -1131,7 +1131,7 @@ impl LogStore {
project: &WeakEntity<Project>,
session_id: SessionId,
) -> Vec<SharedString> {
- self.projects.get(&project).map_or(vec![], |state| {
+ self.projects.get(project).map_or(vec![], |state| {
state
.debug_sessions
.get(&session_id)
@@ -1,8 +1,10 @@
use dap::{DapRegistry, DebugRequest};
use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render};
+use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task};
use gpui::{Subscription, WeakEntity};
use picker::{Picker, PickerDelegate};
+use project::Project;
+use rpc::proto;
use task::ZedDebugConfig;
use util::debug_panic;
@@ -56,29 +58,28 @@ impl AttachModal {
pub fn new(
definition: ZedDebugConfig,
workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
modal: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
- let mut processes: Box<[_]> = System::new_all()
- .processes()
- .values()
- .map(|process| {
- let name = process.name().to_string_lossy().into_owned();
- Candidate {
- name: name.into(),
- pid: process.pid().as_u32(),
- command: process
- .cmd()
- .iter()
- .map(|s| s.to_string_lossy().to_string())
- .collect::<Vec<_>>(),
- }
- })
- .collect();
- processes.sort_by_key(|k| k.name.clone());
- let processes = processes.into_iter().collect();
- Self::with_processes(workspace, definition, processes, modal, window, cx)
+ let processes_task = get_processes_for_project(&project, cx);
+
+ let modal = Self::with_processes(workspace, definition, Arc::new([]), modal, window, cx);
+
+ cx.spawn_in(window, async move |this, cx| {
+ let processes = processes_task.await;
+ this.update_in(cx, |modal, window, cx| {
+ modal.picker.update(cx, |picker, cx| {
+ picker.delegate.candidates = processes;
+ picker.refresh(window, cx);
+ });
+ })?;
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+
+ modal
}
pub(super) fn with_processes(
@@ -288,7 +289,7 @@ impl PickerDelegate for AttachModalDelegate {
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let hit = &self.matches[ix];
+ let hit = &self.matches.get(ix)?;
let candidate = self.candidates.get(hit.candidate_id)?;
Some(
@@ -332,6 +333,57 @@ impl PickerDelegate for AttachModalDelegate {
}
}
+fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Arc<[Candidate]>> {
+ let project = project.read(cx);
+
+ if let Some(remote_client) = project.remote_client() {
+ let proto_client = remote_client.read(cx).proto_client();
+ cx.spawn(async move |_cx| {
+ let response = proto_client
+ .request(proto::GetProcesses {
+ project_id: proto::REMOTE_SERVER_PROJECT_ID,
+ })
+ .await
+ .unwrap_or_else(|_| proto::GetProcessesResponse {
+ processes: Vec::new(),
+ });
+
+ let mut processes: Vec<Candidate> = response
+ .processes
+ .into_iter()
+ .map(|p| Candidate {
+ pid: p.pid,
+ name: p.name.into(),
+ command: p.command,
+ })
+ .collect();
+
+ processes.sort_by_key(|k| k.name.clone());
+ Arc::from(processes.into_boxed_slice())
+ })
+ } else {
+ let mut processes: Box<[_]> = System::new_all()
+ .processes()
+ .values()
+ .map(|process| {
+ let name = process.name().to_string_lossy().into_owned();
+ Candidate {
+ name: name.into(),
+ pid: process.pid().as_u32(),
+ command: process
+ .cmd()
+ .iter()
+ .map(|s| s.to_string_lossy().to_string())
+ .collect::<Vec<_>>(),
+ }
+ })
+ .collect();
+ processes.sort_by_key(|k| k.name.clone());
+ let processes = processes.into_iter().collect();
+ Task::ready(processes)
+ }
+}
+
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
modal.picker.read_with(cx, |picker, _| {
@@ -13,11 +13,8 @@ use anyhow::{Context as _, Result, anyhow};
use collections::IndexMap;
use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition;
-use dap::{
- ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
- client::SessionId, debugger_settings::DebuggerSettings,
-};
use dap::{DapRegistry, StartDebuggingRequestArguments};
+use dap::{client::SessionId, debugger_settings::DebuggerSettings};
use editor::Editor;
use gpui::{
Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
@@ -46,23 +43,6 @@ use workspace::{
};
use zed_actions::ToggleFocus;
-pub enum DebugPanelEvent {
- Exited(SessionId),
- Terminated(SessionId),
- Stopped {
- client_id: SessionId,
- event: StoppedEvent,
- go_to_stack_frame: bool,
- },
- Thread((SessionId, ThreadEvent)),
- Continued((SessionId, ContinuedEvent)),
- Output((SessionId, OutputEvent)),
- Module((SessionId, ModuleEvent)),
- LoadedSource((SessionId, LoadedSourceEvent)),
- ClientShutdown(SessionId),
- CapabilitiesChanged(SessionId),
-}
-
pub struct DebugPanel {
size: Pixels,
active_session: Option<Entity<DebugSession>>,
@@ -257,7 +237,7 @@ impl DebugPanel {
.as_ref()
.map(|entity| entity.downgrade()),
task_context: task_context.clone(),
- worktree_id: worktree_id,
+ worktree_id,
});
};
running.resolve_scenario(
@@ -386,10 +366,10 @@ impl DebugPanel {
return;
};
- let dap_store_handle = self.project.read(cx).dap_store().clone();
+ let dap_store_handle = self.project.read(cx).dap_store();
let label = curr_session.read(cx).label();
let quirks = curr_session.read(cx).quirks();
- let adapter = curr_session.read(cx).adapter().clone();
+ let adapter = curr_session.read(cx).adapter();
let binary = curr_session.read(cx).binary().cloned().unwrap();
let task_context = curr_session.read(cx).task_context().clone();
@@ -447,9 +427,9 @@ impl DebugPanel {
return;
};
- let dap_store_handle = self.project.read(cx).dap_store().clone();
+ let dap_store_handle = self.project.read(cx).dap_store();
let label = self.label_for_child_session(&parent_session, request, cx);
- let adapter = parent_session.read(cx).adapter().clone();
+ let adapter = parent_session.read(cx).adapter();
let quirks = parent_session.read(cx).quirks();
let Some(mut binary) = parent_session.read(cx).binary().cloned() else {
log::error!("Attempted to start a child-session without a binary");
@@ -530,10 +510,9 @@ impl DebugPanel {
.active_session
.as_ref()
.map(|session| session.entity_id())
+ && active_session_id == entity_id
{
- if active_session_id == entity_id {
- this.active_session = this.sessions_with_children.keys().next().cloned();
- }
+ this.active_session = this.sessions_with_children.keys().next().cloned();
}
cx.notify()
})
@@ -693,7 +672,7 @@ impl DebugPanel {
)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
- &running_state,
+ running_state,
|this, _, _window, cx| {
this.pause_thread(cx);
},
@@ -719,7 +698,7 @@ impl DebugPanel {
)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
- &running_state,
+ running_state,
|this, _, _window, cx| this.continue_thread(cx),
))
.disabled(thread_status != ThreadStatus::Stopped)
@@ -742,7 +721,7 @@ impl DebugPanel {
IconButton::new("debug-step-over", IconName::ArrowRight)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
- &running_state,
+ running_state,
|this, _, _window, cx| {
this.step_over(cx);
},
@@ -768,7 +747,7 @@ impl DebugPanel {
)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
- &running_state,
+ running_state,
|this, _, _window, cx| {
this.step_in(cx);
},
@@ -791,7 +770,7 @@ impl DebugPanel {
IconButton::new("debug-step-out", IconName::ArrowUpRight)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
- &running_state,
+ running_state,
|this, _, _window, cx| {
this.step_out(cx);
},
@@ -815,7 +794,7 @@ impl DebugPanel {
IconButton::new("debug-restart", IconName::RotateCcw)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
- &running_state,
+ running_state,
|this, _, window, cx| {
this.rerun_session(window, cx);
},
@@ -837,7 +816,7 @@ impl DebugPanel {
IconButton::new("debug-stop", IconName::Power)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
- &running_state,
+ running_state,
|this, _, _window, cx| {
if this.session().read(cx).is_building() {
this.session().update(cx, |session, cx| {
@@ -892,7 +871,7 @@ impl DebugPanel {
)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
- &running_state,
+ running_state,
|this, _, _, cx| {
this.detach_client(cx);
},
@@ -933,7 +912,6 @@ impl DebugPanel {
.cloned(),
|this, running_state| {
this.children({
- let running_state = running_state.clone();
let threads =
running_state.update(cx, |running_state, cx| {
let session = running_state.session();
@@ -1160,7 +1138,7 @@ impl DebugPanel {
workspace
.project()
.read(cx)
- .project_path_for_absolute_path(&path, cx)
+ .project_path_for_absolute_path(path, cx)
.context(
"Couldn't get project path for .zed/debug.json in active worktree",
)
@@ -1302,10 +1280,10 @@ impl DebugPanel {
cx: &mut Context<'_, Self>,
) -> Option<SharedString> {
let adapter = parent_session.read(cx).adapter();
- if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
- if let Some(label) = adapter.label_for_child_session(request) {
- return Some(label.into());
- }
+ if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter)
+ && let Some(label) = adapter.label_for_child_session(request)
+ {
+ return Some(label.into());
}
None
}
@@ -1409,7 +1387,6 @@ async fn register_session_inner(
}
impl EventEmitter<PanelEvent> for DebugPanel {}
-impl EventEmitter<DebugPanelEvent> for DebugPanel {}
impl Focusable for DebugPanel {
fn focus_handle(&self, _: &App) -> FocusHandle {
@@ -1646,7 +1623,6 @@ impl Render for DebugPanel {
}
})
.on_action({
- let this = this.clone();
move |_: &ToggleSessionPicker, window, cx| {
this.update(cx, |this, cx| {
this.toggle_session_picker(window, cx);
@@ -85,6 +85,10 @@ actions!(
Rerun,
/// Toggles expansion of the selected item in the debugger UI.
ToggleExpandItem,
+ /// Toggle the user frame filter in the stack frame list
+ /// When toggled on, only frames from the user's code are shown
+ /// When toggled off, all frames are shown
+ ToggleUserFrames,
]
);
@@ -279,6 +283,18 @@ pub fn init(cx: &mut App) {
.ok();
}
})
+ .on_action(move |_: &ToggleUserFrames, _, cx| {
+ if let Some((thread_status, stack_frame_list)) = active_item
+ .read_with(cx, |item, cx| {
+ (item.thread_status(cx), item.stack_frame_list().clone())
+ })
+ .ok()
+ {
+ stack_frame_list.update(cx, |stack_frame_list, cx| {
+ stack_frame_list.toggle_frame_filter(thread_status, cx);
+ })
+ }
+ })
});
})
.detach();
@@ -293,9 +309,8 @@ pub fn init(cx: &mut App) {
let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
return;
};
- let Some(active_session) = debug_panel
- .clone()
- .update(cx, |panel, _| panel.active_session())
+ let Some(active_session) =
+ debug_panel.update(cx, |panel, _| panel.active_session())
else {
return;
};
@@ -1,9 +1,9 @@
-use std::{rc::Rc, time::Duration};
+use std::rc::Rc;
use collections::HashMap;
-use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage};
+use gpui::{Entity, WeakEntity};
use project::debugger::session::{ThreadId, ThreadStatus};
-use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
+use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use util::{maybe, truncate_and_trailoff};
use crate::{
@@ -113,23 +113,6 @@ impl DebugPanel {
}
};
session_entries.push(root_entry);
-
- session_entries.extend(
- sessions_with_children
- .by_ref()
- .take_while(|(session, _)| {
- session
- .read(cx)
- .session(cx)
- .read(cx)
- .parent_id(cx)
- .is_some()
- })
- .map(|(session, _)| SessionListEntry {
- leaf: session.clone(),
- ancestors: vec![],
- }),
- );
}
let weak = cx.weak_entity();
@@ -152,11 +135,7 @@ impl DebugPanel {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- )
+ .with_rotate_animation(2)
.into_any_element()
} else {
match running_state.thread_status(cx).unwrap_or_default() {
@@ -272,10 +251,9 @@ impl DebugPanel {
.child(session_entry.label_element(self_depth, cx))
.child(
IconButton::new("close-debug-session", IconName::Close)
- .visible_on_hover(id.clone())
+ .visible_on_hover(id)
.icon_size(IconSize::Small)
.on_click({
- let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(session_entity_id, window, cx);
@@ -20,7 +20,7 @@ use gpui::{
};
use itertools::Itertools as _;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
-use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore};
+use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::Settings;
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
use theme::ThemeSettings;
@@ -88,8 +88,10 @@ impl NewProcessModal {
})?;
workspace.update_in(cx, |workspace, window, cx| {
let workspace_handle = workspace.weak_handle();
+ let project = workspace.project().clone();
workspace.toggle_modal(window, cx, |window, cx| {
- let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
+ let attach_mode =
+ AttachMode::new(None, workspace_handle.clone(), project, window, cx);
let debug_picker = cx.new(|cx| {
let delegate =
@@ -343,10 +345,10 @@ impl NewProcessModal {
return;
}
- if let NewProcessMode::Launch = &self.mode {
- if self.configure_mode.read(cx).save_to_debug_json.selected() {
- self.save_debug_scenario(window, cx);
- }
+ if let NewProcessMode::Launch = &self.mode
+ && self.configure_mode.read(cx).save_to_debug_json.selected()
+ {
+ self.save_debug_scenario(window, cx);
}
let Some(debugger) = self.debugger.clone() else {
@@ -413,7 +415,7 @@ impl NewProcessModal {
let Some(adapter) = self.debugger.as_ref() else {
return;
};
- let scenario = self.debug_scenario(&adapter, cx);
+ let scenario = self.debug_scenario(adapter, cx);
cx.spawn_in(window, async move |this, cx| {
let scenario = scenario.await.context("no scenario to save")?;
let worktree_id = task_contexts
@@ -659,12 +661,7 @@ impl Render for NewProcessModal {
this.mode = NewProcessMode::Attach;
if let Some(debugger) = this.debugger.as_ref() {
- Self::update_attach_picker(
- &this.attach_mode,
- &debugger,
- window,
- cx,
- );
+ Self::update_attach_picker(&this.attach_mode, debugger, window, cx);
}
this.mode_focus_handle(cx).focus(window);
cx.notify();
@@ -790,7 +787,7 @@ impl RenderOnce for AttachMode {
v_flex()
.w_full()
.track_focus(&self.attach_picker.focus_handle(cx))
- .child(self.attach_picker.clone())
+ .child(self.attach_picker)
}
}
@@ -806,12 +803,12 @@ impl ConfigureMode {
pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let program = cx.new(|cx| Editor::single_line(window, cx));
program.update(cx, |this, cx| {
- this.set_placeholder_text("ENV=Zed ~/bin/program --option", cx);
+ this.set_placeholder_text("ENV=Zed ~/bin/program --option", window, cx);
});
let cwd = cx.new(|cx| Editor::single_line(window, cx));
cwd.update(cx, |this, cx| {
- this.set_placeholder_text("Ex: $ZED_WORKTREE_ROOT", cx);
+ this.set_placeholder_text("Ex: $ZED_WORKTREE_ROOT", window, cx);
});
cx.new(|_| Self {
@@ -945,6 +942,7 @@ impl AttachMode {
pub(super) fn new(
debugger: Option<DebugAdapterName>,
workspace: WeakEntity<Workspace>,
+ project: Entity<Project>,
window: &mut Window,
cx: &mut Context<NewProcessModal>,
) -> Entity<Self> {
@@ -955,7 +953,7 @@ impl AttachMode {
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
- let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
+ let modal = AttachModal::new(definition.clone(), workspace, project, false, window, cx);
window.focus(&modal.focus_handle(cx));
modal
@@ -1083,7 +1081,7 @@ impl DebugDelegate {
.into_iter()
.map(|(scenario, context)| {
let (kind, scenario) =
- Self::get_scenario_kind(&languages, &dap_registry, scenario);
+ Self::get_scenario_kind(&languages, dap_registry, scenario);
(kind, scenario, Some(context))
})
.chain(
@@ -1100,7 +1098,7 @@ impl DebugDelegate {
.filter(|(_, scenario)| valid_adapters.contains(&scenario.adapter))
.map(|(kind, scenario)| {
let (language, scenario) =
- Self::get_scenario_kind(&languages, &dap_registry, scenario);
+ Self::get_scenario_kind(&languages, dap_registry, scenario);
(language.or(Some(kind)), scenario, None)
}),
)
@@ -1388,14 +1386,28 @@ impl PickerDelegate for DebugDelegate {
.border_color(cx.theme().colors().border_variant)
.children({
let action = menu::SecondaryConfirm.boxed_clone();
- KeyBinding::for_action(&*action, window, cx).map(|keybind| {
- Button::new("edit-debug-task", "Edit in debug.json")
- .label_size(LabelSize::Small)
- .key_binding(keybind)
- .on_click(move |_, window, cx| {
- window.dispatch_action(action.boxed_clone(), cx)
- })
- })
+ if self.matches.is_empty() {
+ Some(
+ Button::new("edit-debug-json", "Edit debug.json")
+ .label_size(LabelSize::Small)
+ .on_click(cx.listener(|_picker, _, window, cx| {
+ window.dispatch_action(
+ zed_actions::OpenProjectDebugTasks.boxed_clone(),
+ cx,
+ );
+ cx.emit(DismissEvent);
+ })),
+ )
+ } else {
+ KeyBinding::for_action(&*action, window, cx).map(|keybind| {
+ Button::new("edit-debug-task", "Edit in debug.json")
+ .label_size(LabelSize::Small)
+ .key_binding(keybind)
+ .on_click(move |_, window, cx| {
+ window.dispatch_action(action.boxed_clone(), cx)
+ })
+ })
+ }
})
.map(|this| {
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
@@ -1434,7 +1446,7 @@ impl PickerDelegate for DebugDelegate {
window: &mut Window,
cx: &mut Context<picker::Picker<Self>>,
) -> Option<Self::ListItem> {
- let hit = &self.matches[ix];
+ let hit = &self.matches.get(ix)?;
let highlighted_location = HighlightedMatch {
text: hit.string.clone(),
@@ -256,7 +256,7 @@ pub(crate) fn deserialize_pane_layout(
Some(Member::Axis(PaneAxis::load(
if should_invert { axis.invert() } else { axis },
members,
- flexes.clone(),
+ flexes,
)))
}
SerializedPaneLayout::Pane(serialized_pane) => {
@@ -270,12 +270,9 @@ pub(crate) fn deserialize_pane_layout(
.children
.iter()
.map(|child| match child {
- DebuggerPaneItem::Frames => Box::new(SubView::new(
- stack_frame_list.focus_handle(cx),
- stack_frame_list.clone().into(),
- DebuggerPaneItem::Frames,
- cx,
- )),
+ DebuggerPaneItem::Frames => {
+ Box::new(SubView::stack_frame_list(stack_frame_list.clone(), cx))
+ }
DebuggerPaneItem::Variables => Box::new(SubView::new(
variable_list.focus_handle(cx),
variable_list.clone().into(),
@@ -341,7 +338,7 @@ impl SerializedPaneLayout {
pub(crate) fn in_order(&self) -> Vec<SerializedPaneLayout> {
let mut panes = vec![];
- Self::inner_in_order(&self, &mut panes);
+ Self::inner_in_order(self, &mut panes);
panes
}
@@ -2,9 +2,7 @@ pub mod running;
use crate::{StackTraceView, persistence::SerializedLayout, session::running::DebugTerminal};
use dap::client::SessionId;
-use gpui::{
- App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
-};
+use gpui::{App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
use project::debugger::session::Session;
use project::worktree_store::WorktreeStore;
use project::{Project, debugger::session::SessionQuirks};
@@ -24,13 +22,6 @@ pub struct DebugSession {
stack_trace_view: OnceCell<Entity<StackTraceView>>,
_worktree_store: WeakEntity<WorktreeStore>,
workspace: WeakEntity<Workspace>,
- _subscriptions: [Subscription; 1],
-}
-
-#[derive(Debug)]
-pub enum DebugPanelItemEvent {
- Close,
- Stopped { go_to_stack_frame: bool },
}
impl DebugSession {
@@ -59,9 +50,6 @@ impl DebugSession {
let quirks = session.read(cx).quirks();
cx.new(|cx| Self {
- _subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| {
- cx.notify();
- })],
remote_id: None,
running_state,
quirks,
@@ -87,7 +75,7 @@ impl DebugSession {
self.stack_trace_view.get_or_init(|| {
let stackframe_list = running_state.read(cx).stack_frame_list().clone();
- let stack_frame_view = cx.new(|cx| {
+ cx.new(|cx| {
StackTraceView::new(
workspace.clone(),
project.clone(),
@@ -95,9 +83,7 @@ impl DebugSession {
window,
cx,
)
- });
-
- stack_frame_view
+ })
})
}
@@ -135,7 +121,7 @@ impl DebugSession {
}
}
-impl EventEmitter<DebugPanelItemEvent> for DebugSession {}
+impl EventEmitter<()> for DebugSession {}
impl Focusable for DebugSession {
fn focus_handle(&self, cx: &App) -> FocusHandle {
@@ -144,7 +130,7 @@ impl Focusable for DebugSession {
}
impl Item for DebugSession {
- type Event = DebugPanelItemEvent;
+ type Event = ();
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Debugger".into()
}
@@ -14,7 +14,6 @@ use crate::{
session::running::memory_view::MemoryView,
};
-use super::DebugPanelItemEvent;
use anyhow::{Context as _, Result, anyhow};
use breakpoint_list::BreakpointList;
use collections::{HashMap, IndexMap};
@@ -36,7 +35,6 @@ use module_list::ModuleList;
use project::{
DebugScenarioContext, Project, WorktreeId,
debugger::session::{self, Session, SessionEvent, SessionStateEvent, ThreadId, ThreadStatus},
- terminals::TerminalKind,
};
use rpc::proto::ViewId;
use serde_json::Value;
@@ -102,7 +100,7 @@ impl Render for RunningState {
.find(|pane| pane.read(cx).is_zoomed());
let active = self.panes.panes().into_iter().next();
- let pane = if let Some(ref zoomed_pane) = zoomed_pane {
+ let pane = if let Some(zoomed_pane) = zoomed_pane {
zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element())
} else if let Some(active) = active {
self.panes
@@ -158,6 +156,29 @@ impl SubView {
})
}
+ pub(crate) fn stack_frame_list(
+ stack_frame_list: Entity<StackFrameList>,
+ cx: &mut App,
+ ) -> Entity<Self> {
+ let weak_list = stack_frame_list.downgrade();
+ let this = Self::new(
+ stack_frame_list.focus_handle(cx),
+ stack_frame_list.into(),
+ DebuggerPaneItem::Frames,
+ cx,
+ );
+
+ this.update(cx, |this, _| {
+ this.with_actions(Box::new(move |_, cx| {
+ weak_list
+ .update(cx, |this, _| this.render_control_strip())
+ .unwrap_or_else(|_| div().into_any_element())
+ }));
+ });
+
+ this
+ }
+
pub(crate) fn console(console: Entity<Console>, cx: &mut App) -> Entity<Self> {
let weak_console = console.downgrade();
let this = Self::new(
@@ -180,7 +201,7 @@ impl SubView {
let weak_list = list.downgrade();
let focus_handle = list.focus_handle(cx);
let this = Self::new(
- focus_handle.clone(),
+ focus_handle,
list.into(),
DebuggerPaneItem::BreakpointList,
cx,
@@ -291,7 +312,7 @@ pub(crate) fn new_debugger_pane(
let Some(project) = project.upgrade() else {
return ControlFlow::Break(());
};
- let this_pane = cx.entity().clone();
+ let this_pane = cx.entity();
let item = if tab.pane == this_pane {
pane.item_for_index(tab.ix)
} else {
@@ -358,7 +379,7 @@ pub(crate) fn new_debugger_pane(
}
};
- let ret = cx.new(move |cx| {
+ cx.new(move |cx| {
let mut pane = Pane::new(
workspace.clone(),
project.clone(),
@@ -414,7 +435,7 @@ pub(crate) fn new_debugger_pane(
.and_then(|item| item.downcast::<SubView>());
let is_hovered = as_subview
.as_ref()
- .map_or(false, |item| item.read(cx).hovered);
+ .is_some_and(|item| item.read(cx).hovered);
h_flex()
.track_focus(&focus_handle)
@@ -427,7 +448,6 @@ pub(crate) fn new_debugger_pane(
.bg(cx.theme().colors().tab_bar_background)
.on_action(|_: &menu::Cancel, window, cx| {
if cx.stop_active_drag(window) {
- return;
} else {
cx.propagate();
}
@@ -449,7 +469,7 @@ pub(crate) fn new_debugger_pane(
.children(pane.items().enumerate().map(|(ix, item)| {
let selected = active_pane_item
.as_ref()
- .map_or(false, |active| active.item_id() == item.item_id());
+ .is_some_and(|active| active.item_id() == item.item_id());
let deemphasized = !pane.has_focus(window, cx);
let item_ = item.boxed_clone();
div()
@@ -502,7 +522,7 @@ pub(crate) fn new_debugger_pane(
.on_drag(
DraggedTab {
item: item.boxed_clone(),
- pane: cx.entity().clone(),
+ pane: cx.entity(),
detail: 0,
is_active: selected,
ix,
@@ -563,9 +583,7 @@ pub(crate) fn new_debugger_pane(
}
});
pane
- });
-
- ret
+ })
}
pub struct DebugTerminal {
@@ -627,7 +645,7 @@ impl RunningState {
if s.starts_with("\"$ZED_") && s.ends_with('"') {
*s = s[1..s.len() - 1].to_string();
}
- if let Some(substituted) = substitute_variables_in_str(&s, context) {
+ if let Some(substituted) = substitute_variables_in_str(s, context) {
*s = substituted;
}
}
@@ -657,7 +675,7 @@ impl RunningState {
}
resolve_path(s);
- if let Some(substituted) = substitute_variables_in_str(&s, context) {
+ if let Some(substituted) = substitute_variables_in_str(s, context) {
*s = substituted;
}
}
@@ -919,7 +937,11 @@ impl RunningState {
let task_store = project.read(cx).task_store().downgrade();
let weak_project = project.downgrade();
let weak_workspace = workspace.downgrade();
- let is_local = project.read(cx).is_local();
+ let remote_shell = project
+ .read(cx)
+ .remote_client()
+ .as_ref()
+ .and_then(|remote| remote.read(cx).shell());
cx.spawn_in(window, async move |this, cx| {
let DebugScenario {
@@ -954,7 +976,7 @@ impl RunningState {
inventory.read(cx).task_template_by_label(
buffer,
worktree_id,
- &label,
+ label,
cx,
)
})
@@ -1003,7 +1025,7 @@ impl RunningState {
None
};
- let builder = ShellBuilder::new(is_local, &task.resolved.shell);
+ let builder = ShellBuilder::new(remote_shell.as_deref(), &task.resolved.shell);
let command_label = builder.command_label(&task.resolved.command_label);
let (command, args) =
builder.build(task.resolved.command.clone(), &task.resolved.args);
@@ -1016,12 +1038,11 @@ impl RunningState {
};
let terminal = project
.update(cx, |project, cx| {
- project.create_terminal(
- TerminalKind::Task(task_with_shell.clone()),
+ project.create_terminal_task(
+ task_with_shell.clone(),
cx,
)
- })?
- .await?;
+ })?.await?;
let terminal_view = cx.new_window_entity(|window, cx| {
TerminalView::new(
@@ -1116,9 +1137,8 @@ impl RunningState {
};
let session = self.session.read(cx);
- let cwd = Some(&request.cwd)
- .filter(|cwd| cwd.len() > 0)
- .map(PathBuf::from)
+ let cwd = (!request.cwd.is_empty())
+ .then(|| PathBuf::from(&request.cwd))
.or_else(|| session.binary().unwrap().cwd.clone());
let mut envs: HashMap<String, String> =
@@ -1153,7 +1173,7 @@ impl RunningState {
} else {
None
}
- } else if args.len() > 0 {
+ } else if !args.is_empty() {
Some(args.remove(0))
} else {
None
@@ -1166,13 +1186,13 @@ impl RunningState {
.filter(|title| !title.is_empty())
.or_else(|| command.clone())
.unwrap_or_else(|| "Debug terminal".to_string());
- let kind = TerminalKind::Task(task::SpawnInTerminal {
+ let kind = task::SpawnInTerminal {
id: task::TaskId("debug".to_string()),
full_label: title.clone(),
label: title.clone(),
- command: command.clone(),
+ command,
args,
- command_label: title.clone(),
+ command_label: title,
cwd,
env: envs,
use_new_terminal: true,
@@ -1184,12 +1204,13 @@ impl RunningState {
show_summary: false,
show_command: false,
show_rerun: false,
- });
+ };
let workspace = self.workspace.clone();
let weak_project = project.downgrade();
- let terminal_task = project.update(cx, |project, cx| project.create_terminal(kind, cx));
+ let terminal_task =
+ project.update(cx, |project, cx| project.create_terminal_task(kind, cx));
let terminal_task = cx.spawn_in(window, async move |_, cx| {
let terminal = terminal_task.await?;
@@ -1310,7 +1331,7 @@ impl RunningState {
let mut pane_item_status = IndexMap::from_iter(
DebuggerPaneItem::all()
.iter()
- .filter(|kind| kind.is_supported(&caps))
+ .filter(|kind| kind.is_supported(caps))
.map(|kind| (*kind, false)),
);
self.panes.panes().iter().for_each(|pane| {
@@ -1371,7 +1392,7 @@ impl RunningState {
this.serialize_layout(window, cx);
match event {
Event::Remove { .. } => {
- let _did_find_pane = this.panes.remove(&source_pane).is_ok();
+ let _did_find_pane = this.panes.remove(source_pane).is_ok();
debug_assert!(_did_find_pane);
cx.notify();
}
@@ -1759,7 +1780,7 @@ impl RunningState {
this.activate_item(0, false, false, window, cx);
});
- let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
+ let rightmost_pane = new_debugger_pane(workspace.clone(), project, window, cx);
rightmost_pane.update(cx, |this, cx| {
this.add_item(
Box::new(SubView::new(
@@ -1804,8 +1825,6 @@ impl RunningState {
}
}
-impl EventEmitter<DebugPanelItemEvent> for RunningState {}
-
impl Focusable for RunningState {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
@@ -219,7 +219,7 @@ impl BreakpointList {
});
self.input.update(cx, |this, cx| {
- this.set_placeholder_text(placeholder, cx);
+ this.set_placeholder_text(placeholder, window, cx);
this.set_read_only(is_exception_breakpoint);
this.set_text(active_value.as_deref().unwrap_or(""), window, cx);
});
@@ -239,14 +239,12 @@ impl BreakpointList {
}
fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
- if self.strip_mode.is_some() {
- if self.input.focus_handle(cx).contains_focused(window, cx) {
- cx.propagate();
- return;
- }
+ if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
}
let ix = match self.selected_ix {
- _ if self.breakpoints.len() == 0 => None,
+ _ if self.breakpoints.is_empty() => None,
None => Some(0),
Some(ix) => {
if ix == self.breakpoints.len() - 1 {
@@ -265,14 +263,12 @@ impl BreakpointList {
window: &mut Window,
cx: &mut Context<Self>,
) {
- if self.strip_mode.is_some() {
- if self.input.focus_handle(cx).contains_focused(window, cx) {
- cx.propagate();
- return;
- }
+ if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
}
let ix = match self.selected_ix {
- _ if self.breakpoints.len() == 0 => None,
+ _ if self.breakpoints.is_empty() => None,
None => Some(self.breakpoints.len() - 1),
Some(ix) => {
if ix == 0 {
@@ -286,13 +282,11 @@ impl BreakpointList {
}
fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
- if self.strip_mode.is_some() {
- if self.input.focus_handle(cx).contains_focused(window, cx) {
- cx.propagate();
- return;
- }
+ if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
}
- let ix = if self.breakpoints.len() > 0 {
+ let ix = if !self.breakpoints.is_empty() {
Some(0)
} else {
None
@@ -301,13 +295,11 @@ impl BreakpointList {
}
fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
- if self.strip_mode.is_some() {
- if self.input.focus_handle(cx).contains_focused(window, cx) {
- cx.propagate();
- return;
- }
+ if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
}
- let ix = if self.breakpoints.len() > 0 {
+ let ix = if !self.breakpoints.is_empty() {
Some(self.breakpoints.len() - 1)
} else {
None
@@ -337,8 +329,8 @@ impl BreakpointList {
let text = self.input.read(cx).text(cx);
match mode {
- ActiveBreakpointStripMode::Log => match &entry.kind {
- BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ ActiveBreakpointStripMode::Log => {
+ if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind {
Self::edit_line_breakpoint_inner(
&self.breakpoint_store,
line_breakpoint.breakpoint.path.clone(),
@@ -347,10 +339,9 @@ impl BreakpointList {
cx,
);
}
- _ => {}
- },
- ActiveBreakpointStripMode::Condition => match &entry.kind {
- BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ }
+ ActiveBreakpointStripMode::Condition => {
+ if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind {
Self::edit_line_breakpoint_inner(
&self.breakpoint_store,
line_breakpoint.breakpoint.path.clone(),
@@ -359,10 +350,9 @@ impl BreakpointList {
cx,
);
}
- _ => {}
- },
- ActiveBreakpointStripMode::HitCondition => match &entry.kind {
- BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ }
+ ActiveBreakpointStripMode::HitCondition => {
+ if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &entry.kind {
Self::edit_line_breakpoint_inner(
&self.breakpoint_store,
line_breakpoint.breakpoint.path.clone(),
@@ -371,8 +361,7 @@ impl BreakpointList {
cx,
);
}
- _ => {}
- },
+ }
}
self.focus_handle.focus(window);
} else {
@@ -401,11 +390,9 @@ impl BreakpointList {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
- if self.strip_mode.is_some() {
- if self.input.focus_handle(cx).contains_focused(window, cx) {
- cx.propagate();
- return;
- }
+ if self.strip_mode.is_some() && self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
}
match &mut entry.kind {
@@ -436,13 +423,10 @@ impl BreakpointList {
return;
};
- match &mut entry.kind {
- BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
- let path = line_breakpoint.breakpoint.path.clone();
- let row = line_breakpoint.breakpoint.row;
- self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
- }
- _ => {}
+ if let BreakpointEntryKind::LineBreakpoint(line_breakpoint) = &mut entry.kind {
+ let path = line_breakpoint.breakpoint.path.clone();
+ let row = line_breakpoint.breakpoint.row;
+ self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx);
}
cx.notify();
}
@@ -494,7 +478,7 @@ impl BreakpointList {
fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
if let Some(session) = &self.session {
session.update(cx, |this, cx| {
- this.toggle_data_breakpoint(&id, cx);
+ this.toggle_data_breakpoint(id, cx);
});
}
}
@@ -502,7 +486,7 @@ impl BreakpointList {
fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) {
if let Some(session) = &self.session {
session.update(cx, |this, cx| {
- this.toggle_exception_breakpoint(&id, cx);
+ this.toggle_exception_breakpoint(id, cx);
});
cx.notify();
const EXCEPTION_SERIALIZATION_INTERVAL: Duration = Duration::from_secs(1);
@@ -538,7 +522,7 @@ impl BreakpointList {
cx.background_executor()
.spawn(async move { KEY_VALUE_STORE.write_kvp(key, value?).await })
} else {
- return Task::ready(Result::Ok(()));
+ Task::ready(Result::Ok(()))
}
}
@@ -701,7 +685,6 @@ impl BreakpointList {
selection_kind.map(|kind| kind.0) != Some(SelectedBreakpointKind::Source),
)
.on_click({
- let focus_handle = focus_handle.clone();
move |_, window, cx| {
focus_handle.focus(window);
window.dispatch_action(UnsetBreakpoint.boxed_clone(), cx)
@@ -977,7 +960,7 @@ impl LineBreakpoint {
props,
breakpoint: BreakpointEntry {
kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
- weak: weak,
+ weak,
},
is_selected,
focus_handle,
@@ -1155,7 +1138,6 @@ impl ExceptionBreakpoint {
}
})
.on_click({
- let list = list.clone();
move |_, _, cx| {
list.update(cx, |this, cx| {
this.toggle_exception_breakpoint(&id, cx);
@@ -1189,7 +1171,7 @@ impl ExceptionBreakpoint {
props,
breakpoint: BreakpointEntry {
kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
- weak: weak,
+ weak,
},
is_selected,
focus_handle,
@@ -15,7 +15,7 @@ use gpui::{
use language::{Anchor, Buffer, CodeLabel, TextBufferSnapshot, ToOffset};
use menu::{Confirm, SelectNext, SelectPrevious};
use project::{
- Completion, CompletionResponse,
+ Completion, CompletionDisplayOptions, CompletionResponse,
debugger::session::{CompletionsQuery, OutputToken, Session},
lsp_store::CompletionDocumentation,
search_history::{SearchHistory, SearchHistoryCursor},
@@ -83,7 +83,7 @@ impl Console {
let this = cx.weak_entity();
let query_bar = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("Evaluate an expression", cx);
+ editor.set_placeholder_text("Evaluate an expression", window, cx);
editor.set_use_autoclose(false);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
@@ -365,7 +365,7 @@ impl Console {
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.when_some(keybinding_target.clone(), |el, keybinding_target| {
- el.context(keybinding_target.clone())
+ el.context(keybinding_target)
})
.action("Watch Expression", WatchExpression.boxed_clone())
}))
@@ -611,17 +611,16 @@ impl ConsoleQueryBarCompletionProvider {
for variable in console.variable_list.update(cx, |variable_list, cx| {
variable_list.completion_variables(cx)
}) {
- if let Some(evaluate_name) = &variable.evaluate_name {
- if variables
+ if let Some(evaluate_name) = &variable.evaluate_name
+ && variables
.insert(evaluate_name.clone(), variable.value.clone())
.is_none()
- {
- string_matches.push(StringMatchCandidate {
- id: 0,
- string: evaluate_name.clone(),
- char_bag: evaluate_name.chars().collect(),
- });
- }
+ {
+ string_matches.push(StringMatchCandidate {
+ id: 0,
+ string: evaluate_name.clone(),
+ char_bag: evaluate_name.chars().collect(),
+ });
}
if variables
@@ -686,6 +685,7 @@ impl ConsoleQueryBarCompletionProvider {
Ok(vec![project::CompletionResponse {
is_incomplete: completions.len() >= LIMIT,
+ display_options: CompletionDisplayOptions::default(),
completions,
}])
})
@@ -697,7 +697,7 @@ impl ConsoleQueryBarCompletionProvider {
new_bytes: &[u8],
snapshot: &TextBufferSnapshot,
) -> Range<Anchor> {
- let buffer_offset = buffer_position.to_offset(&snapshot);
+ let buffer_offset = buffer_position.to_offset(snapshot);
let buffer_bytes = &buffer_text.as_bytes()[0..buffer_offset];
let mut prefix_len = 0;
@@ -798,6 +798,7 @@ impl ConsoleQueryBarCompletionProvider {
Ok(vec![project::CompletionResponse {
completions,
+ display_options: CompletionDisplayOptions::default(),
is_incomplete: false,
}])
})
@@ -977,7 +978,7 @@ mod tests {
&cx.buffer_text(),
snapshot.anchor_before(buffer_position),
replacement.as_bytes(),
- &snapshot,
+ snapshot,
);
cx.update_editor(|editor, _, cx| {
@@ -57,7 +57,7 @@ impl LoadedSourceList {
h_flex()
.text_ui_xs(cx)
.text_color(cx.theme().colors().text_muted)
- .when_some(source.path.clone(), |this, path| this.child(path)),
+ .when_some(source.path, |this, path| this.child(path)),
)
.into_any()
}
@@ -262,7 +262,7 @@ impl MemoryView {
cx: &mut Context<Self>,
) {
use parse_int::parse;
- let Ok(as_address) = parse::<u64>(&memory_reference) else {
+ let Ok(as_address) = parse::<u64>(memory_reference) else {
return;
};
let access_size = evaluate_name
@@ -428,14 +428,14 @@ impl MemoryView {
if !self.is_writing_memory {
self.query_editor.update(cx, |this, cx| {
this.clear(window, cx);
- this.set_placeholder_text("Write to Selected Memory Range", cx);
+ this.set_placeholder_text("Write to Selected Memory Range", window, cx);
});
self.is_writing_memory = true;
self.query_editor.focus_handle(cx).focus(window);
} else {
self.query_editor.update(cx, |this, cx| {
this.clear(window, cx);
- this.set_placeholder_text("Go to Memory Address / Expression", cx);
+ this.set_placeholder_text("Go to Memory Address / Expression", window, cx);
});
self.is_writing_memory = false;
}
@@ -461,7 +461,7 @@ impl MemoryView {
let data_breakpoint_info = this.data_breakpoint_info(context.clone(), None, cx);
cx.spawn(async move |this, cx| {
if let Some(info) = data_breakpoint_info.await {
- let Some(data_id) = info.data_id.clone() else {
+ let Some(data_id) = info.data_id else {
return;
};
_ = this.update(cx, |this, cx| {
@@ -931,7 +931,7 @@ impl Render for MemoryView {
v_flex()
.size_full()
.on_drag_move(cx.listener(|this, evt, _, _| {
- this.handle_memory_drag(&evt);
+ this.handle_memory_drag(evt);
}))
.child(self.render_memory(cx).size_full())
.children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
@@ -157,7 +157,7 @@ impl ModuleList {
h_flex()
.text_ui_xs(cx)
.text_color(cx.theme().colors().text_muted)
- .when_some(module.path.clone(), |this, path| this.child(path)),
+ .when_some(module.path, |this, path| this.child(path)),
)
.into_any()
}
@@ -223,7 +223,7 @@ impl ModuleList {
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
let ix = match self.selected_ix {
- _ if self.entries.len() == 0 => None,
+ _ if self.entries.is_empty() => None,
None => Some(0),
Some(ix) => {
if ix == self.entries.len() - 1 {
@@ -243,7 +243,7 @@ impl ModuleList {
cx: &mut Context<Self>,
) {
let ix = match self.selected_ix {
- _ if self.entries.len() == 0 => None,
+ _ if self.entries.is_empty() => None,
None => Some(self.entries.len() - 1),
Some(ix) => {
if ix == 0 {
@@ -262,7 +262,7 @@ impl ModuleList {
_window: &mut Window,
cx: &mut Context<Self>,
) {
- let ix = if self.entries.len() > 0 {
+ let ix = if !self.entries.is_empty() {
Some(0)
} else {
None
@@ -271,7 +271,7 @@ impl ModuleList {
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
- let ix = if self.entries.len() > 0 {
+ let ix = if !self.entries.is_empty() {
Some(self.entries.len() - 1)
} else {
None
@@ -4,16 +4,17 @@ use std::time::Duration;
use anyhow::{Context as _, Result, anyhow};
use dap::StackFrameId;
+use db::kvp::KEY_VALUE_STORE;
use gpui::{
- AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton,
- Stateful, Subscription, Task, WeakEntity, list,
+ Action, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState,
+ MouseButton, Stateful, Subscription, Task, WeakEntity, list,
};
use util::debug_panic;
-use crate::StackTraceView;
+use crate::{StackTraceView, ToggleUserFrames};
use language::PointUtf16;
use project::debugger::breakpoint_store::ActiveStackFrame;
-use project::debugger::session::{Session, SessionEvent, StackFrame};
+use project::debugger::session::{Session, SessionEvent, StackFrame, ThreadStatus};
use project::{ProjectItem, ProjectPath};
use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
use workspace::{ItemHandle, Workspace};
@@ -26,6 +27,34 @@ pub enum StackFrameListEvent {
BuiltEntries,
}
+/// Represents the filter applied to the stack frame list
+#[derive(PartialEq, Eq, Copy, Clone, Debug)]
+pub(crate) enum StackFrameFilter {
+ /// Show all frames
+ All,
+ /// Show only frames from the user's code
+ OnlyUserFrames,
+}
+
+impl StackFrameFilter {
+ fn from_str_or_default(s: impl AsRef<str>) -> Self {
+ match s.as_ref() {
+ "user" => StackFrameFilter::OnlyUserFrames,
+ "all" => StackFrameFilter::All,
+ _ => StackFrameFilter::All,
+ }
+ }
+}
+
+impl From<StackFrameFilter> for String {
+ fn from(filter: StackFrameFilter) -> Self {
+ match filter {
+ StackFrameFilter::All => "all".to_string(),
+ StackFrameFilter::OnlyUserFrames => "user".to_string(),
+ }
+ }
+}
+
pub struct StackFrameList {
focus_handle: FocusHandle,
_subscription: Subscription,
@@ -37,6 +66,8 @@ pub struct StackFrameList {
opened_stack_frame_id: Option<StackFrameId>,
scrollbar_state: ScrollbarState,
list_state: ListState,
+ list_filter: StackFrameFilter,
+ filter_entries_indices: Vec<usize>,
error: Option<SharedString>,
_refresh_task: Task<()>,
}
@@ -73,6 +104,16 @@ impl StackFrameList {
let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
let scrollbar_state = ScrollbarState::new(list_state.clone());
+ let list_filter = KEY_VALUE_STORE
+ .read_kvp(&format!(
+ "stack-frame-list-filter-{}",
+ session.read(cx).adapter().0
+ ))
+ .ok()
+ .flatten()
+ .map(StackFrameFilter::from_str_or_default)
+ .unwrap_or(StackFrameFilter::All);
+
let mut this = Self {
session,
workspace,
@@ -80,9 +121,11 @@ impl StackFrameList {
state,
_subscription,
entries: Default::default(),
+ filter_entries_indices: Vec::default(),
error: None,
selected_ix: None,
opened_stack_frame_id: None,
+ list_filter,
list_state,
scrollbar_state,
_refresh_task: Task::ready(()),
@@ -103,7 +146,15 @@ impl StackFrameList {
) -> Vec<dap::StackFrame> {
self.entries
.iter()
- .flat_map(|frame| match frame {
+ .enumerate()
+ .filter(|(ix, _)| {
+ self.list_filter == StackFrameFilter::All
+ || self
+ .filter_entries_indices
+ .binary_search_by_key(&ix, |ix| ix)
+ .is_ok()
+ })
+ .flat_map(|(_, frame)| match frame {
StackFrameEntry::Normal(frame) => vec![frame.clone()],
StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()],
StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(),
@@ -123,11 +174,29 @@ impl StackFrameList {
#[cfg(test)]
pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
- self.stack_frames(cx)
- .unwrap_or_default()
- .into_iter()
- .map(|stack_frame| stack_frame.dap.clone())
- .collect()
+ match self.list_filter {
+ StackFrameFilter::All => self
+ .stack_frames(cx)
+ .unwrap_or_default()
+ .into_iter()
+ .map(|stack_frame| stack_frame.dap)
+ .collect(),
+ StackFrameFilter::OnlyUserFrames => self
+ .filter_entries_indices
+ .iter()
+ .map(|ix| match &self.entries[*ix] {
+ StackFrameEntry::Label(label) => label,
+ StackFrameEntry::Collapsed(_) => panic!("Collapsed tabs should not be visible"),
+ StackFrameEntry::Normal(frame) => frame,
+ })
+ .cloned()
+ .collect(),
+ }
+ }
+
+ #[cfg(test)]
+ pub(crate) fn list_filter(&self) -> StackFrameFilter {
+ self.list_filter
}
pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
@@ -187,12 +256,34 @@ impl StackFrameList {
self.entries.clear();
self.selected_ix = None;
self.list_state.reset(0);
+ self.filter_entries_indices.clear();
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
return;
}
};
- for stack_frame in &stack_frames {
+
+ let worktree_prefixes: Vec<_> = self
+ .workspace
+ .read_with(cx, |workspace, cx| {
+ workspace
+ .visible_worktrees(cx)
+ .map(|tree| tree.read(cx).abs_path())
+ .collect()
+ })
+ .unwrap_or_default();
+
+ let mut filter_entries_indices = Vec::default();
+ for stack_frame in stack_frames.iter() {
+ let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| {
+ source.path.as_ref().is_some_and(|path| {
+ worktree_prefixes
+ .iter()
+ .filter_map(|tree| tree.to_str())
+ .any(|tree| path.starts_with(tree))
+ })
+ });
+
match stack_frame.dap.presentation_hint {
Some(dap::StackFramePresentationHint::Deemphasize)
| Some(dap::StackFramePresentationHint::Subtle) => {
@@ -218,15 +309,19 @@ impl StackFrameList {
first_stack_frame_with_path.get_or_insert(entries.len());
}
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
+ if frame_in_visible_worktree {
+ filter_entries_indices.push(entries.len() - 1);
+ }
}
}
}
let collapsed_entries = std::mem::take(&mut collapsed_entries);
if !collapsed_entries.is_empty() {
- entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
+ entries.push(StackFrameEntry::Collapsed(collapsed_entries));
}
self.entries = entries;
+ self.filter_entries_indices = filter_entries_indices;
if let Some(ix) = first_stack_frame_with_path
.or(first_stack_frame)
@@ -242,7 +337,14 @@ impl StackFrameList {
self.selected_ix = ix;
}
- self.list_state.reset(self.entries.len());
+ match self.list_filter {
+ StackFrameFilter::All => {
+ self.list_state.reset(self.entries.len());
+ }
+ StackFrameFilter::OnlyUserFrames => {
+ self.list_state.reset(self.filter_entries_indices.len());
+ }
+ }
cx.emit(StackFrameListEvent::BuiltEntries);
cx.notify();
}
@@ -418,7 +520,7 @@ impl StackFrameList {
let source = stack_frame.source.clone();
let is_selected_frame = Some(ix) == self.selected_ix;
- let path = source.clone().and_then(|s| s.path.or(s.name));
+ let path = source.and_then(|s| s.path.or(s.name));
let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
let formatted_path = formatted_path.map(|path| {
Label::new(path)
@@ -519,7 +621,16 @@ impl StackFrameList {
let entries = std::mem::take(stack_frames)
.into_iter()
.map(StackFrameEntry::Normal);
+ // HERE
+ let entries_len = entries.len();
self.entries.splice(ix..ix + 1, entries);
+ let (Ok(filtered_indices_start) | Err(filtered_indices_start)) =
+ self.filter_entries_indices.binary_search(&ix);
+
+ for idx in &mut self.filter_entries_indices[filtered_indices_start..] {
+ *idx += entries_len - 1;
+ }
+
self.selected_ix = Some(ix);
self.list_state.reset(self.entries.len());
cx.emit(StackFrameListEvent::BuiltEntries);
@@ -572,6 +683,11 @@ impl StackFrameList {
}
fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
+ let ix = match self.list_filter {
+ StackFrameFilter::All => ix,
+ StackFrameFilter::OnlyUserFrames => self.filter_entries_indices[ix],
+ };
+
match &self.entries[ix] {
StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
@@ -621,7 +737,7 @@ impl StackFrameList {
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
let ix = match self.selected_ix {
- _ if self.entries.len() == 0 => None,
+ _ if self.entries.is_empty() => None,
None => Some(0),
Some(ix) => {
if ix == self.entries.len() - 1 {
@@ -641,7 +757,7 @@ impl StackFrameList {
cx: &mut Context<Self>,
) {
let ix = match self.selected_ix {
- _ if self.entries.len() == 0 => None,
+ _ if self.entries.is_empty() => None,
None => Some(self.entries.len() - 1),
Some(ix) => {
if ix == 0 {
@@ -660,7 +776,7 @@ impl StackFrameList {
_window: &mut Window,
cx: &mut Context<Self>,
) {
- let ix = if self.entries.len() > 0 {
+ let ix = if !self.entries.is_empty() {
Some(0)
} else {
None
@@ -669,7 +785,7 @@ impl StackFrameList {
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
- let ix = if self.entries.len() > 0 {
+ let ix = if !self.entries.is_empty() {
Some(self.entries.len() - 1)
} else {
None
@@ -702,6 +818,67 @@ impl StackFrameList {
self.activate_selected_entry(window, cx);
}
+ pub(crate) fn toggle_frame_filter(
+ &mut self,
+ thread_status: Option<ThreadStatus>,
+ cx: &mut Context<Self>,
+ ) {
+ self.list_filter = match self.list_filter {
+ StackFrameFilter::All => StackFrameFilter::OnlyUserFrames,
+ StackFrameFilter::OnlyUserFrames => StackFrameFilter::All,
+ };
+
+ if let Some(database_id) = self
+ .workspace
+ .read_with(cx, |workspace, _| workspace.database_id())
+ .ok()
+ .flatten()
+ {
+ let database_id: i64 = database_id.into();
+ let save_task = KEY_VALUE_STORE.write_kvp(
+ format!(
+ "stack-frame-list-filter-{}-{}",
+ self.session.read(cx).adapter().0,
+ database_id,
+ ),
+ self.list_filter.into(),
+ );
+ cx.background_spawn(save_task).detach();
+ }
+
+ if let Some(ThreadStatus::Stopped) = thread_status {
+ match self.list_filter {
+ StackFrameFilter::All => {
+ self.list_state.reset(self.entries.len());
+ }
+ StackFrameFilter::OnlyUserFrames => {
+ self.list_state.reset(self.filter_entries_indices.len());
+ if !self
+ .selected_ix
+ .map(|ix| self.filter_entries_indices.contains(&ix))
+ .unwrap_or_default()
+ {
+ self.selected_ix = None;
+ }
+ }
+ }
+
+ if let Some(ix) = self.selected_ix {
+ let scroll_to = match self.list_filter {
+ StackFrameFilter::All => ix,
+ StackFrameFilter::OnlyUserFrames => self
+ .filter_entries_indices
+ .binary_search_by_key(&ix, |ix| *ix)
+ .expect("This index will always exist"),
+ };
+ self.list_state.scroll_to_reveal_item(scroll_to);
+ }
+
+ cx.emit(StackFrameListEvent::BuiltEntries);
+ cx.notify();
+ }
+ }
+
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().p_1().size_full().child(
list(
@@ -711,6 +888,30 @@ impl StackFrameList {
.size_full(),
)
}
+
+ pub(crate) fn render_control_strip(&self) -> AnyElement {
+ let tooltip_title = match self.list_filter {
+ StackFrameFilter::All => "Show stack frames from your project",
+ StackFrameFilter::OnlyUserFrames => "Show all stack frames",
+ };
+
+ h_flex()
+ .child(
+ IconButton::new(
+ "filter-by-visible-worktree-stack-frame-list",
+ IconName::ListFilter,
+ )
+ .tooltip(move |window, cx| {
+ Tooltip::for_action(tooltip_title, &ToggleUserFrames, window, cx)
+ })
+ .toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames)
+ .icon_size(IconSize::Small)
+ .on_click(|_, window, cx| {
+ window.dispatch_action(ToggleUserFrames.boxed_clone(), cx)
+ }),
+ )
+ .into_any_element()
+ }
}
impl Render for StackFrameList {
@@ -272,7 +272,7 @@ impl VariableList {
let mut entries = vec![];
let scopes: Vec<_> = self.session.update(cx, |session, cx| {
- session.scopes(stack_frame_id, cx).iter().cloned().collect()
+ session.scopes(stack_frame_id, cx).to_vec()
});
let mut contains_local_scope = false;
@@ -291,7 +291,7 @@ impl VariableList {
}
self.session.update(cx, |session, cx| {
- session.variables(scope.variables_reference, cx).len() > 0
+ !session.variables(scope.variables_reference, cx).is_empty()
})
})
.map(|scope| {
@@ -313,7 +313,7 @@ impl VariableList {
watcher.variables_reference,
watcher.variables_reference,
EntryPath::for_watcher(watcher.expression.clone()),
- DapEntry::Watcher(watcher.clone()),
+ DapEntry::Watcher(watcher),
)
})
.collect::<Vec<_>>(),
@@ -947,7 +947,7 @@ impl VariableList {
#[track_caller]
#[cfg(test)]
pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) {
- const INDENT: &'static str = " ";
+ const INDENT: &str = " ";
let entries = &self.entries;
let mut visual_entries = Vec::with_capacity(entries.len());
@@ -997,7 +997,7 @@ impl VariableList {
DapEntry::Watcher { .. } => continue,
DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()),
DapEntry::Scope(scope) => {
- if scopes.len() > 0 {
+ if !scopes.is_empty() {
idx += 1;
}
@@ -1289,7 +1289,7 @@ impl VariableList {
}),
)
.child(self.render_variable_value(
- &entry,
+ entry,
&variable_color,
watcher.value.to_string(),
cx,
@@ -1301,8 +1301,6 @@ impl VariableList {
IconName::Close,
)
.on_click({
- let weak = weak.clone();
- let path = path.clone();
move |_, window, cx| {
weak.update(cx, |variable_list, cx| {
variable_list.selection = Some(path.clone());
@@ -1470,7 +1468,6 @@ impl VariableList {
}))
})
.on_secondary_mouse_down(cx.listener({
- let path = path.clone();
let entry = variable.clone();
move |this, event: &MouseDownEvent, window, cx| {
this.selection = Some(path.clone());
@@ -1494,7 +1491,7 @@ impl VariableList {
}),
)
.child(self.render_variable_value(
- &variable,
+ variable,
&variable_color,
dap.value.clone(),
cx,
@@ -139,7 +139,7 @@ async fn test_show_attach_modal_and_select_process(
workspace
.update(cx, |_, window, cx| {
let names =
- attach_modal.update(cx, |modal, cx| attach_modal::_process_names(&modal, cx));
+ attach_modal.update(cx, |modal, cx| attach_modal::_process_names(modal, cx));
// Initially all processes are visible.
assert_eq!(3, names.len());
attach_modal.update(cx, |this, cx| {
@@ -154,7 +154,7 @@ async fn test_show_attach_modal_and_select_process(
workspace
.update(cx, |_, _, cx| {
let names =
- attach_modal.update(cx, |modal, cx| attach_modal::_process_names(&modal, cx));
+ attach_modal.update(cx, |modal, cx| attach_modal::_process_names(modal, cx));
// Initially all processes are visible.
assert_eq!(2, names.len());
})
@@ -1330,7 +1330,6 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
let called_set_breakpoints = Arc::new(AtomicBool::new(false));
client.on_request::<SetBreakpoints, _>({
- let called_set_breakpoints = called_set_breakpoints.clone();
move |_, args| {
assert!(
args.breakpoints.is_none_or(|bps| bps.is_empty()),
@@ -1445,7 +1444,6 @@ async fn test_we_send_arguments_from_user_config(
let launch_handler_called = Arc::new(AtomicBool::new(false));
start_debug_session_with(&workspace, cx, debug_definition.clone(), {
- let debug_definition = debug_definition.clone();
let launch_handler_called = launch_handler_called.clone();
move |client| {
@@ -1783,9 +1781,8 @@ async fn test_debug_adapters_shutdown_on_app_quit(
let disconnect_request_received = Arc::new(AtomicBool::new(false));
let disconnect_clone = disconnect_request_received.clone();
- let disconnect_clone_for_handler = disconnect_clone.clone();
client.on_request::<Disconnect, _>(move |_, _| {
- disconnect_clone_for_handler.store(true, Ordering::SeqCst);
+ disconnect_clone.store(true, Ordering::SeqCst);
Ok(())
});
@@ -106,9 +106,7 @@ async fn test_debug_session_substitutes_variables_and_relativizes_paths(
);
let expected_other_field = if input_path.contains("$ZED_WORKTREE_ROOT") {
- input_path
- .replace("$ZED_WORKTREE_ROOT", &path!("/test/worktree/path"))
- .to_owned()
+ input_path.replace("$ZED_WORKTREE_ROOT", path!("/test/worktree/path"))
} else {
input_path.to_string()
};
@@ -1,6 +1,6 @@
use crate::{
debugger_panel::DebugPanel,
- session::running::stack_frame_list::StackFrameEntry,
+ session::running::stack_frame_list::{StackFrameEntry, StackFrameFilter},
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
};
use dap::{
@@ -752,3 +752,346 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo
});
});
}
+
+#[gpui::test]
+async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(executor.clone());
+
+ let test_file_content = r#"
+ function main() {
+ doSomething();
+ }
+
+ function doSomething() {
+ console.log('doing something');
+ }
+ "#
+ .unindent();
+
+ fs.insert_tree(
+ path!("/project"),
+ json!({
+ "src": {
+ "test.js": test_file_content,
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
+ let workspace = init_test_workspace(&project, cx).await;
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
+ let client = session.update(cx, |session, _| session.adapter_client().unwrap());
+
+ client.on_request::<Threads, _>(move |_, _| {
+ Ok(dap::ThreadsResponse {
+ threads: vec![dap::Thread {
+ id: 1,
+ name: "Thread 1".into(),
+ }],
+ })
+ });
+
+ client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
+
+ let stack_frames = vec![
+ StackFrame {
+ id: 1,
+ name: "main".into(),
+ source: Some(dap::Source {
+ name: Some("test.js".into()),
+ path: Some(path!("/project/src/test.js").into()),
+ source_reference: None,
+ presentation_hint: None,
+ origin: None,
+ sources: None,
+ adapter_data: None,
+ checksums: None,
+ }),
+ line: 2,
+ column: 1,
+ end_line: None,
+ end_column: None,
+ can_restart: None,
+ instruction_pointer_reference: None,
+ module_id: None,
+ presentation_hint: None,
+ },
+ StackFrame {
+ id: 2,
+ name: "node:internal/modules/cjs/loader".into(),
+ source: Some(dap::Source {
+ name: Some("loader.js".into()),
+ path: Some(path!("/usr/lib/node/internal/modules/cjs/loader.js").into()),
+ source_reference: None,
+ presentation_hint: None,
+ origin: None,
+ sources: None,
+ adapter_data: None,
+ checksums: None,
+ }),
+ line: 100,
+ column: 1,
+ end_line: None,
+ end_column: None,
+ can_restart: None,
+ instruction_pointer_reference: None,
+ module_id: None,
+ presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
+ },
+ StackFrame {
+ id: 3,
+ name: "node:internal/modules/run_main".into(),
+ source: Some(dap::Source {
+ name: Some("run_main.js".into()),
+ path: Some(path!("/usr/lib/node/internal/modules/run_main.js").into()),
+ source_reference: None,
+ presentation_hint: None,
+ origin: None,
+ sources: None,
+ adapter_data: None,
+ checksums: None,
+ }),
+ line: 50,
+ column: 1,
+ end_line: None,
+ end_column: None,
+ can_restart: None,
+ instruction_pointer_reference: None,
+ module_id: None,
+ presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
+ },
+ StackFrame {
+ id: 4,
+ name: "node:internal/modules/run_main2".into(),
+ source: Some(dap::Source {
+ name: Some("run_main.js".into()),
+ path: Some(path!("/usr/lib/node/internal/modules/run_main2.js").into()),
+ source_reference: None,
+ presentation_hint: None,
+ origin: None,
+ sources: None,
+ adapter_data: None,
+ checksums: None,
+ }),
+ line: 50,
+ column: 1,
+ end_line: None,
+ end_column: None,
+ can_restart: None,
+ instruction_pointer_reference: None,
+ module_id: None,
+ presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
+ },
+ StackFrame {
+ id: 5,
+ name: "doSomething".into(),
+ source: Some(dap::Source {
+ name: Some("test.js".into()),
+ path: Some(path!("/project/src/test.js").into()),
+ source_reference: None,
+ presentation_hint: None,
+ origin: None,
+ sources: None,
+ adapter_data: None,
+ checksums: None,
+ }),
+ line: 3,
+ column: 1,
+ end_line: None,
+ end_column: None,
+ can_restart: None,
+ instruction_pointer_reference: None,
+ module_id: None,
+ presentation_hint: None,
+ },
+ ];
+
+ // Store a copy for assertions
+ let stack_frames_for_assertions = stack_frames.clone();
+
+ client.on_request::<StackTrace, _>({
+ let stack_frames = Arc::new(stack_frames.clone());
+ move |_, args| {
+ assert_eq!(1, args.thread_id);
+
+ Ok(dap::StackTraceResponse {
+ stack_frames: (*stack_frames).clone(),
+ total_frames: None,
+ })
+ }
+ });
+
+ client
+ .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
+ reason: dap::StoppedEventReason::Pause,
+ description: None,
+ thread_id: Some(1),
+ preserve_focus_hint: None,
+ text: None,
+ all_threads_stopped: None,
+ hit_breakpoint_ids: None,
+ }))
+ .await;
+
+ cx.run_until_parked();
+
+ // trigger threads to load
+ active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
+ session.running_state().update(cx, |running_state, cx| {
+ running_state
+ .session()
+ .update(cx, |session, cx| session.threads(cx));
+ });
+ });
+
+ cx.run_until_parked();
+
+ // select first thread
+ active_debug_session_panel(workspace, cx).update_in(cx, |session, window, cx| {
+ session.running_state().update(cx, |running_state, cx| {
+ running_state.select_current_thread(
+ &running_state
+ .session()
+ .update(cx, |session, cx| session.threads(cx)),
+ window,
+ cx,
+ );
+ });
+ });
+
+ cx.run_until_parked();
+
+ // trigger stack frames to load
+ active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
+ let stack_frame_list = debug_panel_item
+ .running_state()
+ .update(cx, |state, _| state.stack_frame_list().clone());
+
+ stack_frame_list.update(cx, |stack_frame_list, cx| {
+ stack_frame_list.dap_stack_frames(cx);
+ });
+ });
+
+ cx.run_until_parked();
+
+ let stack_frame_list =
+ active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
+ let stack_frame_list = debug_panel_item
+ .running_state()
+ .update(cx, |state, _| state.stack_frame_list().clone());
+
+ stack_frame_list.update(cx, |stack_frame_list, cx| {
+ stack_frame_list.build_entries(true, window, cx);
+
+ // Verify we have the expected collapsed structure
+ assert_eq!(
+ stack_frame_list.entries(),
+ &vec![
+ StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
+ StackFrameEntry::Collapsed(vec![
+ stack_frames_for_assertions[1].clone(),
+ stack_frames_for_assertions[2].clone(),
+ stack_frames_for_assertions[3].clone()
+ ]),
+ StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
+ ]
+ );
+ });
+
+ stack_frame_list
+ });
+
+ stack_frame_list.update(cx, |stack_frame_list, cx| {
+ let all_frames = stack_frame_list.flatten_entries(true, false);
+ assert_eq!(all_frames.len(), 5, "Should see all 5 frames initially");
+
+ stack_frame_list
+ .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
+ assert_eq!(
+ stack_frame_list.list_filter(),
+ StackFrameFilter::OnlyUserFrames
+ );
+ });
+
+ stack_frame_list.update(cx, |stack_frame_list, cx| {
+ let user_frames = stack_frame_list.dap_stack_frames(cx);
+ assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
+ assert_eq!(user_frames[0].name, "main");
+ assert_eq!(user_frames[1].name, "doSomething");
+
+ // Toggle back to all frames
+ stack_frame_list
+ .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
+ assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
+ });
+
+ stack_frame_list.update(cx, |stack_frame_list, cx| {
+ let all_frames_again = stack_frame_list.flatten_entries(true, false);
+ assert_eq!(
+ all_frames_again.len(),
+ 5,
+ "Should see all 5 frames after toggling back"
+ );
+
+ // Test 3: Verify collapsed entries stay expanded
+ stack_frame_list.expand_collapsed_entry(1, cx);
+ assert_eq!(
+ stack_frame_list.entries(),
+ &vec![
+ StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
+ StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
+ StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
+ StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
+ StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
+ ]
+ );
+
+ stack_frame_list
+ .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
+ assert_eq!(
+ stack_frame_list.list_filter(),
+ StackFrameFilter::OnlyUserFrames
+ );
+ });
+
+ stack_frame_list.update(cx, |stack_frame_list, cx| {
+ stack_frame_list
+ .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
+ assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
+ });
+
+ stack_frame_list.update(cx, |stack_frame_list, cx| {
+ stack_frame_list
+ .toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
+ assert_eq!(
+ stack_frame_list.list_filter(),
+ StackFrameFilter::OnlyUserFrames
+ );
+
+ assert_eq!(
+ stack_frame_list.dap_stack_frames(cx).as_slice(),
+ &[
+ stack_frames_for_assertions[0].clone(),
+ stack_frames_for_assertions[4].clone()
+ ]
+ );
+
+ // Verify entries remain expanded
+ assert_eq!(
+ stack_frame_list.entries(),
+ &vec![
+ StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
+ StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
+ StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
+ StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
+ StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
+ ],
+ "Expanded entries should remain expanded after toggling filter"
+ );
+ });
+}
@@ -1445,11 +1445,8 @@ async fn test_variable_list_only_sends_requests_when_rendering(
cx.run_until_parked();
- let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, _, _| {
- let state = item.running_state().clone();
-
- state
- });
+ let running_state = active_debug_session_panel(workspace, cx)
+ .update_in(cx, |item, _, _| item.running_state().clone());
client
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::convert::TryFrom;
-pub const DEEPSEEK_API_URL: &str = "https://api.deepseek.com";
+pub const DEEPSEEK_API_URL: &str = "https://api.deepseek.com/v1";
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
@@ -96,7 +96,7 @@ impl Model {
pub fn max_token_count(&self) -> u64 {
match self {
- Self::Chat | Self::Reasoner => 64_000,
+ Self::Chat | Self::Reasoner => 128_000,
Self::Custom { max_tokens, .. } => *max_tokens,
}
}
@@ -104,7 +104,7 @@ impl Model {
pub fn max_output_tokens(&self) -> Option<u64> {
match self {
Self::Chat => Some(8_192),
- Self::Reasoner => Some(8_192),
+ Self::Reasoner => Some(64_000),
Self::Custom {
max_output_tokens, ..
} => *max_output_tokens,
@@ -263,12 +263,12 @@ pub async fn stream_completion(
api_key: &str,
request: Request,
) -> Result<BoxStream<'static, Result<StreamResponse>>> {
- let uri = format!("{api_url}/v1/chat/completions");
+ let uri = format!("{api_url}/chat/completions");
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")
- .header("Authorization", format!("Bearer {}", api_key));
+ .header("Authorization", format!("Bearer {}", api_key.trim()));
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;
@@ -18,7 +18,6 @@ collections.workspace = true
component.workspace = true
ctor.workspace = true
editor.workspace = true
-futures.workspace = true
gpui.workspace = true
indoc.workspace = true
language.workspace = true
@@ -0,0 +1,982 @@
+use crate::{
+ DIAGNOSTICS_UPDATE_DELAY, IncludeWarnings, ToggleWarnings, context_range_for_entry,
+ diagnostic_renderer::{DiagnosticBlock, DiagnosticRenderer},
+ toolbar_controls::DiagnosticsToolbarEditor,
+};
+use anyhow::Result;
+use collections::HashMap;
+use editor::{
+ Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
+ display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
+ multibuffer_context_lines,
+};
+use gpui::{
+ AnyElement, App, AppContext, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
+ InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
+ Task, WeakEntity, Window, actions, div,
+};
+use language::{Buffer, DiagnosticEntry, Point};
+use project::{
+ DiagnosticSummary, Event, Project, ProjectItem, ProjectPath,
+ project_settings::{DiagnosticSeverity, ProjectSettings},
+};
+use settings::Settings;
+use std::{
+ any::{Any, TypeId},
+ cmp::Ordering,
+ sync::Arc,
+};
+use text::{Anchor, BufferSnapshot, OffsetRangeExt};
+use ui::{Button, ButtonStyle, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
+use util::paths::PathExt;
+use workspace::{
+ ItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace,
+ item::{BreadcrumbText, Item, ItemEvent, TabContentParams},
+};
+
+actions!(
+ diagnostics,
+ [
+ /// Opens the project diagnostics view for the currently focused file.
+ DeployCurrentFile,
+ ]
+);
+
+/// The `BufferDiagnosticsEditor` is meant to be used when dealing specifically
+/// with diagnostics for a single buffer, as only the excerpts of the buffer
+/// where diagnostics are available are displayed.
+pub(crate) struct BufferDiagnosticsEditor {
+ pub project: Entity<Project>,
+ focus_handle: FocusHandle,
+ editor: Entity<Editor>,
+ /// The current diagnostic entries in the `BufferDiagnosticsEditor`. Used to
+ /// allow quick comparison of updated diagnostics, to confirm if anything
+ /// has changed.
+ pub(crate) diagnostics: Vec<DiagnosticEntry<Anchor>>,
+ /// The blocks used to display the diagnostics' content in the editor, next
+ /// to the excerpts where the diagnostic originated.
+ blocks: Vec<CustomBlockId>,
+ /// Multibuffer to contain all excerpts that contain diagnostics, which are
+ /// to be rendered in the editor.
+ multibuffer: Entity<MultiBuffer>,
+ /// The buffer for which the editor is displaying diagnostics and excerpts
+ /// for.
+ buffer: Option<Entity<Buffer>>,
+ /// The path for which the editor is displaying diagnostics for.
+ project_path: ProjectPath,
+ /// Summary of the number of warnings and errors for the path. Used to
+ /// display the number of warnings and errors in the tab's content.
+ summary: DiagnosticSummary,
+ /// Whether to include warnings in the list of diagnostics shown in the
+ /// editor.
+ pub(crate) include_warnings: bool,
+ /// Keeps track of whether there's a background task already running to
+ /// update the excerpts, in order to avoid firing multiple tasks for this purpose.
+ pub(crate) update_excerpts_task: Option<Task<Result<()>>>,
+ /// The project's subscription, responsible for processing events related to
+ /// diagnostics.
+ _subscription: Subscription,
+}
+
+impl BufferDiagnosticsEditor {
+ /// Creates new instance of the `BufferDiagnosticsEditor` which can then be
+ /// displayed by adding it to a pane.
+ pub fn new(
+ project_path: ProjectPath,
+ project_handle: Entity<Project>,
+ buffer: Option<Entity<Buffer>>,
+ include_warnings: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ // Subscribe to project events related to diagnostics so the
+ // `BufferDiagnosticsEditor` can update its state accordingly.
+ let project_event_subscription = cx.subscribe_in(
+ &project_handle,
+ window,
+ |buffer_diagnostics_editor, _project, event, window, cx| match event {
+ Event::DiskBasedDiagnosticsStarted { .. } => {
+ cx.notify();
+ }
+ Event::DiskBasedDiagnosticsFinished { .. } => {
+ buffer_diagnostics_editor.update_all_excerpts(window, cx);
+ }
+ Event::DiagnosticsUpdated {
+ paths,
+ language_server_id,
+ } => {
+ // When diagnostics have been updated, the
+ // `BufferDiagnosticsEditor` should update its state only if
+ // one of the paths matches its `project_path`, otherwise
+ // the event should be ignored.
+ if paths.contains(&buffer_diagnostics_editor.project_path) {
+ buffer_diagnostics_editor.update_diagnostic_summary(cx);
+
+ if buffer_diagnostics_editor.editor.focus_handle(cx).contains_focused(window, cx) || buffer_diagnostics_editor.focus_handle.contains_focused(window, cx) {
+ log::debug!("diagnostics updated for server {language_server_id}. recording change");
+ } else {
+ log::debug!("diagnostics updated for server {language_server_id}. updating excerpts");
+ buffer_diagnostics_editor.update_all_excerpts(window, cx);
+ }
+ }
+ }
+ _ => {}
+ },
+ );
+
+ let focus_handle = cx.focus_handle();
+
+ cx.on_focus_in(
+ &focus_handle,
+ window,
+ |buffer_diagnostics_editor, window, cx| buffer_diagnostics_editor.focus_in(window, cx),
+ )
+ .detach();
+
+ cx.on_focus_out(
+ &focus_handle,
+ window,
+ |buffer_diagnostics_editor, _event, window, cx| {
+ buffer_diagnostics_editor.focus_out(window, cx)
+ },
+ )
+ .detach();
+
+ let summary = project_handle
+ .read(cx)
+ .diagnostic_summary_for_path(&project_path, cx);
+
+ let multibuffer = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
+ let max_severity = Self::max_diagnostics_severity(include_warnings);
+ let editor = cx.new(|cx| {
+ let mut editor = Editor::for_multibuffer(
+ multibuffer.clone(),
+ Some(project_handle.clone()),
+ window,
+ cx,
+ );
+ editor.set_vertical_scroll_margin(5, cx);
+ editor.disable_inline_diagnostics();
+ editor.set_max_diagnostics_severity(max_severity, cx);
+ editor.set_all_diagnostics_active(cx);
+ editor
+ });
+
+ // Subscribe to events triggered by the editor in order to correctly
+ // update the buffer's excerpts.
+ cx.subscribe_in(
+ &editor,
+ window,
+ |buffer_diagnostics_editor, _editor, event: &EditorEvent, window, cx| {
+ cx.emit(event.clone());
+
+ match event {
+ // If the user tries to focus on the editor but there's actually
+ // no excerpts for the buffer, focus back on the
+ // `BufferDiagnosticsEditor` instance.
+ EditorEvent::Focused => {
+ if buffer_diagnostics_editor.multibuffer.read(cx).is_empty() {
+ window.focus(&buffer_diagnostics_editor.focus_handle);
+ }
+ }
+ EditorEvent::Blurred => {
+ buffer_diagnostics_editor.update_all_excerpts(window, cx)
+ }
+ _ => {}
+ }
+ },
+ )
+ .detach();
+
+ let diagnostics = vec![];
+ let update_excerpts_task = None;
+ let mut buffer_diagnostics_editor = Self {
+ project: project_handle,
+ focus_handle,
+ editor,
+ diagnostics,
+ blocks: Default::default(),
+ multibuffer,
+ buffer,
+ project_path,
+ summary,
+ include_warnings,
+ update_excerpts_task,
+ _subscription: project_event_subscription,
+ };
+
+ buffer_diagnostics_editor.update_all_diagnostics(window, cx);
+ buffer_diagnostics_editor
+ }
+
+ fn deploy(
+ workspace: &mut Workspace,
+ _: &DeployCurrentFile,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ ) {
+ // Determine the currently opened path by finding the active editor and
+ // finding the project path for the buffer.
+ // If there's no active editor with a project path, avoiding deploying
+ // the buffer diagnostics view.
+ if let Some(editor) = workspace.active_item_as::<Editor>(cx)
+ && let Some(project_path) = editor.project_path(cx)
+ {
+ // Check if there's already a `BufferDiagnosticsEditor` tab for this
+ // same path, and if so, focus on that one instead of creating a new
+ // one.
+ let existing_editor = workspace
+ .items_of_type::<BufferDiagnosticsEditor>(cx)
+ .find(|editor| editor.read(cx).project_path == project_path);
+
+ if let Some(editor) = existing_editor {
+ workspace.activate_item(&editor, true, true, window, cx);
+ } else {
+ let include_warnings = match cx.try_global::<IncludeWarnings>() {
+ Some(include_warnings) => include_warnings.0,
+ None => ProjectSettings::get_global(cx).diagnostics.include_warnings,
+ };
+
+ let item = cx.new(|cx| {
+ Self::new(
+ project_path,
+ workspace.project().clone(),
+ editor.read(cx).buffer().read(cx).as_singleton(),
+ include_warnings,
+ window,
+ cx,
+ )
+ });
+
+ workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
+ }
+ }
+ }
+
+ pub fn register(
+ workspace: &mut Workspace,
+ _window: Option<&mut Window>,
+ _: &mut Context<Workspace>,
+ ) {
+ workspace.register_action(Self::deploy);
+ }
+
+ fn update_all_diagnostics(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.update_all_excerpts(window, cx);
+ }
+
+ fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
+ let project = self.project.read(cx);
+
+ self.summary = project.diagnostic_summary_for_path(&self.project_path, cx);
+ }
+
+ /// Enqueue an update to the excerpts and diagnostic blocks being shown in
+ /// the editor.
+ pub(crate) fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ // If there's already a task updating the excerpts, early return and let
+ // the other task finish.
+ if self.update_excerpts_task.is_some() {
+ return;
+ }
+
+ let buffer = self.buffer.clone();
+
+ self.update_excerpts_task = Some(cx.spawn_in(window, async move |editor, cx| {
+ cx.background_executor()
+ .timer(DIAGNOSTICS_UPDATE_DELAY)
+ .await;
+
+ if let Some(buffer) = buffer {
+ editor
+ .update_in(cx, |editor, window, cx| {
+ editor.update_excerpts(buffer, window, cx)
+ })?
+ .await?;
+ };
+
+ let _ = editor.update(cx, |editor, cx| {
+ editor.update_excerpts_task = None;
+ cx.notify();
+ });
+
+ Ok(())
+ }));
+ }
+
+ /// Updates the excerpts in the `BufferDiagnosticsEditor` for a single
+ /// buffer.
+ fn update_excerpts(
+ &mut self,
+ buffer: Entity<Buffer>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ let was_empty = self.multibuffer.read(cx).is_empty();
+ let multibuffer_context = multibuffer_context_lines(cx);
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ let buffer_snapshot_max = buffer_snapshot.max_point();
+ let max_severity = Self::max_diagnostics_severity(self.include_warnings)
+ .into_lsp()
+ .unwrap_or(lsp::DiagnosticSeverity::WARNING);
+
+ cx.spawn_in(window, async move |buffer_diagnostics_editor, mut cx| {
+ // Fetch the diagnostics for the whole of the buffer
+ // (`Point::zero()..buffer_snapshot.max_point()`) so we can confirm
+ // if the diagnostics changed, if it didn't, early return as there's
+ // nothing to update.
+ let diagnostics = buffer_snapshot
+ .diagnostics_in_range::<_, Anchor>(Point::zero()..buffer_snapshot_max, false)
+ .collect::<Vec<_>>();
+
+ let unchanged =
+ buffer_diagnostics_editor.update(cx, |buffer_diagnostics_editor, _cx| {
+ if buffer_diagnostics_editor
+ .diagnostics_are_unchanged(&diagnostics, &buffer_snapshot)
+ {
+ return true;
+ }
+
+ buffer_diagnostics_editor.set_diagnostics(&diagnostics);
+ return false;
+ })?;
+
+ if unchanged {
+ return Ok(());
+ }
+
+ // Mapping between the Group ID and a vector of DiagnosticEntry.
+ let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
+ for entry in diagnostics {
+ grouped
+ .entry(entry.diagnostic.group_id)
+ .or_default()
+ .push(DiagnosticEntry {
+ range: entry.range.to_point(&buffer_snapshot),
+ diagnostic: entry.diagnostic,
+ })
+ }
+
+ let mut blocks: Vec<DiagnosticBlock> = Vec::new();
+ for (_, group) in grouped {
+ // If the minimum severity of the group is higher than the
+ // maximum severity, or it doesn't even have severity, skip this
+ // group.
+ if group
+ .iter()
+ .map(|d| d.diagnostic.severity)
+ .min()
+ .is_none_or(|severity| severity > max_severity)
+ {
+ continue;
+ }
+
+ let diagnostic_blocks = cx.update(|_window, cx| {
+ DiagnosticRenderer::diagnostic_blocks_for_group(
+ group,
+ buffer_snapshot.remote_id(),
+ Some(Arc::new(buffer_diagnostics_editor.clone())),
+ cx,
+ )
+ })?;
+
+ // For each of the diagnostic blocks to be displayed in the
+ // editor, figure out its index in the list of blocks.
+ //
+ // The following rules are used to determine the order:
+ // 1. Blocks with a lower start position should come first.
+ // 2. If two blocks have the same start position, the one with
+ // the higher end position should come first.
+ for diagnostic_block in diagnostic_blocks {
+ let index = blocks.partition_point(|probe| {
+ match probe
+ .initial_range
+ .start
+ .cmp(&diagnostic_block.initial_range.start)
+ {
+ Ordering::Less => true,
+ Ordering::Greater => false,
+ Ordering::Equal => {
+ probe.initial_range.end > diagnostic_block.initial_range.end
+ }
+ }
+ });
+
+ blocks.insert(index, diagnostic_block);
+ }
+ }
+
+ // Build the excerpt ranges for this specific buffer's diagnostics,
+ // so those excerpts can later be used to update the excerpts shown
+ // in the editor.
+ // This is done by iterating over the list of diagnostic blocks and
+ // determine what range does the diagnostic block span.
+ let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
+
+ for diagnostic_block in blocks.iter() {
+ let excerpt_range = context_range_for_entry(
+ diagnostic_block.initial_range.clone(),
+ multibuffer_context,
+ buffer_snapshot.clone(),
+ &mut cx,
+ )
+ .await;
+
+ let index = excerpt_ranges
+ .binary_search_by(|probe| {
+ probe
+ .context
+ .start
+ .cmp(&excerpt_range.start)
+ .then(probe.context.end.cmp(&excerpt_range.end))
+ .then(
+ probe
+ .primary
+ .start
+ .cmp(&diagnostic_block.initial_range.start),
+ )
+ .then(probe.primary.end.cmp(&diagnostic_block.initial_range.end))
+ .then(Ordering::Greater)
+ })
+ .unwrap_or_else(|index| index);
+
+ excerpt_ranges.insert(
+ index,
+ ExcerptRange {
+ context: excerpt_range,
+ primary: diagnostic_block.initial_range.clone(),
+ },
+ )
+ }
+
+ // Finally, update the editor's content with the new excerpt ranges
+ // for this editor, as well as the diagnostic blocks.
+ buffer_diagnostics_editor.update_in(cx, |buffer_diagnostics_editor, window, cx| {
+ // Remove the list of `CustomBlockId` from the editor's display
+ // map, ensuring that if any diagnostics have been solved, the
+ // associated block stops being shown.
+ let block_ids = buffer_diagnostics_editor.blocks.clone();
+
+ buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
+ editor.display_map.update(cx, |display_map, cx| {
+ display_map.remove_blocks(block_ids.into_iter().collect(), cx);
+ })
+ });
+
+ let (anchor_ranges, _) =
+ buffer_diagnostics_editor
+ .multibuffer
+ .update(cx, |multibuffer, cx| {
+ multibuffer.set_excerpt_ranges_for_path(
+ PathKey::for_buffer(&buffer, cx),
+ buffer.clone(),
+ &buffer_snapshot,
+ excerpt_ranges,
+ cx,
+ )
+ });
+
+ if was_empty {
+ if let Some(anchor_range) = anchor_ranges.first() {
+ let range_to_select = anchor_range.start..anchor_range.start;
+
+ buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
+ editor.change_selections(Default::default(), window, cx, |selection| {
+ selection.select_anchor_ranges([range_to_select])
+ })
+ });
+
+ // If the `BufferDiagnosticsEditor` is currently
+ // focused, move focus to its editor.
+ if buffer_diagnostics_editor.focus_handle.is_focused(window) {
+ buffer_diagnostics_editor
+ .editor
+ .read(cx)
+ .focus_handle(cx)
+ .focus(window);
+ }
+ }
+ }
+
+ // Cloning the blocks before moving ownership so these can later
+ // be used to set the block contents for testing purposes.
+ #[cfg(test)]
+ let cloned_blocks = blocks.clone();
+
+ // Build new diagnostic blocks to be added to the editor's
+ // display map for the new diagnostics. Update the `blocks`
+ // property before finishing, to ensure the blocks are removed
+ // on the next execution.
+ let editor_blocks =
+ anchor_ranges
+ .into_iter()
+ .zip(blocks.into_iter())
+ .map(|(anchor, block)| {
+ let editor = buffer_diagnostics_editor.editor.downgrade();
+
+ BlockProperties {
+ placement: BlockPlacement::Near(anchor.start),
+ height: Some(1),
+ style: BlockStyle::Flex,
+ render: Arc::new(move |block_context| {
+ block.render_block(editor.clone(), block_context)
+ }),
+ priority: 1,
+ }
+ });
+
+ let block_ids = buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
+ editor.display_map.update(cx, |display_map, cx| {
+ display_map.insert_blocks(editor_blocks, cx)
+ })
+ });
+
+ // In order to be able to verify which diagnostic blocks are
+ // rendered in the editor, the `set_block_content_for_tests`
+ // function must be used, so that the
+ // `editor::test::editor_content_with_blocks` function can then
+ // be called to fetch these blocks.
+ #[cfg(test)]
+ {
+ for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
+ let markdown = block.markdown.clone();
+ editor::test::set_block_content_for_tests(
+ &buffer_diagnostics_editor.editor,
+ *block_id,
+ cx,
+ move |cx| {
+ markdown::MarkdownElement::rendered_text(
+ markdown.clone(),
+ cx,
+ editor::hover_popover::diagnostics_markdown_style,
+ )
+ },
+ );
+ }
+ }
+
+ buffer_diagnostics_editor.blocks = block_ids;
+ cx.notify()
+ })
+ })
+ }
+
+ fn set_diagnostics(&mut self, diagnostics: &Vec<DiagnosticEntry<Anchor>>) {
+ self.diagnostics = diagnostics.clone();
+ }
+
+ fn diagnostics_are_unchanged(
+ &self,
+ diagnostics: &Vec<DiagnosticEntry<Anchor>>,
+ snapshot: &BufferSnapshot,
+ ) -> bool {
+ if self.diagnostics.len() != diagnostics.len() {
+ return false;
+ }
+
+ self.diagnostics
+ .iter()
+ .zip(diagnostics.iter())
+ .all(|(existing, new)| {
+ existing.diagnostic.message == new.diagnostic.message
+ && existing.diagnostic.severity == new.diagnostic.severity
+ && existing.diagnostic.is_primary == new.diagnostic.is_primary
+ && existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
+ })
+ }
+
+ fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ // If the `BufferDiagnosticsEditor` is focused and the multibuffer is
+ // not empty, focus on the editor instead, which will allow the user to
+ // start interacting and editing the buffer's contents.
+ if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
+ self.editor.focus_handle(cx).focus(window)
+ }
+ }
+
+ fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
+ {
+ self.update_all_excerpts(window, cx);
+ }
+ }
+
+ pub fn toggle_warnings(
+ &mut self,
+ _: &ToggleWarnings,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let include_warnings = !self.include_warnings;
+ let max_severity = Self::max_diagnostics_severity(include_warnings);
+
+ self.editor.update(cx, |editor, cx| {
+ editor.set_max_diagnostics_severity(max_severity, cx);
+ });
+
+ self.include_warnings = include_warnings;
+ self.diagnostics.clear();
+ self.update_all_diagnostics(window, cx);
+ }
+
+ fn max_diagnostics_severity(include_warnings: bool) -> DiagnosticSeverity {
+ match include_warnings {
+ true => DiagnosticSeverity::Warning,
+ false => DiagnosticSeverity::Error,
+ }
+ }
+
+ #[cfg(test)]
+ pub fn editor(&self) -> &Entity<Editor> {
+ &self.editor
+ }
+
+ #[cfg(test)]
+ pub fn summary(&self) -> &DiagnosticSummary {
+ &self.summary
+ }
+}
+
+impl Focusable for BufferDiagnosticsEditor {
+ fn focus_handle(&self, _: &App) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl EventEmitter<EditorEvent> for BufferDiagnosticsEditor {}
+
+impl Item for BufferDiagnosticsEditor {
+ type Event = EditorEvent;
+
+ fn act_as_type<'a>(
+ &'a self,
+ type_id: std::any::TypeId,
+ self_handle: &'a Entity<Self>,
+ _: &'a App,
+ ) -> Option<gpui::AnyView> {
+ if type_id == TypeId::of::<Self>() {
+ Some(self_handle.to_any())
+ } else if type_id == TypeId::of::<Editor>() {
+ Some(self.editor.to_any())
+ } else {
+ None
+ }
+ }
+
+ fn added_to_workspace(
+ &mut self,
+ workspace: &mut Workspace,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.editor.update(cx, |editor, cx| {
+ editor.added_to_workspace(workspace, window, cx)
+ });
+ }
+
+ fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
+ ToolbarItemLocation::PrimaryLeft
+ }
+
+ fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
+ self.editor.breadcrumbs(theme, cx)
+ }
+
+ fn can_save(&self, _cx: &App) -> bool {
+ true
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: Option<workspace::WorkspaceId>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<Entity<Self>>
+ where
+ Self: Sized,
+ {
+ Some(cx.new(|cx| {
+ BufferDiagnosticsEditor::new(
+ self.project_path.clone(),
+ self.project.clone(),
+ self.buffer.clone(),
+ self.include_warnings,
+ window,
+ cx,
+ )
+ }))
+ }
+
+ fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.editor
+ .update(cx, |editor, cx| editor.deactivated(window, cx));
+ }
+
+ fn for_each_project_item(&self, cx: &App, f: &mut dyn FnMut(EntityId, &dyn ProjectItem)) {
+ self.editor.for_each_project_item(cx, f);
+ }
+
+ fn has_conflict(&self, cx: &App) -> bool {
+ self.multibuffer.read(cx).has_conflict(cx)
+ }
+
+ fn has_deleted_file(&self, cx: &App) -> bool {
+ self.multibuffer.read(cx).has_deleted_file(cx)
+ }
+
+ fn is_dirty(&self, cx: &App) -> bool {
+ self.multibuffer.read(cx).is_dirty(cx)
+ }
+
+ fn is_singleton(&self, _cx: &App) -> bool {
+ false
+ }
+
+ fn navigate(
+ &mut self,
+ data: Box<dyn Any>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> bool {
+ self.editor
+ .update(cx, |editor, cx| editor.navigate(data, window, cx))
+ }
+
+ fn reload(
+ &mut self,
+ project: Entity<Project>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ self.editor.reload(project, window, cx)
+ }
+
+ fn save(
+ &mut self,
+ options: workspace::item::SaveOptions,
+ project: Entity<Project>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ self.editor.save(options, project, window, cx)
+ }
+
+ fn save_as(
+ &mut self,
+ _project: Entity<Project>,
+ _path: ProjectPath,
+ _window: &mut Window,
+ _cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ unreachable!()
+ }
+
+ fn set_nav_history(
+ &mut self,
+ nav_history: ItemNavHistory,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.editor.update(cx, |editor, _| {
+ editor.set_nav_history(Some(nav_history));
+ })
+ }
+
+ // Builds the content to be displayed in the tab.
+ fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
+ let error_count = self.summary.error_count;
+ let warning_count = self.summary.warning_count;
+ let label = Label::new(
+ self.project_path
+ .path
+ .file_name()
+ .map(|f| f.to_sanitized_string())
+ .unwrap_or_else(|| self.project_path.path.to_sanitized_string()),
+ );
+
+ h_flex()
+ .gap_1()
+ .child(label)
+ .when(error_count == 0 && warning_count == 0, |parent| {
+ parent.child(
+ h_flex()
+ .gap_1()
+ .child(Icon::new(IconName::Check).color(Color::Success)),
+ )
+ })
+ .when(error_count > 0, |parent| {
+ parent.child(
+ h_flex()
+ .gap_1()
+ .child(Icon::new(IconName::XCircle).color(Color::Error))
+ .child(Label::new(error_count.to_string()).color(params.text_color())),
+ )
+ })
+ .when(warning_count > 0, |parent| {
+ parent.child(
+ h_flex()
+ .gap_1()
+ .child(Icon::new(IconName::Warning).color(Color::Warning))
+ .child(Label::new(warning_count.to_string()).color(params.text_color())),
+ )
+ })
+ .into_any_element()
+ }
+
+ fn tab_content_text(&self, _detail: usize, _app: &App) -> SharedString {
+ "Buffer Diagnostics".into()
+ }
+
+ fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
+ Some(
+ format!(
+ "Buffer Diagnostics - {}",
+ self.project_path.path.to_sanitized_string()
+ )
+ .into(),
+ )
+ }
+
+ fn telemetry_event_text(&self) -> Option<&'static str> {
+ Some("Buffer Diagnostics Opened")
+ }
+
+ fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
+ Editor::to_item_events(event, f)
+ }
+}
+
+impl Render for BufferDiagnosticsEditor {
+ fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let filename = self.project_path.path.to_sanitized_string();
+ let error_count = self.summary.error_count;
+ let warning_count = match self.include_warnings {
+ true => self.summary.warning_count,
+ false => 0,
+ };
+
+ let child = if error_count + warning_count == 0 {
+ let label = match warning_count {
+ 0 => "No problems in",
+ _ => "No errors in",
+ };
+
+ v_flex()
+ .key_context("EmptyPane")
+ .size_full()
+ .gap_1()
+ .justify_center()
+ .items_center()
+ .text_center()
+ .bg(cx.theme().colors().editor_background)
+ .child(
+ div()
+ .h_flex()
+ .child(Label::new(label).color(Color::Muted))
+ .child(
+ Button::new("open-file", filename)
+ .style(ButtonStyle::Transparent)
+ .tooltip(Tooltip::text("Open File"))
+ .on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
+ if let Some(workspace) = window.root::<Workspace>().flatten() {
+ workspace.update(cx, |workspace, cx| {
+ workspace
+ .open_path(
+ buffer_diagnostics.project_path.clone(),
+ None,
+ true,
+ window,
+ cx,
+ )
+ .detach_and_log_err(cx);
+ })
+ }
+ })),
+ ),
+ )
+ .when(self.summary.warning_count > 0, |div| {
+ let label = match self.summary.warning_count {
+ 1 => "Show 1 warning".into(),
+ warning_count => format!("Show {} warnings", warning_count),
+ };
+
+ div.child(
+ Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
+ |buffer_diagnostics_editor, _, window, cx| {
+ buffer_diagnostics_editor.toggle_warnings(
+ &Default::default(),
+ window,
+ cx,
+ );
+ cx.notify();
+ },
+ )),
+ )
+ })
+ } else {
+ div().size_full().child(self.editor.clone())
+ };
+
+ div()
+ .key_context("Diagnostics")
+ .track_focus(&self.focus_handle(cx))
+ .size_full()
+ .child(child)
+ }
+}
+
+impl DiagnosticsToolbarEditor for WeakEntity<BufferDiagnosticsEditor> {
+ fn include_warnings(&self, cx: &App) -> bool {
+ self.read_with(cx, |buffer_diagnostics_editor, _cx| {
+ buffer_diagnostics_editor.include_warnings
+ })
+ .unwrap_or(false)
+ }
+
+ fn has_stale_excerpts(&self, _cx: &App) -> bool {
+ false
+ }
+
+ fn is_updating(&self, cx: &App) -> bool {
+ self.read_with(cx, |buffer_diagnostics_editor, cx| {
+ buffer_diagnostics_editor.update_excerpts_task.is_some()
+ || buffer_diagnostics_editor
+ .project
+ .read(cx)
+ .language_servers_running_disk_based_diagnostics(cx)
+ .next()
+ .is_some()
+ })
+ .unwrap_or(false)
+ }
+
+ fn stop_updating(&self, cx: &mut App) {
+ let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
+ buffer_diagnostics_editor.update_excerpts_task = None;
+ cx.notify();
+ });
+ }
+
+ fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
+ let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
+ buffer_diagnostics_editor.update_all_excerpts(window, cx);
+ });
+ }
+
+ fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
+ let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
+ buffer_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
+ });
+ }
+
+ fn get_diagnostics_for_buffer(
+ &self,
+ _buffer_id: text::BufferId,
+ cx: &App,
+ ) -> Vec<language::DiagnosticEntry<text::Anchor>> {
+ self.read_with(cx, |buffer_diagnostics_editor, _cx| {
+ buffer_diagnostics_editor.diagnostics.clone()
+ })
+ .unwrap_or_default()
+ }
+}
@@ -18,7 +18,7 @@ use ui::{
};
use util::maybe;
-use crate::ProjectDiagnosticsEditor;
+use crate::toolbar_controls::DiagnosticsToolbarEditor;
pub struct DiagnosticRenderer;
@@ -26,7 +26,7 @@ impl DiagnosticRenderer {
pub fn diagnostic_blocks_for_group(
diagnostic_group: Vec<DiagnosticEntry<Point>>,
buffer_id: BufferId,
- diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
+ diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
cx: &mut App,
) -> Vec<DiagnosticBlock> {
let Some(primary_ix) = diagnostic_group
@@ -46,7 +46,7 @@ impl DiagnosticRenderer {
markdown.push_str(" (");
}
if let Some(source) = diagnostic.source.as_ref() {
- markdown.push_str(&Markdown::escape(&source));
+ markdown.push_str(&Markdown::escape(source));
}
if diagnostic.source.is_some() && diagnostic.code.is_some() {
markdown.push(' ');
@@ -130,6 +130,7 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
cx: &mut App,
) -> Vec<BlockProperties<Anchor>> {
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
+
blocks
.into_iter()
.map(|block| {
@@ -182,7 +183,7 @@ pub(crate) struct DiagnosticBlock {
pub(crate) initial_range: Range<Point>,
pub(crate) severity: DiagnosticSeverity,
pub(crate) markdown: Entity<Markdown>,
- pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
+ pub(crate) diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
}
impl DiagnosticBlock {
@@ -233,7 +234,7 @@ impl DiagnosticBlock {
pub fn open_link(
editor: &mut Editor,
- diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
+ diagnostics_editor: &Option<Arc<dyn DiagnosticsToolbarEditor>>,
link: SharedString,
window: &mut Window,
cx: &mut Context<Editor>,
@@ -254,18 +255,10 @@ impl DiagnosticBlock {
if let Some(diagnostics_editor) = diagnostics_editor {
if let Some(diagnostic) = diagnostics_editor
- .read_with(cx, |diagnostics, _| {
- diagnostics
- .diagnostics
- .get(&buffer_id)
- .cloned()
- .unwrap_or_default()
- .into_iter()
- .filter(|d| d.diagnostic.group_id == group_id)
- .nth(ix)
- })
- .ok()
- .flatten()
+ .get_diagnostics_for_buffer(buffer_id, cx)
+ .into_iter()
+ .filter(|d| d.diagnostic.group_id == group_id)
+ .nth(ix)
{
let multibuffer = editor.buffer().read(cx);
let Some(snapshot) = multibuffer
@@ -287,26 +280,24 @@ impl DiagnosticBlock {
}
}
}
- } else {
- if let Some(diagnostic) = editor
- .snapshot(window, cx)
- .buffer_snapshot
- .diagnostic_group(buffer_id, group_id)
- .nth(ix)
- {
- Self::jump_to(editor, diagnostic.range, window, cx)
- }
+ } else if let Some(diagnostic) = editor
+ .snapshot(window, cx)
+ .buffer_snapshot
+ .diagnostic_group(buffer_id, group_id)
+ .nth(ix)
+ {
+ Self::jump_to(editor, diagnostic.range, window, cx)
};
}
- fn jump_to<T: ToOffset>(
+ fn jump_to<I: ToOffset>(
editor: &mut Editor,
- range: Range<T>,
+ range: Range<I>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
let snapshot = &editor.buffer().read(cx).snapshot(cx);
- let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
+ let range = range.start.to_offset(snapshot)..range.end.to_offset(snapshot);
editor.unfold_ranges(&[range.start..range.end], true, false, cx);
editor.change_selections(Default::default(), window, cx, |s| {
@@ -1,19 +1,21 @@
pub mod items;
mod toolbar_controls;
+mod buffer_diagnostics;
mod diagnostic_renderer;
#[cfg(test)]
mod diagnostics_tests;
use anyhow::Result;
+use buffer_diagnostics::BufferDiagnosticsEditor;
use collections::{BTreeSet, HashMap};
use diagnostic_renderer::DiagnosticBlock;
use editor::{
- DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
+ Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
+ multibuffer_context_lines,
};
-use futures::future::join_all;
use gpui::{
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
@@ -24,7 +26,6 @@ use language::{
};
use project::{
DiagnosticSummary, Project, ProjectPath,
- lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck},
project_settings::{DiagnosticSeverity, ProjectSettings},
};
use settings::Settings;
@@ -37,6 +38,7 @@ use std::{
};
use text::{BufferId, OffsetRangeExt};
use theme::ActiveTheme;
+use toolbar_controls::DiagnosticsToolbarEditor;
pub use toolbar_controls::ToolbarControls;
use ui::{Icon, IconName, Label, h_flex, prelude::*};
use util::ResultExt;
@@ -65,6 +67,7 @@ impl Global for IncludeWarnings {}
pub fn init(cx: &mut App) {
editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx);
cx.observe_new(ProjectDiagnosticsEditor::register).detach();
+ cx.observe_new(BufferDiagnosticsEditor::register).detach();
}
pub(crate) struct ProjectDiagnosticsEditor {
@@ -79,20 +82,14 @@ pub(crate) struct ProjectDiagnosticsEditor {
paths_to_update: BTreeSet<ProjectPath>,
include_warnings: bool,
update_excerpts_task: Option<Task<Result<()>>>,
- cargo_diagnostics_fetch: CargoDiagnosticsFetchState,
diagnostic_summary_update: Task<()>,
_subscription: Subscription,
}
-struct CargoDiagnosticsFetchState {
- fetch_task: Option<Task<()>>,
- cancel_task: Option<Task<()>>,
- diagnostic_sources: Arc<Vec<ProjectPath>>,
-}
-
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
+const DIAGNOSTICS_SUMMARY_UPDATE_DELAY: Duration = Duration::from_millis(30);
impl Render for ProjectDiagnosticsEditor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -102,43 +99,44 @@ impl Render for ProjectDiagnosticsEditor {
0
};
- let child = if warning_count + self.summary.error_count == 0 {
- let label = if self.summary.warning_count == 0 {
- SharedString::new_static("No problems in workspace")
+ let child =
+ if warning_count + self.summary.error_count == 0 && self.editor.read(cx).is_empty(cx) {
+ let label = if self.summary.warning_count == 0 {
+ SharedString::new_static("No problems in workspace")
+ } else {
+ SharedString::new_static("No errors in workspace")
+ };
+ v_flex()
+ .key_context("EmptyPane")
+ .size_full()
+ .gap_1()
+ .justify_center()
+ .items_center()
+ .text_center()
+ .bg(cx.theme().colors().editor_background)
+ .child(Label::new(label).color(Color::Muted))
+ .when(self.summary.warning_count > 0, |this| {
+ let plural_suffix = if self.summary.warning_count > 1 {
+ "s"
+ } else {
+ ""
+ };
+ let label = format!(
+ "Show {} warning{}",
+ self.summary.warning_count, plural_suffix
+ );
+ this.child(
+ Button::new("diagnostics-show-warning-label", label).on_click(
+ cx.listener(|this, _, window, cx| {
+ this.toggle_warnings(&Default::default(), window, cx);
+ cx.notify();
+ }),
+ ),
+ )
+ })
} else {
- SharedString::new_static("No errors in workspace")
+ div().size_full().child(self.editor.clone())
};
- v_flex()
- .key_context("EmptyPane")
- .size_full()
- .gap_1()
- .justify_center()
- .items_center()
- .text_center()
- .bg(cx.theme().colors().editor_background)
- .child(Label::new(label).color(Color::Muted))
- .when(self.summary.warning_count > 0, |this| {
- let plural_suffix = if self.summary.warning_count > 1 {
- "s"
- } else {
- ""
- };
- let label = format!(
- "Show {} warning{}",
- self.summary.warning_count, plural_suffix
- );
- this.child(
- Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
- |this, _, window, cx| {
- this.toggle_warnings(&Default::default(), window, cx);
- cx.notify();
- },
- )),
- )
- })
- } else {
- div().size_full().child(self.editor.clone())
- };
div()
.key_context("Diagnostics")
@@ -151,7 +149,7 @@ impl Render for ProjectDiagnosticsEditor {
}
impl ProjectDiagnosticsEditor {
- fn register(
+ pub fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_: &mut Context<Workspace>,
@@ -167,7 +165,7 @@ impl ProjectDiagnosticsEditor {
cx: &mut Context<Self>,
) -> Self {
let project_event_subscription =
- cx.subscribe_in(&project_handle, window, |this, project, event, window, cx| match event {
+ cx.subscribe_in(&project_handle, window, |this, _project, event, window, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
}
@@ -180,13 +178,12 @@ impl ProjectDiagnosticsEditor {
paths,
} => {
this.paths_to_update.extend(paths.clone());
- let project = project.clone();
this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
cx.background_executor()
- .timer(Duration::from_millis(30))
+ .timer(DIAGNOSTICS_SUMMARY_UPDATE_DELAY)
.await;
this.update(cx, |this, cx| {
- this.summary = project.read(cx).diagnostic_summary(false, cx);
+ this.update_diagnostic_summary(cx);
})
.log_err();
});
@@ -241,6 +238,7 @@ impl ProjectDiagnosticsEditor {
}
}
EditorEvent::Blurred => this.update_stale_excerpts(window, cx),
+ EditorEvent::Saved => this.update_stale_excerpts(window, cx),
_ => {}
}
},
@@ -260,11 +258,7 @@ impl ProjectDiagnosticsEditor {
)
});
this.diagnostics.clear();
- this.update_all_diagnostics(false, window, cx);
- })
- .detach();
- cx.observe_release(&cx.entity(), |editor, _, cx| {
- editor.stop_cargo_diagnostics_fetch(cx);
+ this.update_all_excerpts(window, cx);
})
.detach();
@@ -281,20 +275,15 @@ impl ProjectDiagnosticsEditor {
editor,
paths_to_update: Default::default(),
update_excerpts_task: None,
- cargo_diagnostics_fetch: CargoDiagnosticsFetchState {
- fetch_task: None,
- cancel_task: None,
- diagnostic_sources: Arc::new(Vec::new()),
- },
diagnostic_summary_update: Task::ready(()),
_subscription: project_event_subscription,
};
- this.update_all_diagnostics(true, window, cx);
+ this.update_all_excerpts(window, cx);
this
}
fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- if self.update_excerpts_task.is_some() {
+ if self.update_excerpts_task.is_some() || self.multibuffer.read(cx).is_dirty(cx) {
return;
}
@@ -341,6 +330,7 @@ impl ProjectDiagnosticsEditor {
let is_active = workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == existing.item_id());
+
workspace.activate_item(&existing, true, !is_active, window, cx);
} else {
let workspace_handle = cx.entity().downgrade();
@@ -373,22 +363,10 @@ impl ProjectDiagnosticsEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
- .diagnostics
- .fetch_cargo_diagnostics();
-
- if fetch_cargo_diagnostics {
- if self.cargo_diagnostics_fetch.fetch_task.is_some() {
- self.stop_cargo_diagnostics_fetch(cx);
- } else {
- self.update_all_diagnostics(false, window, cx);
- }
+ if self.update_excerpts_task.is_some() {
+ self.update_excerpts_task = None;
} else {
- if self.update_excerpts_task.is_some() {
- self.update_excerpts_task = None;
- } else {
- self.update_all_diagnostics(false, window, cx);
- }
+ self.update_all_excerpts(window, cx);
}
cx.notify();
}
@@ -406,93 +384,29 @@ impl ProjectDiagnosticsEditor {
}
}
- fn update_all_diagnostics(
- &mut self,
- first_launch: bool,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx);
- if cargo_diagnostics_sources.is_empty() {
- self.update_all_excerpts(window, cx);
- } else if first_launch && !self.summary.is_empty() {
- self.update_all_excerpts(window, cx);
- } else {
- self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx);
- }
- }
-
- fn fetch_cargo_diagnostics(
- &mut self,
- diagnostics_sources: Arc<Vec<ProjectPath>>,
- cx: &mut Context<Self>,
- ) {
- let project = self.project.clone();
- self.cargo_diagnostics_fetch.cancel_task = None;
- self.cargo_diagnostics_fetch.fetch_task = None;
- self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone();
- if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() {
- return;
- }
-
- self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| {
- let mut fetch_tasks = Vec::new();
- for buffer_path in diagnostics_sources.iter().cloned() {
- if cx
- .update(|cx| {
- fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx));
- })
- .is_err()
- {
- break;
- }
- }
-
- let _ = join_all(fetch_tasks).await;
- editor
- .update(cx, |editor, _| {
- editor.cargo_diagnostics_fetch.fetch_task = None;
- })
- .ok();
- }));
- }
-
- fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) {
- self.cargo_diagnostics_fetch.fetch_task = None;
- let mut cancel_gasks = Vec::new();
- for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources)
- .iter()
- .cloned()
- {
- cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx));
- }
-
- self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move {
- let _ = join_all(cancel_gasks).await;
- log::info!("Finished fetching cargo diagnostics");
- }));
- }
-
/// Enqueue an update of all excerpts. Updates all paths that either
/// currently have diagnostics or are currently present in this view.
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.project.update(cx, |project, cx| {
- let mut paths = project
+ let mut project_paths = project
.diagnostic_summaries(false, cx)
- .map(|(path, _, _)| path)
+ .map(|(project_path, _, _)| project_path)
.collect::<BTreeSet<_>>();
+
self.multibuffer.update(cx, |multibuffer, cx| {
for buffer in multibuffer.all_buffers() {
if let Some(file) = buffer.read(cx).file() {
- paths.insert(ProjectPath {
+ project_paths.insert(ProjectPath {
path: file.path().clone(),
worktree_id: file.worktree_id(cx),
});
}
}
});
- self.paths_to_update = paths;
+
+ self.paths_to_update = project_paths;
});
+
self.update_stale_excerpts(window, cx);
}
@@ -522,19 +436,21 @@ impl ProjectDiagnosticsEditor {
let was_empty = self.multibuffer.read(cx).is_empty();
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer_snapshot.remote_id();
+
let max_severity = if self.include_warnings {
lsp::DiagnosticSeverity::WARNING
} else {
lsp::DiagnosticSeverity::ERROR
};
- cx.spawn_in(window, async move |this, mut cx| {
+ cx.spawn_in(window, async move |this, cx| {
let diagnostics = buffer_snapshot
.diagnostics_in_range::<_, text::Anchor>(
Point::zero()..buffer_snapshot.max_point(),
false,
)
.collect::<Vec<_>>();
+
let unchanged = this.update(cx, |this, _| {
if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot)
@@ -542,7 +458,7 @@ impl ProjectDiagnosticsEditor {
return true;
}
this.diagnostics.insert(buffer_id, diagnostics.clone());
- return false;
+ false
})?;
if unchanged {
return Ok(());
@@ -569,7 +485,7 @@ impl ProjectDiagnosticsEditor {
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
group,
buffer_snapshot.remote_id(),
- Some(this.clone()),
+ Some(Arc::new(this.clone())),
cx,
)
})?;
@@ -590,14 +506,16 @@ impl ProjectDiagnosticsEditor {
}
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
+ let context_lines = cx.update(|_, cx| multibuffer_context_lines(cx))?;
for b in blocks.iter() {
let excerpt_range = context_range_for_entry(
b.initial_range.clone(),
- DEFAULT_MULTIBUFFER_CONTEXT,
+ context_lines,
buffer_snapshot.clone(),
- &mut cx,
+ cx,
)
.await;
+
let i = excerpt_ranges
.binary_search_by(|probe| {
probe
@@ -639,17 +557,15 @@ impl ProjectDiagnosticsEditor {
#[cfg(test)]
let cloned_blocks = blocks.clone();
- if was_empty {
- if let Some(anchor_range) = anchor_ranges.first() {
- let range_to_select = anchor_range.start..anchor_range.start;
- this.editor.update(cx, |editor, cx| {
- editor.change_selections(Default::default(), window, cx, |s| {
- s.select_anchor_ranges([range_to_select]);
- })
- });
- if this.focus_handle.is_focused(window) {
- this.editor.read(cx).focus_handle(cx).focus(window);
- }
+ if was_empty && let Some(anchor_range) = anchor_ranges.first() {
+ let range_to_select = anchor_range.start..anchor_range.start;
+ this.editor.update(cx, |editor, cx| {
+ editor.change_selections(Default::default(), window, cx, |s| {
+ s.select_anchor_ranges([range_to_select]);
+ })
+ });
+ if this.focus_handle.is_focused(window) {
+ this.editor.read(cx).focus_handle(cx).focus(window);
}
}
@@ -669,6 +585,7 @@ impl ProjectDiagnosticsEditor {
priority: 1,
}
});
+
let block_ids = this.editor.update(cx, |editor, cx| {
editor.display_map.update(cx, |display_map, cx| {
display_map.insert_blocks(editor_blocks, cx)
@@ -700,28 +617,8 @@ impl ProjectDiagnosticsEditor {
})
}
- pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec<ProjectPath> {
- let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
- .diagnostics
- .fetch_cargo_diagnostics();
- if !fetch_cargo_diagnostics {
- return Vec::new();
- }
- self.project
- .read(cx)
- .worktrees(cx)
- .filter_map(|worktree| {
- let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?;
- let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| {
- entry
- .path
- .extension()
- .and_then(|extension| extension.to_str())
- == Some("rs")
- })?;
- self.project.read(cx).path_for_entry(rust_file_entry.id, cx)
- })
- .collect()
+ fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
+ self.summary = self.project.read(cx).diagnostic_summary(false, cx);
}
}
@@ -931,6 +828,68 @@ impl Item for ProjectDiagnosticsEditor {
}
}
+impl DiagnosticsToolbarEditor for WeakEntity<ProjectDiagnosticsEditor> {
+ fn include_warnings(&self, cx: &App) -> bool {
+ self.read_with(cx, |project_diagnostics_editor, _cx| {
+ project_diagnostics_editor.include_warnings
+ })
+ .unwrap_or(false)
+ }
+
+ fn has_stale_excerpts(&self, cx: &App) -> bool {
+ self.read_with(cx, |project_diagnostics_editor, _cx| {
+ !project_diagnostics_editor.paths_to_update.is_empty()
+ })
+ .unwrap_or(false)
+ }
+
+ fn is_updating(&self, cx: &App) -> bool {
+ self.read_with(cx, |project_diagnostics_editor, cx| {
+ project_diagnostics_editor.update_excerpts_task.is_some()
+ || project_diagnostics_editor
+ .project
+ .read(cx)
+ .language_servers_running_disk_based_diagnostics(cx)
+ .next()
+ .is_some()
+ })
+ .unwrap_or(false)
+ }
+
+ fn stop_updating(&self, cx: &mut App) {
+ let _ = self.update(cx, |project_diagnostics_editor, cx| {
+ project_diagnostics_editor.update_excerpts_task = None;
+ cx.notify();
+ });
+ }
+
+ fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
+ let _ = self.update(cx, |project_diagnostics_editor, cx| {
+ project_diagnostics_editor.update_all_excerpts(window, cx);
+ });
+ }
+
+ fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
+ let _ = self.update(cx, |project_diagnostics_editor, cx| {
+ project_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
+ });
+ }
+
+ fn get_diagnostics_for_buffer(
+ &self,
+ buffer_id: text::BufferId,
+ cx: &App,
+ ) -> Vec<language::DiagnosticEntry<text::Anchor>> {
+ self.read_with(cx, |project_diagnostics_editor, _cx| {
+ project_diagnostics_editor
+ .diagnostics
+ .get(&buffer_id)
+ .cloned()
+ .unwrap_or_default()
+ })
+ .unwrap_or_default()
+ }
+}
const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
async fn context_range_for_entry(
@@ -980,18 +939,16 @@ async fn heuristic_syntactic_expand(
// Remove blank lines from start and end
if let Some(start_row) = (outline_range.start.row..outline_range.end.row)
.find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
- {
- if let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1)
+ && let Some(end_row) = (outline_range.start.row..outline_range.end.row + 1)
.rev()
.find(|row| !snapshot.line_indent_for_row(*row).is_line_blank())
- {
- let row_count = end_row.saturating_sub(start_row);
- if row_count <= max_row_count {
- return Some(RangeInclusive::new(
- outline_range.start.row,
- outline_range.end.row,
- ));
- }
+ {
+ let row_count = end_row.saturating_sub(start_row);
+ if row_count <= max_row_count {
+ return Some(RangeInclusive::new(
+ outline_range.start.row,
+ outline_range.end.row,
+ ));
}
}
}
@@ -24,6 +24,7 @@ use settings::SettingsStore;
use std::{
env,
path::{Path, PathBuf},
+ str::FromStr,
};
use unindent::Unindent as _;
use util::{RandomCharIter, path, post_inc};
@@ -70,7 +71,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
- let uri = lsp::Url::from_file_path(path!("/test/main.rs")).unwrap();
+ let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
// Create some diagnostics
lsp_store.update(cx, |lsp_store, cx| {
@@ -167,7 +168,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
.update_diagnostics(
language_server_id,
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/test/consts.rs")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 15),
@@ -243,7 +244,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
.update_diagnostics(
language_server_id,
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/test/consts.rs")).unwrap(),
diagnostics: vec![
lsp::Diagnostic {
range: lsp::Range::new(
@@ -356,14 +357,14 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_1,
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(4, 0), lsp::Position::new(4, 4)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "no method `tset`".to_string(),
related_information: Some(vec![lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(
- lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+ lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
lsp::Range::new(
lsp::Position::new(0, 9),
lsp::Position::new(0, 13),
@@ -465,7 +466,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_1,
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -509,7 +510,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_2,
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 1)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
@@ -552,7 +553,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_1,
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 1)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -571,7 +572,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_2,
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/test/main.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap(),
diagnostics: vec![],
version: None,
},
@@ -608,7 +609,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
.update_diagnostics(
server_id_2,
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/test/main.js")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 1)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -681,7 +682,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
Default::default();
for _ in 0..operations {
- match rng.gen_range(0..100) {
+ match rng.random_range(0..100) {
// language server completes its diagnostic check
0..=20 if !updated_language_servers.is_empty() => {
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
@@ -690,7 +691,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
lsp_store.disk_based_diagnostics_finished(server_id, cx)
});
- if rng.gen_bool(0.5) {
+ if rng.random_bool(0.5) {
cx.run_until_parked();
}
}
@@ -700,7 +701,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
let (path, server_id, diagnostics) =
match current_diagnostics.iter_mut().choose(&mut rng) {
// update existing set of diagnostics
- Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
+ Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => {
(path.clone(), *server_id, diagnostics)
}
@@ -708,13 +709,13 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
_ => {
let path: PathBuf =
format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
- let len = rng.gen_range(128..256);
+ let len = rng.random_range(128..256);
let content =
RandomCharIter::new(&mut rng).take(len).collect::<String>();
fs.insert_file(&path, content.into_bytes()).await;
let server_id = match language_server_ids.iter().choose(&mut rng) {
- Some(server_id) if rng.gen_bool(0.5) => *server_id,
+ Some(server_id) if rng.random_bool(0.5) => *server_id,
_ => {
let id = LanguageServerId(language_server_ids.len());
language_server_ids.push(id);
@@ -745,8 +746,8 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
.update_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
- lsp::Url::parse("file:///test/fallback.rs").unwrap()
+ uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| {
+ lsp::Uri::from_str("file:///test/fallback.rs").unwrap()
}),
diagnostics: diagnostics.clone(),
version: None,
@@ -845,7 +846,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
let mut next_inlay_id = 0;
for _ in 0..operations {
- match rng.gen_range(0..100) {
+ match rng.random_range(0..100) {
// language server completes its diagnostic check
0..=20 if !updated_language_servers.is_empty() => {
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
@@ -854,7 +855,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
lsp_store.disk_based_diagnostics_finished(server_id, cx)
});
- if rng.gen_bool(0.5) {
+ if rng.random_bool(0.5) {
cx.run_until_parked();
}
}
@@ -862,8 +863,8 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
diagnostics.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
- if snapshot.buffer_snapshot.len() > 0 {
- let position = rng.gen_range(0..snapshot.buffer_snapshot.len());
+ if !snapshot.buffer_snapshot.is_empty() {
+ let position = rng.random_range(0..snapshot.buffer_snapshot.len());
let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
log::info!(
"adding inlay at {position}/{}: {:?}",
@@ -889,7 +890,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
let (path, server_id, diagnostics) =
match current_diagnostics.iter_mut().choose(&mut rng) {
// update existing set of diagnostics
- Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
+ Some(((path, server_id), diagnostics)) if rng.random_bool(0.5) => {
(path.clone(), *server_id, diagnostics)
}
@@ -897,13 +898,13 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
_ => {
let path: PathBuf =
format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
- let len = rng.gen_range(128..256);
+ let len = rng.random_range(128..256);
let content =
RandomCharIter::new(&mut rng).take(len).collect::<String>();
fs.insert_file(&path, content.into_bytes()).await;
let server_id = match language_server_ids.iter().choose(&mut rng) {
- Some(server_id) if rng.gen_bool(0.5) => *server_id,
+ Some(server_id) if rng.random_bool(0.5) => *server_id,
_ => {
let id = LanguageServerId(language_server_ids.len());
language_server_ids.push(id);
@@ -934,8 +935,8 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
.update_diagnostics(
server_id,
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
- lsp::Url::parse("file:///test/fallback.rs").unwrap()
+ uri: lsp::Uri::from_file_path(&path).unwrap_or_else(|_| {
+ lsp::Uri::from_str("file:///test/fallback.rs").unwrap()
}),
diagnostics: diagnostics.clone(),
version: None,
@@ -971,7 +972,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
let mut cx = EditorTestContext::new(cx).await;
let lsp_store =
- cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+ cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.set_state(indoc! {"
ˇfn func(abc def: i32) -> u32 {
@@ -985,7 +986,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(
@@ -1028,7 +1029,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: Vec::new(),
},
@@ -1065,7 +1066,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx).await;
let lsp_store =
- cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+ cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.set_state(indoc! {"
ˇfn func(abc def: i32) -> u32 {
@@ -1078,7 +1079,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
@@ -1239,14 +1240,14 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
}
"});
let lsp_store =
- cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+ cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
@@ -1293,13 +1294,13 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext)
fn «test»() { println!(); }
"});
let lsp_store =
- cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+ cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.update(|_, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/root/dir/file.rs")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/root/dir/file.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
range,
@@ -1376,7 +1377,7 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let workspace = window.root(cx).unwrap();
- let uri = lsp::Url::from_file_path(path!("/root/main.js")).unwrap();
+ let uri = lsp::Uri::from_file_path(path!("/root/main.js")).unwrap();
// Create diagnostics with code fields
lsp_store.update(cx, |lsp_store, cx| {
@@ -1450,7 +1451,7 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx).await;
let lsp_store =
- cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
+ cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
cx.set_state(indoc! {"error warning info hiˇnt"});
@@ -1460,7 +1461,7 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
.update_diagnostics(
LanguageServerId(0),
lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
@@ -1566,6 +1567,440 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
}
+#[gpui::test]
+async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ // We'll be creating two different files, both with diagnostics, so we can
+ // later verify that, since the `BufferDiagnosticsEditor` only shows
+ // diagnostics for the provided path, the diagnostics for the other file
+ // will not be shown, contrary to what happens with
+ // `ProjectDiagnosticsEditor`.
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/test"),
+ json!({
+ "main.rs": "
+ fn main() {
+ let x = vec![];
+ let y = vec![];
+ a(x);
+ b(y);
+ c(y);
+ d(x);
+ }
+ "
+ .unindent(),
+ "other.rs": "
+ fn other() {
+ let unused = 42;
+ undefined_function();
+ }
+ "
+ .unindent(),
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
+ let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let cx = &mut VisualTestContext::from_window(*window, cx);
+ let project_path = project::ProjectPath {
+ worktree_id: project.read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ }),
+ path: Arc::from(Path::new("main.rs")),
+ };
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_buffer(project_path.clone(), cx)
+ })
+ .await
+ .ok();
+
+ // Create the diagnostics for `main.rs`.
+ let language_server_id = LanguageServerId(0);
+ let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
+ let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
+
+ lsp_store.update(cx, |lsp_store, cx| {
+ lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
+ uri: uri.clone(),
+ diagnostics: vec![
+ lsp::Diagnostic{
+ range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
+ severity: Some(lsp::DiagnosticSeverity::WARNING),
+ message: "use of moved value\nvalue used here after move".to_string(),
+ related_information: Some(vec![
+ lsp::DiagnosticRelatedInformation {
+ location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
+ message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
+ },
+ lsp::DiagnosticRelatedInformation {
+ location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
+ message: "value moved here".to_string()
+ },
+ ]),
+ ..Default::default()
+ },
+ lsp::Diagnostic{
+ range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
+ severity: Some(lsp::DiagnosticSeverity::ERROR),
+ message: "use of moved value\nvalue used here after move".to_string(),
+ related_information: Some(vec![
+ lsp::DiagnosticRelatedInformation {
+ location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
+ message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
+ },
+ lsp::DiagnosticRelatedInformation {
+ location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
+ message: "value moved here".to_string()
+ },
+ ]),
+ ..Default::default()
+ }
+ ],
+ version: None
+ }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
+
+ // Create diagnostics for other.rs to ensure that the file and
+ // diagnostics are not included in `BufferDiagnosticsEditor` when it is
+ // deployed for main.rs.
+ lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
+ uri: lsp::Uri::from_file_path(path!("/test/other.rs")).unwrap(),
+ diagnostics: vec![
+ lsp::Diagnostic{
+ range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 14)),
+ severity: Some(lsp::DiagnosticSeverity::WARNING),
+ message: "unused variable: `unused`".to_string(),
+ ..Default::default()
+ },
+ lsp::Diagnostic{
+ range: lsp::Range::new(lsp::Position::new(2, 4), lsp::Position::new(2, 22)),
+ severity: Some(lsp::DiagnosticSeverity::ERROR),
+ message: "cannot find function `undefined_function` in this scope".to_string(),
+ ..Default::default()
+ }
+ ],
+ version: None
+ }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
+ });
+
+ let buffer_diagnostics = window.build_entity(cx, |window, cx| {
+ BufferDiagnosticsEditor::new(
+ project_path.clone(),
+ project.clone(),
+ buffer,
+ true,
+ window,
+ cx,
+ )
+ });
+ let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
+ buffer_diagnostics.editor().clone()
+ });
+
+ // Since the excerpt updates is handled by a background task, we need to
+ // wait a little bit to ensure that the buffer diagnostic's editor content
+ // is rendered.
+ cx.executor()
+ .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+
+ pretty_assertions::assert_eq!(
+ editor_content_with_blocks(&editor, cx),
+ indoc::indoc! {
+ "§ main.rs
+ § -----
+ fn main() {
+ let x = vec![];
+ § move occurs because `x` has type `Vec<char>`, which does not implement
+ § the `Copy` trait (back)
+ let y = vec![];
+ § move occurs because `y` has type `Vec<char>`, which does not implement
+ § the `Copy` trait
+ a(x); § value moved here
+ b(y); § value moved here
+ c(y);
+ § use of moved value
+ § value used here after move
+ d(x);
+ § use of moved value
+ § value used here after move
+ § hint: move occurs because `x` has type `Vec<char>`, which does not
+ § implement the `Copy` trait
+ }"
+ }
+ );
+}
+
+#[gpui::test]
+async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/test"),
+ json!({
+ "main.rs": "
+ fn main() {
+ let x = vec![];
+ let y = vec![];
+ a(x);
+ b(y);
+ c(y);
+ d(x);
+ }
+ "
+ .unindent(),
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
+ let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let cx = &mut VisualTestContext::from_window(*window, cx);
+ let project_path = project::ProjectPath {
+ worktree_id: project.read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ }),
+ path: Arc::from(Path::new("main.rs")),
+ };
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_buffer(project_path.clone(), cx)
+ })
+ .await
+ .ok();
+
+ let language_server_id = LanguageServerId(0);
+ let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
+ let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
+
+ lsp_store.update(cx, |lsp_store, cx| {
+ lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
+ uri: uri.clone(),
+ diagnostics: vec![
+ lsp::Diagnostic{
+ range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
+ severity: Some(lsp::DiagnosticSeverity::WARNING),
+ message: "use of moved value\nvalue used here after move".to_string(),
+ related_information: Some(vec![
+ lsp::DiagnosticRelatedInformation {
+ location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
+ message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
+ },
+ lsp::DiagnosticRelatedInformation {
+ location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
+ message: "value moved here".to_string()
+ },
+ ]),
+ ..Default::default()
+ },
+ lsp::Diagnostic{
+ range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
+ severity: Some(lsp::DiagnosticSeverity::ERROR),
+ message: "use of moved value\nvalue used here after move".to_string(),
+ related_information: Some(vec![
+ lsp::DiagnosticRelatedInformation {
+ location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
+ message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
+ },
+ lsp::DiagnosticRelatedInformation {
+ location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
+ message: "value moved here".to_string()
+ },
+ ]),
+ ..Default::default()
+ }
+ ],
+ version: None
+ }, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
+ });
+
+ let include_warnings = false;
+ let buffer_diagnostics = window.build_entity(cx, |window, cx| {
+ BufferDiagnosticsEditor::new(
+ project_path.clone(),
+ project.clone(),
+ buffer,
+ include_warnings,
+ window,
+ cx,
+ )
+ });
+
+ let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
+ buffer_diagnostics.editor().clone()
+ });
+
+ // Since the excerpt updates is handled by a background task, we need to
+ // wait a little bit to ensure that the buffer diagnostic's editor content
+ // is rendered.
+ cx.executor()
+ .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+
+ pretty_assertions::assert_eq!(
+ editor_content_with_blocks(&editor, cx),
+ indoc::indoc! {
+ "§ main.rs
+ § -----
+ fn main() {
+ let x = vec![];
+ § move occurs because `x` has type `Vec<char>`, which does not implement
+ § the `Copy` trait (back)
+ let y = vec![];
+ a(x); § value moved here
+ b(y);
+ c(y);
+ d(x);
+ § use of moved value
+ § value used here after move
+ § hint: move occurs because `x` has type `Vec<char>`, which does not
+ § implement the `Copy` trait
+ }"
+ }
+ );
+}
+
+#[gpui::test]
+async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ path!("/test"),
+ json!({
+ "main.rs": "
+ fn main() {
+ let x = vec![];
+ let y = vec![];
+ a(x);
+ b(y);
+ c(y);
+ d(x);
+ }
+ "
+ .unindent(),
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
+ let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let cx = &mut VisualTestContext::from_window(*window, cx);
+ let project_path = project::ProjectPath {
+ worktree_id: project.read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ }),
+ path: Arc::from(Path::new("main.rs")),
+ };
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_buffer(project_path.clone(), cx)
+ })
+ .await
+ .ok();
+
+ // Create the diagnostics for `main.rs`.
+ // Two warnings are being created, one for each language server, in order to
+ // assert that both warnings are rendered in the editor.
+ let language_server_id_a = LanguageServerId(0);
+ let language_server_id_b = LanguageServerId(1);
+ let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
+ let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
+
+ lsp_store.update(cx, |lsp_store, cx| {
+ lsp_store
+ .update_diagnostics(
+ language_server_id_a,
+ lsp::PublishDiagnosticsParams {
+ uri: uri.clone(),
+ diagnostics: vec![lsp::Diagnostic {
+ range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
+ severity: Some(lsp::DiagnosticSeverity::WARNING),
+ message: "use of moved value\nvalue used here after move".to_string(),
+ related_information: None,
+ ..Default::default()
+ }],
+ version: None,
+ },
+ None,
+ DiagnosticSourceKind::Pushed,
+ &[],
+ cx,
+ )
+ .unwrap();
+
+ lsp_store
+ .update_diagnostics(
+ language_server_id_b,
+ lsp::PublishDiagnosticsParams {
+ uri: uri.clone(),
+ diagnostics: vec![lsp::Diagnostic {
+ range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
+ severity: Some(lsp::DiagnosticSeverity::WARNING),
+ message: "use of moved value\nvalue used here after move".to_string(),
+ related_information: None,
+ ..Default::default()
+ }],
+ version: None,
+ },
+ None,
+ DiagnosticSourceKind::Pushed,
+ &[],
+ cx,
+ )
+ .unwrap();
+ });
+
+ let buffer_diagnostics = window.build_entity(cx, |window, cx| {
+ BufferDiagnosticsEditor::new(
+ project_path.clone(),
+ project.clone(),
+ buffer,
+ true,
+ window,
+ cx,
+ )
+ });
+ let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
+ buffer_diagnostics.editor().clone()
+ });
+
+ // Since the excerpt updates is handled by a background task, we need to
+ // wait a little bit to ensure that the buffer diagnostic's editor content
+ // is rendered.
+ cx.executor()
+ .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+
+ pretty_assertions::assert_eq!(
+ editor_content_with_blocks(&editor, cx),
+ indoc::indoc! {
+ "§ main.rs
+ § -----
+ a(x);
+ b(y);
+ c(y);
+ § use of moved value
+ § value used here after move
+ d(x);
+ § use of moved value
+ § value used here after move
+ }"
+ }
+ );
+
+ buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
+ assert_eq!(
+ *buffer_diagnostics.summary(),
+ DiagnosticSummary {
+ warning_count: 2,
+ error_count: 0
+ }
+ );
+ })
+}
+
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
zlog::init_test();
@@ -1588,10 +2023,10 @@ fn randomly_update_diagnostics_for_path(
next_id: &mut usize,
rng: &mut impl Rng,
) {
- let mutation_count = rng.gen_range(1..=3);
+ let mutation_count = rng.random_range(1..=3);
for _ in 0..mutation_count {
- if rng.gen_bool(0.3) && !diagnostics.is_empty() {
- let idx = rng.gen_range(0..diagnostics.len());
+ if rng.random_bool(0.3) && !diagnostics.is_empty() {
+ let idx = rng.random_range(0..diagnostics.len());
log::info!(" removing diagnostic at index {idx}");
diagnostics.remove(idx);
} else {
@@ -1600,7 +2035,7 @@ fn randomly_update_diagnostics_for_path(
let new_diagnostic = random_lsp_diagnostic(rng, fs, path, unique_id);
- let ix = rng.gen_range(0..=diagnostics.len());
+ let ix = rng.random_range(0..=diagnostics.len());
log::info!(
" inserting {} at index {ix}. {},{}..{},{}",
new_diagnostic.message,
@@ -1637,8 +2072,8 @@ fn random_lsp_diagnostic(
let file_content = fs.read_file_sync(path).unwrap();
let file_text = Rope::from(String::from_utf8_lossy(&file_content).as_ref());
- let start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
- let end = rng.gen_range(start..file_text.len().saturating_add(ERROR_MARGIN));
+ let start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN));
+ let end = rng.random_range(start..file_text.len().saturating_add(ERROR_MARGIN));
let start_point = file_text.offset_to_point_utf16(start);
let end_point = file_text.offset_to_point_utf16(end);
@@ -1648,7 +2083,7 @@ fn random_lsp_diagnostic(
lsp::Position::new(end_point.row, end_point.column),
);
- let severity = if rng.gen_bool(0.5) {
+ let severity = if rng.random_bool(0.5) {
Some(lsp::DiagnosticSeverity::ERROR)
} else {
Some(lsp::DiagnosticSeverity::WARNING)
@@ -1656,13 +2091,14 @@ fn random_lsp_diagnostic(
let message = format!("diagnostic {unique_id}");
- let related_information = if rng.gen_bool(0.3) {
- let info_count = rng.gen_range(1..=3);
+ let related_information = if rng.random_bool(0.3) {
+ let info_count = rng.random_range(1..=3);
let mut related_info = Vec::with_capacity(info_count);
for i in 0..info_count {
- let info_start = rng.gen_range(0..file_text.len().saturating_add(ERROR_MARGIN));
- let info_end = rng.gen_range(info_start..file_text.len().saturating_add(ERROR_MARGIN));
+ let info_start = rng.random_range(0..file_text.len().saturating_add(ERROR_MARGIN));
+ let info_end =
+ rng.random_range(info_start..file_text.len().saturating_add(ERROR_MARGIN));
let info_start_point = file_text.offset_to_point_utf16(info_start);
let info_end_point = file_text.offset_to_point_utf16(info_end);
@@ -1673,7 +2109,7 @@ fn random_lsp_diagnostic(
);
related_info.push(lsp::DiagnosticRelatedInformation {
- location: lsp::Location::new(lsp::Url::from_file_path(path).unwrap(), info_range),
+ location: lsp::Location::new(lsp::Uri::from_file_path(path).unwrap(), info_range),
message: format!("related info {i} for diagnostic {unique_id}"),
});
}
@@ -32,49 +32,38 @@ impl Render for DiagnosticIndicator {
}
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
- (0, 0) => h_flex().map(|this| {
- this.child(
- Icon::new(IconName::Check)
- .size(IconSize::Small)
- .color(Color::Default),
- )
- }),
- (0, warning_count) => h_flex()
- .gap_1()
- .child(
- Icon::new(IconName::Warning)
- .size(IconSize::Small)
- .color(Color::Warning),
- )
- .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
- (error_count, 0) => h_flex()
- .gap_1()
- .child(
- Icon::new(IconName::XCircle)
- .size(IconSize::Small)
- .color(Color::Error),
- )
- .child(Label::new(error_count.to_string()).size(LabelSize::Small)),
+ (0, 0) => h_flex().child(
+ Icon::new(IconName::Check)
+ .size(IconSize::Small)
+ .color(Color::Default),
+ ),
(error_count, warning_count) => h_flex()
.gap_1()
- .child(
- Icon::new(IconName::XCircle)
- .size(IconSize::Small)
- .color(Color::Error),
- )
- .child(Label::new(error_count.to_string()).size(LabelSize::Small))
- .child(
- Icon::new(IconName::Warning)
- .size(IconSize::Small)
- .color(Color::Warning),
- )
- .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
+ .when(error_count > 0, |this| {
+ this.child(
+ Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error),
+ )
+ .child(Label::new(error_count.to_string()).size(LabelSize::Small))
+ })
+ .when(warning_count > 0, |this| {
+ this.child(
+ Icon::new(IconName::Warning)
+ .size(IconSize::Small)
+ .color(Color::Warning),
+ )
+ .child(Label::new(warning_count.to_string()).size(LabelSize::Small))
+ }),
};
let status = if let Some(diagnostic) = &self.current_diagnostic {
- let message = diagnostic.message.split('\n').next().unwrap().to_string();
+ let message = diagnostic
+ .message
+ .split_once('\n')
+ .map_or(&*diagnostic.message, |(first, _)| first);
Some(
- Button::new("diagnostic_message", message)
+ Button::new("diagnostic_message", SharedString::new(message))
.label_size(LabelSize::Small)
.tooltip(|window, cx| {
Tooltip::for_action(
@@ -1,43 +1,56 @@
-use std::sync::Arc;
-
-use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
-use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window};
+use crate::{BufferDiagnosticsEditor, ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
+use gpui::{Context, EventEmitter, ParentElement, Render, Window};
+use language::DiagnosticEntry;
+use text::{Anchor, BufferId};
use ui::prelude::*;
use ui::{IconButton, IconButtonShape, IconName, Tooltip};
use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, item::ItemHandle};
pub struct ToolbarControls {
- editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
+ editor: Option<Box<dyn DiagnosticsToolbarEditor>>,
+}
+
+pub(crate) trait DiagnosticsToolbarEditor: Send + Sync {
+ /// Informs the toolbar whether warnings are included in the diagnostics.
+ fn include_warnings(&self, cx: &App) -> bool;
+ /// Toggles whether warning diagnostics should be displayed by the
+ /// diagnostics editor.
+ fn toggle_warnings(&self, window: &mut Window, cx: &mut App);
+ /// Indicates whether any of the excerpts displayed by the diagnostics
+ /// editor are stale.
+ fn has_stale_excerpts(&self, cx: &App) -> bool;
+ /// Indicates whether the diagnostics editor is currently updating the
+ /// diagnostics.
+ fn is_updating(&self, cx: &App) -> bool;
+ /// Requests that the diagnostics editor stop updating the diagnostics.
+ fn stop_updating(&self, cx: &mut App);
+ /// Requests that the diagnostics editor updates the displayed diagnostics
+ /// with the latest information.
+ fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App);
+ /// Returns a list of diagnostics for the provided buffer id.
+ fn get_diagnostics_for_buffer(
+ &self,
+ buffer_id: BufferId,
+ cx: &App,
+ ) -> Vec<DiagnosticEntry<Anchor>>;
}
impl Render for ToolbarControls {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let mut include_warnings = false;
let mut has_stale_excerpts = false;
+ let mut include_warnings = false;
let mut is_updating = false;
- let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| {
- editor.read(cx).cargo_diagnostics_sources(cx)
- }));
- let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty();
- if let Some(editor) = self.diagnostics() {
- let diagnostics = editor.read(cx);
- include_warnings = diagnostics.include_warnings;
- has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
- is_updating = if fetch_cargo_diagnostics {
- diagnostics.cargo_diagnostics_fetch.fetch_task.is_some()
- } else {
- diagnostics.update_excerpts_task.is_some()
- || diagnostics
- .project
- .read(cx)
- .language_servers_running_disk_based_diagnostics(cx)
- .next()
- .is_some()
- };
+ match &self.editor {
+ Some(editor) => {
+ include_warnings = editor.include_warnings(cx);
+ has_stale_excerpts = editor.has_stale_excerpts(cx);
+ is_updating = editor.is_updating(cx);
+ }
+ None => {}
}
- let tooltip = if include_warnings {
+ let warning_tooltip = if include_warnings {
"Exclude Warnings"
} else {
"Include Warnings"
@@ -62,12 +75,12 @@ impl Render for ToolbarControls {
&ToggleDiagnosticsRefresh,
))
.on_click(cx.listener(move |toolbar_controls, _, _, cx| {
- if let Some(diagnostics) = toolbar_controls.diagnostics() {
- diagnostics.update(cx, |diagnostics, cx| {
- diagnostics.stop_cargo_diagnostics_fetch(cx);
- diagnostics.update_excerpts_task = None;
+ match toolbar_controls.editor() {
+ Some(editor) => {
+ editor.stop_updating(cx);
cx.notify();
- });
+ }
+ None => {}
}
})),
)
@@ -76,27 +89,17 @@ impl Render for ToolbarControls {
IconButton::new("refresh-diagnostics", IconName::ArrowCircle)
.icon_color(Color::Info)
.shape(IconButtonShape::Square)
- .disabled(!has_stale_excerpts && !fetch_cargo_diagnostics)
+ .disabled(!has_stale_excerpts)
.tooltip(Tooltip::for_action_title(
"Refresh diagnostics",
&ToggleDiagnosticsRefresh,
))
.on_click(cx.listener({
- move |toolbar_controls, _, window, cx| {
- if let Some(diagnostics) = toolbar_controls.diagnostics() {
- let cargo_diagnostics_sources =
- Arc::clone(&cargo_diagnostics_sources);
- diagnostics.update(cx, move |diagnostics, cx| {
- if fetch_cargo_diagnostics {
- diagnostics.fetch_cargo_diagnostics(
- cargo_diagnostics_sources,
- cx,
- );
- } else {
- diagnostics.update_all_excerpts(window, cx);
- }
- });
- }
+ move |toolbar_controls, _, window, cx| match toolbar_controls
+ .editor()
+ {
+ Some(editor) => editor.refresh_diagnostics(window, cx),
+ None => {}
}
})),
)
@@ -106,13 +109,10 @@ impl Render for ToolbarControls {
IconButton::new("toggle-warnings", IconName::Warning)
.icon_color(warning_color)
.shape(IconButtonShape::Square)
- .tooltip(Tooltip::text(tooltip))
- .on_click(cx.listener(|this, _, window, cx| {
- if let Some(editor) = this.diagnostics() {
- editor.update(cx, |editor, cx| {
- editor.toggle_warnings(&Default::default(), window, cx);
- });
- }
+ .tooltip(Tooltip::text(warning_tooltip))
+ .on_click(cx.listener(|this, _, window, cx| match &this.editor {
+ Some(editor) => editor.toggle_warnings(window, cx),
+ None => {}
})),
)
}
@@ -129,7 +129,10 @@ impl ToolbarItemView for ToolbarControls {
) -> ToolbarItemLocation {
if let Some(pane_item) = active_pane_item.as_ref() {
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
- self.editor = Some(editor.downgrade());
+ self.editor = Some(Box::new(editor.downgrade()));
+ ToolbarItemLocation::PrimaryRight
+ } else if let Some(editor) = pane_item.downcast::<BufferDiagnosticsEditor>() {
+ self.editor = Some(Box::new(editor.downgrade()));
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
@@ -151,7 +154,7 @@ impl ToolbarControls {
ToolbarControls { editor: None }
}
- fn diagnostics(&self) -> Option<Entity<ProjectDiagnosticsEditor>> {
- self.editor.as_ref()?.upgrade()
+ fn editor(&self) -> Option<&dyn DiagnosticsToolbarEditor> {
+ self.editor.as_deref()
}
}
@@ -19,9 +19,13 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
});
+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(dump_all_gpui_actions);
-const FRONT_MATTER_COMMENT: &'static str = "<!-- ZED_META {} -->";
+const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
fn main() -> Result<()> {
zlog::init();
@@ -61,15 +65,13 @@ impl PreprocessorError {
for alias in action.deprecated_aliases {
if alias == &action_name {
return PreprocessorError::DeprecatedActionUsed {
- used: action_name.clone(),
+ used: action_name,
should_be: action.name.to_string(),
};
}
}
}
- PreprocessorError::ActionNotFound {
- action_name: action_name.to_string(),
- }
+ PreprocessorError::ActionNotFound { action_name }
}
}
@@ -101,12 +103,13 @@ fn handle_preprocessing() -> Result<()> {
let mut errors = HashSet::<PreprocessorError>::new();
handle_frontmatter(&mut book, &mut errors);
+ template_big_table_of_actions(&mut book);
template_and_validate_keybindings(&mut book, &mut errors);
template_and_validate_actions(&mut book, &mut errors);
if !errors.is_empty() {
- const ANSI_RED: &'static str = "\x1b[31m";
- const ANSI_RESET: &'static str = "\x1b[0m";
+ const ANSI_RED: &str = "\x1b[31m";
+ const ANSI_RESET: &str = "\x1b[0m";
for error in &errors {
eprintln!("{ANSI_RED}ERROR{ANSI_RESET}: {}", error);
}
@@ -129,7 +132,7 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>)
let Some((name, value)) = line.split_once(':') else {
errors.insert(PreprocessorError::InvalidFrontmatterLine(format!(
"{}: {}",
- chapter_breadcrumbs(&chapter),
+ chapter_breadcrumbs(chapter),
line
)));
continue;
@@ -143,11 +146,20 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>)
&serde_json::to_string(&metadata).expect("Failed to serialize metadata"),
)
});
- match new_content {
- Cow::Owned(content) => {
- chapter.content = content;
- }
- Cow::Borrowed(_) => {}
+ if let Cow::Owned(content) = new_content {
+ chapter.content = content;
+ }
+ });
+}
+
+fn template_big_table_of_actions(book: &mut Book) {
+ for_each_chapter_mut(book, |chapter| {
+ let needle = "{#ACTIONS_TABLE#}";
+ if let Some(start) = chapter.content.rfind(needle) {
+ chapter.content.replace_range(
+ start..start + needle.len(),
+ &generate_big_table_of_actions(),
+ );
}
});
}
@@ -208,6 +220,7 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
let keymap = match os {
"macos" => &KEYMAP_MACOS,
"linux" | "freebsd" => &KEYMAP_LINUX,
+ "windows" => &KEYMAP_WINDOWS,
_ => unreachable!("Not a valid OS: {}", os),
};
@@ -282,6 +295,7 @@ struct ActionDef {
name: &'static str,
human_name: String,
deprecated_aliases: &'static [&'static str],
+ docs: Option<&'static str>,
}
fn dump_all_gpui_actions() -> Vec<ActionDef> {
@@ -290,12 +304,13 @@ fn dump_all_gpui_actions() -> Vec<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);
- return actions;
+ actions
}
fn handle_postprocessing() -> Result<()> {
@@ -402,20 +417,20 @@ fn handle_postprocessing() -> Result<()> {
path: &'a std::path::PathBuf,
root: &'a std::path::PathBuf,
) -> &'a std::path::Path {
- &path.strip_prefix(&root).unwrap_or(&path)
+ path.strip_prefix(&root).unwrap_or(path)
}
fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String {
let title_tag_contents = &title_regex()
- .captures(&contents)
+ .captures(contents)
.with_context(|| format!("Failed to find title in {:?}", pretty_path))
.expect("Page has <title> element")[1];
- let title = title_tag_contents
+
+ title_tag_contents
.trim()
.strip_suffix("- Zed")
.unwrap_or(title_tag_contents)
.trim()
- .to_string();
- title
+ .to_string()
}
}
@@ -423,3 +438,54 @@ fn title_regex() -> &'static Regex {
static TITLE_REGEX: OnceLock<Regex> = OnceLock::new();
TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap())
}
+
+fn generate_big_table_of_actions() -> String {
+ let actions = &*ALL_ACTIONS;
+ let mut output = String::new();
+
+ let mut actions_sorted = actions.iter().collect::<Vec<_>>();
+ 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");
+
+ for action in actions_sorted.into_iter() {
+ // Add the humanized action name as the term with margin
+ output.push_str(
+ "<dt style=\"margin-top: 1.5em; margin-bottom: 0.5em; font-weight: bold;\"><code>",
+ );
+ output.push_str(&action.human_name);
+ output.push_str("</code></dt>\n");
+
+ // Add the definition with keymap name and description
+ 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 {
+ output.push_str(
+ &description
+ .replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">"),
+ );
+ output.push_str("<br>\n");
+ }
+ output.push_str("Keymap Name: <code>");
+ output.push_str(action.name);
+ output.push_str("</code><br>\n");
+ if !action.deprecated_aliases.is_empty() {
+ output.push_str("Deprecated Aliases:");
+ for alias in action.deprecated_aliases.iter() {
+ output.push_str("<code>");
+ output.push_str(alias);
+ output.push_str("</code>, ");
+ }
+ }
+ output.push_str("\n</dd>\n");
+ }
+
+ // Close the definition list
+ output.push_str("</dl>\n");
+
+ output
+}
@@ -34,7 +34,7 @@ pub enum DataCollectionState {
impl DataCollectionState {
pub fn is_supported(&self) -> bool {
- !matches!(self, DataCollectionState::Unsupported { .. })
+ !matches!(self, DataCollectionState::Unsupported)
}
pub fn is_enabled(&self) -> bool {
@@ -89,9 +89,6 @@ pub trait EditPredictionProvider: 'static + Sized {
debounce: bool,
cx: &mut Context<Self>,
);
- fn needs_terms_acceptance(&self, _cx: &App) -> bool {
- false
- }
fn cycle(
&mut self,
buffer: Entity<Buffer>,
@@ -124,7 +121,6 @@ pub trait EditPredictionProviderHandle {
fn data_collection_state(&self, cx: &App) -> DataCollectionState;
fn usage(&self, cx: &App) -> Option<EditPredictionUsage>;
fn toggle_data_collection(&self, cx: &mut App);
- fn needs_terms_acceptance(&self, cx: &App) -> bool;
fn is_refreshing(&self, cx: &App) -> bool;
fn refresh(
&self,
@@ -196,10 +192,6 @@ where
self.read(cx).is_enabled(buffer, cursor_position, cx)
}
- fn needs_terms_acceptance(&self, cx: &App) -> bool {
- self.read(cx).needs_terms_acceptance(cx)
- }
-
fn is_refreshing(&self, cx: &App) -> bool {
self.read(cx).is_refreshing()
}
@@ -127,7 +127,7 @@ impl Render for EditPredictionButton {
}),
);
}
- let this = cx.entity().clone();
+ let this = cx.entity();
div().child(
PopoverMenu::new("copilot")
@@ -168,7 +168,7 @@ impl Render for EditPredictionButton {
let account_status = agent.account_status.clone();
match account_status {
AccountStatus::NeedsActivation { activate_url } => {
- SupermavenButtonStatus::NeedsActivation(activate_url.clone())
+ SupermavenButtonStatus::NeedsActivation(activate_url)
}
AccountStatus::Unknown => SupermavenButtonStatus::Initializing,
AccountStatus::Ready => SupermavenButtonStatus::Ready,
@@ -182,10 +182,10 @@ impl Render for EditPredictionButton {
let icon = status.to_icon();
let tooltip_text = status.to_tooltip();
let has_menu = status.has_menu();
- let this = cx.entity().clone();
+ let this = cx.entity();
let fs = self.fs.clone();
- return div().child(
+ div().child(
PopoverMenu::new("supermaven")
.menu(move |window, cx| match &status {
SupermavenButtonStatus::NeedsActivation(activate_url) => {
@@ -230,7 +230,7 @@ impl Render for EditPredictionButton {
},
)
.with_handle(self.popover_menu_handle.clone()),
- );
+ )
}
EditPredictionProvider::Zed => {
@@ -242,13 +242,9 @@ impl Render for EditPredictionButton {
IconName::ZedPredictDisabled
};
- if zeta::should_show_upsell_modal(&self.user_store, cx) {
+ if zeta::should_show_upsell_modal() {
let tooltip_meta = if self.user_store.read(cx).current_user().is_some() {
- if self.user_store.read(cx).has_accepted_terms_of_service() {
- "Choose a Plan"
- } else {
- "Accept the Terms of Service"
- }
+ "Choose a Plan"
} else {
"Sign In"
};
@@ -331,7 +327,7 @@ impl Render for EditPredictionButton {
})
});
- let this = cx.entity().clone();
+ let this = cx.entity();
let mut popover_menu = PopoverMenu::new("zeta")
.menu(move |window, cx| {
@@ -343,7 +339,7 @@ impl Render for EditPredictionButton {
let is_refreshing = self
.edit_prediction_provider
.as_ref()
- .map_or(false, |provider| provider.is_refreshing(cx));
+ .is_some_and(|provider| provider.is_refreshing(cx));
if is_refreshing {
popover_menu = popover_menu.trigger(
@@ -94,6 +94,7 @@ zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
+criterion.workspace = true
ctor.workspace = true
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
@@ -119,3 +120,12 @@ util = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
zlog.workspace = true
+
+
+[[bench]]
+name = "editor_render"
+harness = false
+
+[[bench]]
+name = "display_map"
+harness = false
@@ -0,0 +1,102 @@
+use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
+use editor::MultiBuffer;
+use gpui::TestDispatcher;
+use itertools::Itertools;
+use rand::{Rng, SeedableRng, rngs::StdRng};
+use std::num::NonZeroU32;
+use text::Bias;
+use util::RandomCharIter;
+
+fn to_tab_point_benchmark(c: &mut Criterion) {
+ let rng = StdRng::seed_from_u64(1);
+ let dispatcher = TestDispatcher::new(rng);
+ let cx = gpui::TestAppContext::build(dispatcher, None);
+
+ let create_tab_map = |length: usize| {
+ let mut rng = StdRng::seed_from_u64(1);
+ let text = RandomCharIter::new(&mut rng)
+ .take(length)
+ .collect::<String>();
+ let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
+
+ let buffer_snapshot = cx.read(|cx| buffer.read(cx).snapshot(cx));
+ use editor::display_map::*;
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
+ let fold_point = fold_snapshot.to_fold_point(
+ inlay_snapshot.to_point(InlayOffset(rng.random_range(0..length))),
+ Bias::Left,
+ );
+ let (_, snapshot) = TabMap::new(fold_snapshot, NonZeroU32::new(4).unwrap());
+
+ (length, snapshot, fold_point)
+ };
+
+ let inputs = [1024].into_iter().map(create_tab_map).collect_vec();
+
+ let mut group = c.benchmark_group("To tab point");
+
+ for (batch_size, snapshot, fold_point) in inputs {
+ group.bench_with_input(
+ BenchmarkId::new("to_tab_point", batch_size),
+ &snapshot,
+ |bench, snapshot| {
+ bench.iter(|| {
+ snapshot.to_tab_point(fold_point);
+ });
+ },
+ );
+ }
+
+ group.finish();
+}
+
+fn to_fold_point_benchmark(c: &mut Criterion) {
+ let rng = StdRng::seed_from_u64(1);
+ let dispatcher = TestDispatcher::new(rng);
+ let cx = gpui::TestAppContext::build(dispatcher, None);
+
+ let create_tab_map = |length: usize| {
+ let mut rng = StdRng::seed_from_u64(1);
+ let text = RandomCharIter::new(&mut rng)
+ .take(length)
+ .collect::<String>();
+ let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
+
+ let buffer_snapshot = cx.read(|cx| buffer.read(cx).snapshot(cx));
+ use editor::display_map::*;
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
+
+ let fold_point = fold_snapshot.to_fold_point(
+ inlay_snapshot.to_point(InlayOffset(rng.random_range(0..length))),
+ Bias::Left,
+ );
+
+ let (_, snapshot) = TabMap::new(fold_snapshot, NonZeroU32::new(4).unwrap());
+ let tab_point = snapshot.to_tab_point(fold_point);
+
+ (length, snapshot, tab_point)
+ };
+
+ let inputs = [1024].into_iter().map(create_tab_map).collect_vec();
+
+ let mut group = c.benchmark_group("To fold point");
+
+ for (batch_size, snapshot, tab_point) in inputs {
+ group.bench_with_input(
+ BenchmarkId::new("to_fold_point", batch_size),
+ &snapshot,
+ |bench, snapshot| {
+ bench.iter(|| {
+ snapshot.to_fold_point(tab_point, Bias::Left);
+ });
+ },
+ );
+ }
+
+ group.finish();
+}
+
+criterion_group!(benches, to_tab_point_benchmark, to_fold_point_benchmark);
+criterion_main!(benches);
@@ -0,0 +1,172 @@
+use criterion::{Bencher, BenchmarkId};
+use editor::{
+ Editor, EditorMode, MultiBuffer,
+ actions::{DeleteToPreviousWordStart, SelectAll, SplitSelectionIntoLines},
+};
+use gpui::{AppContext, Focusable as _, TestAppContext, TestDispatcher};
+use project::Project;
+use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
+use settings::SettingsStore;
+use ui::IntoElement;
+use util::RandomCharIter;
+
+fn editor_input_with_1000_cursors(bencher: &mut Bencher<'_>, cx: &TestAppContext) {
+ let mut cx = cx.clone();
+ let text = String::from_iter(["line:\n"; 1000]);
+ let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
+
+ let cx = cx.add_empty_window();
+ let editor = cx.update(|window, cx| {
+ let editor = cx.new(|cx| {
+ let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
+ editor.set_style(editor::EditorStyle::default(), window, cx);
+ editor.select_all(&SelectAll, window, cx);
+ editor.split_selection_into_lines(
+ &SplitSelectionIntoLines {
+ keep_selections: true,
+ },
+ window,
+ cx,
+ );
+ editor
+ });
+ window.focus(&editor.focus_handle(cx));
+ editor
+ });
+
+ bencher.iter(|| {
+ cx.update(|window, cx| {
+ editor.update(cx, |editor, cx| {
+ editor.handle_input("hello world", window, cx);
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: false,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: false,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ })
+ });
+}
+
+fn open_editor_with_one_long_line(bencher: &mut Bencher<'_>, args: &(String, TestAppContext)) {
+ let (text, cx) = args;
+ let mut cx = cx.clone();
+
+ bencher.iter(|| {
+ let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
+
+ let cx = cx.add_empty_window();
+ let _ = cx.update(|window, cx| {
+ let editor = cx.new(|cx| {
+ let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
+ editor.set_style(editor::EditorStyle::default(), window, cx);
+ editor
+ });
+ window.focus(&editor.focus_handle(cx));
+ editor
+ });
+ });
+}
+
+fn editor_render(bencher: &mut Bencher<'_>, cx: &TestAppContext) {
+ let mut cx = cx.clone();
+ let buffer = cx.update(|cx| {
+ let mut rng = StdRng::seed_from_u64(1);
+ let text_len = rng.random_range(10000..90000);
+ if rng.random() {
+ let text = RandomCharIter::new(&mut rng)
+ .take(text_len)
+ .collect::<String>();
+ MultiBuffer::build_simple(&text, cx)
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ }
+ });
+
+ let cx = cx.add_empty_window();
+ let editor = cx.update(|window, cx| {
+ let editor = cx.new(|cx| {
+ let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
+ editor.set_style(editor::EditorStyle::default(), window, cx);
+ editor
+ });
+ window.focus(&editor.focus_handle(cx));
+ editor
+ });
+
+ bencher.iter(|| {
+ cx.update(|window, cx| {
+ // editor.update(cx, |editor, cx| editor.move_down(&MoveDown, window, cx));
+ let mut view = editor.clone().into_any_element();
+ let _ = view.request_layout(window, cx);
+ let _ = view.prepaint(window, cx);
+ view.paint(window, cx);
+ });
+ })
+}
+
+pub fn benches() {
+ let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(1));
+ let cx = gpui::TestAppContext::build(dispatcher, None);
+ cx.update(|cx| {
+ let store = SettingsStore::test(cx);
+ cx.set_global(store);
+ assets::Assets.load_test_fonts(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ // release_channel::init(SemanticVersion::default(), cx);
+ client::init_settings(cx);
+ language::init(cx);
+ workspace::init_settings(cx);
+ Project::init_settings(cx);
+ editor::init(cx);
+ });
+
+ let mut criterion: criterion::Criterion<_> =
+ (criterion::Criterion::default()).configure_from_args();
+
+ // setup app context
+ let mut group = criterion.benchmark_group("Time to render");
+ group.bench_with_input(
+ BenchmarkId::new("editor_render", "TestAppContext"),
+ &cx,
+ editor_render,
+ );
+
+ group.finish();
+
+ let text = String::from_iter(["char"; 1000]);
+ let mut group = criterion.benchmark_group("Build buffer with one long line");
+ group.bench_with_input(
+ BenchmarkId::new("editor_with_one_long_line", "(String, TestAppContext )"),
+ &(text, cx.clone()),
+ open_editor_with_one_long_line,
+ );
+
+ group.finish();
+
+ let mut group = criterion.benchmark_group("multi cursor edits");
+ group.bench_with_input(
+ BenchmarkId::new("editor_input_with_1000_cursors", "TestAppContext"),
+ &cx,
+ editor_input_with_1000_cursors,
+ );
+ group.finish();
+}
+
+fn main() {
+ benches();
+ criterion::Criterion::default()
+ .configure_from_args()
+ .final_summary();
+}
@@ -228,21 +228,38 @@ pub struct ShowCompletions {
pub struct HandleInput(pub String);
/// Deletes from the cursor to the end of the next word.
+/// Stops before the end of the next word, if whitespace sequences of length >= 2 are encountered.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
pub struct DeleteToNextWordEnd {
#[serde(default)]
pub ignore_newlines: bool,
+ // Whether to stop before the end of the next word, if language-defined bracket is encountered.
+ #[serde(default)]
+ pub ignore_brackets: bool,
}
/// Deletes from the cursor to the start of the previous word.
+/// Stops before the start of the previous word, if whitespace sequences of length >= 2 are encountered.
#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = editor)]
#[serde(deny_unknown_fields)]
pub struct DeleteToPreviousWordStart {
#[serde(default)]
pub ignore_newlines: bool,
+ // Whether to stop before the start of the previous word, if language-defined bracket is encountered.
+ #[serde(default)]
+ pub ignore_brackets: bool,
+}
+
+/// Cuts from cursor to end of line.
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
+#[action(namespace = editor)]
+#[serde(deny_unknown_fields)]
+pub struct CutToEndOfLine {
+ #[serde(default)]
+ pub stop_at_newlines: bool,
}
/// Folds all code blocks at the specified indentation level.
@@ -273,6 +290,16 @@ pub enum UuidVersion {
V7,
}
+/// Splits selection into individual lines.
+#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
+#[action(namespace = editor)]
+#[serde(deny_unknown_fields)]
+pub struct SplitSelectionIntoLines {
+ /// Keep the text selected after splitting instead of collapsing to cursors.
+ #[serde(default)]
+ pub keep_selections: bool,
+}
+
/// Goes to the next diagnostic in the file.
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = editor)]
@@ -394,8 +421,6 @@ actions!(
CopyPermalinkToLine,
/// Cuts selected text to the clipboard.
Cut,
- /// Cuts from cursor to end of line.
- CutToEndOfLine,
/// Deletes the character after the cursor.
Delete,
/// Deletes the current line.
@@ -475,6 +500,10 @@ actions!(
GoToTypeDefinition,
/// Goes to type definition in a split pane.
GoToTypeDefinitionSplit,
+ /// Goes to the next document highlight.
+ GoToNextDocumentHighlight,
+ /// Goes to the previous document highlight.
+ GoToPreviousDocumentHighlight,
/// Scrolls down by half a page.
HalfPageDown,
/// Scrolls up by half a page.
@@ -622,6 +651,10 @@ actions!(
SelectEnclosingSymbol,
/// Selects the next larger syntax node.
SelectLargerSyntaxNode,
+ /// Selects the next syntax node sibling.
+ SelectNextSyntaxNode,
+ /// Selects the previous syntax node sibling.
+ SelectPreviousSyntaxNode,
/// Extends selection left.
SelectLeft,
/// Selects the current line.
@@ -672,8 +705,6 @@ actions!(
SortLinesCaseInsensitive,
/// Sorts selected lines case-sensitively.
SortLinesCaseSensitive,
- /// Splits selection into individual lines.
- SplitSelectionIntoLines,
/// Stops the language server for the current file.
StopLanguageServer,
/// Switches between source and header files.
@@ -745,6 +776,8 @@ actions!(
UniqueLinesCaseInsensitive,
/// Removes duplicate lines (case-sensitive).
UniqueLinesCaseSensitive,
- UnwrapSyntaxNode
+ UnwrapSyntaxNode,
+ /// Wraps selections in tag specified by language.
+ WrapSelectionsInTag
]
);
@@ -13,7 +13,7 @@ use crate::{Editor, SwitchSourceHeader, element::register_action};
use project::lsp_store::clangd_ext::CLANGD_SERVER_NAME;
fn is_c_language(language: &Language) -> bool {
- return language.name() == "C++".into() || language.name() == "C".into();
+ language.name() == "C++".into() || language.name() == "C".into()
}
pub fn switch_source_header(
@@ -104,6 +104,6 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
.filter_map(|buffer| buffer.read(cx).language())
.any(|language| is_c_language(language))
{
- register_action(&editor, window, switch_source_header);
+ register_action(editor, window, switch_source_header);
}
}
@@ -317,7 +317,7 @@ async fn filter_and_sort_matches(
let candidates: Arc<[StringMatchCandidate]> = completions
.iter()
.enumerate()
- .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text()))
+ .map(|(id, completion)| StringMatchCandidate::new(id, completion.label.filter_text()))
.collect();
let cancel_flag = Arc::new(AtomicBool::new(false));
let background_executor = cx.executor();
@@ -331,5 +331,5 @@ async fn filter_and_sort_matches(
background_executor,
)
.await;
- CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, &completions)
+ CompletionsMenu::sort_string_matches(matches, Some(query), snippet_sort_order, completions)
}
@@ -1,7 +1,9 @@
+use crate::scroll::ScrollAmount;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollStrategy, SharedString,
- Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px, uniform_list,
+ AnyElement, Entity, Focusable, FontWeight, ListSizingBehavior, ScrollHandle, ScrollStrategy,
+ SharedString, Size, StrikethroughStyle, StyledText, Task, UniformListScrollHandle, div, px,
+ uniform_list,
};
use itertools::Itertools;
use language::CodeLabel;
@@ -9,9 +11,9 @@ use language::{Buffer, LanguageName, LanguageRegistry};
use markdown::{Markdown, MarkdownElement};
use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat;
-use project::CompletionSource;
use project::lsp_store::CompletionDocumentation;
use project::{CodeAction, Completion, TaskSourceKind};
+use project::{CompletionDisplayOptions, CompletionSource};
use task::DebugScenario;
use task::TaskContext;
@@ -184,6 +186,20 @@ impl CodeContextMenu {
CodeContextMenu::CodeActions(_) => false,
}
}
+
+ pub fn scroll_aside(
+ &mut self,
+ scroll_amount: ScrollAmount,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) {
+ match self {
+ CodeContextMenu::Completions(completions_menu) => {
+ completions_menu.scroll_aside(scroll_amount, window, cx)
+ }
+ CodeContextMenu::CodeActions(_) => (),
+ }
+ }
}
pub enum ContextMenuOrigin {
@@ -207,12 +223,16 @@ pub struct CompletionsMenu {
filter_task: Task<()>,
cancel_filter: Arc<AtomicBool>,
scroll_handle: UniformListScrollHandle,
+ // The `ScrollHandle` used on the Markdown documentation rendered on the
+ // side of the completions menu.
+ pub scroll_handle_aside: ScrollHandle,
resolve_completions: bool,
show_completion_documentation: bool,
last_rendered_range: Rc<RefCell<Option<Range<usize>>>>,
markdown_cache: Rc<RefCell<VecDeque<(MarkdownCacheKey, Entity<Markdown>)>>>,
language_registry: Option<Arc<LanguageRegistry>>,
language: Option<LanguageName>,
+ display_options: CompletionDisplayOptions,
snippet_sort_order: SnippetSortOrder,
}
@@ -231,7 +251,7 @@ enum MarkdownCacheKey {
pub enum CompletionsMenuSource {
Normal,
SnippetChoices,
- Words,
+ Words { ignore_threshold: bool },
}
// TODO: There should really be a wrapper around fuzzy match tasks that does this.
@@ -252,6 +272,7 @@ impl CompletionsMenu {
is_incomplete: bool,
buffer: Entity<Buffer>,
completions: Box<[Completion]>,
+ display_options: CompletionDisplayOptions,
snippet_sort_order: SnippetSortOrder,
language_registry: Option<Arc<LanguageRegistry>>,
language: Option<LanguageName>,
@@ -279,11 +300,13 @@ impl CompletionsMenu {
filter_task: Task::ready(()),
cancel_filter: Arc::new(AtomicBool::new(false)),
scroll_handle: UniformListScrollHandle::new(),
+ scroll_handle_aside: ScrollHandle::new(),
resolve_completions: true,
last_rendered_range: RefCell::new(None).into(),
markdown_cache: RefCell::new(VecDeque::new()).into(),
language_registry,
language,
+ display_options,
snippet_sort_order,
};
@@ -321,7 +344,7 @@ impl CompletionsMenu {
let match_candidates = choices
.iter()
.enumerate()
- .map(|(id, completion)| StringMatchCandidate::new(id, &completion))
+ .map(|(id, completion)| StringMatchCandidate::new(id, completion))
.collect();
let entries = choices
.iter()
@@ -348,12 +371,14 @@ impl CompletionsMenu {
filter_task: Task::ready(()),
cancel_filter: Arc::new(AtomicBool::new(false)),
scroll_handle: UniformListScrollHandle::new(),
+ scroll_handle_aside: ScrollHandle::new(),
resolve_completions: false,
show_completion_documentation: false,
last_rendered_range: RefCell::new(None).into(),
markdown_cache: RefCell::new(VecDeque::new()).into(),
language_registry: None,
language: None,
+ display_options: CompletionDisplayOptions::default(),
snippet_sort_order,
}
}
@@ -514,7 +539,7 @@ impl CompletionsMenu {
// Expand the range to resolve more completions than are predicted to be visible, to reduce
// jank on navigation.
let entry_indices = util::expanded_and_wrapped_usize_range(
- entry_range.clone(),
+ entry_range,
RESOLVE_BEFORE_ITEMS,
RESOLVE_AFTER_ITEMS,
entries.len(),
@@ -716,6 +741,33 @@ impl CompletionsMenu {
cx: &mut Context<Editor>,
) -> AnyElement {
let show_completion_documentation = self.show_completion_documentation;
+ let widest_completion_ix = if self.display_options.dynamic_width {
+ let completions = self.completions.borrow();
+ let widest_completion_ix = self
+ .entries
+ .borrow()
+ .iter()
+ .enumerate()
+ .max_by_key(|(_, mat)| {
+ let completion = &completions[mat.candidate_id];
+ let documentation = &completion.documentation;
+
+ let mut len = completion.label.text.chars().count();
+ if let Some(CompletionDocumentation::SingleLine(text)) = documentation {
+ if show_completion_documentation {
+ len += text.chars().count();
+ }
+ }
+
+ len
+ })
+ .map(|(ix, _)| ix);
+ drop(completions);
+ widest_completion_ix
+ } else {
+ None
+ };
+
let selected_item = self.selected_item;
let completions = self.completions.clone();
let entries = self.entries.clone();
@@ -842,7 +894,13 @@ impl CompletionsMenu {
.max_h(max_height_in_lines as f32 * window.line_height())
.track_scroll(self.scroll_handle.clone())
.with_sizing_behavior(ListSizingBehavior::Infer)
- .w(rems(34.));
+ .map(|this| {
+ if self.display_options.dynamic_width {
+ this.with_width_from_item(widest_completion_ix)
+ } else {
+ this.w(rems(34.))
+ }
+ });
Popover::new().child(list).into_any_element()
}
@@ -911,6 +969,7 @@ impl CompletionsMenu {
.max_w(max_size.width)
.max_h(max_size.height)
.overflow_y_scroll()
+ .track_scroll(&self.scroll_handle_aside)
.occlude(),
)
.into_any_element(),
@@ -1111,10 +1170,8 @@ impl CompletionsMenu {
let query_start_doesnt_match_split_words = query_start_lower
.map(|query_char| {
!split_words(&string_match.string).any(|word| {
- word.chars()
- .next()
- .and_then(|c| c.to_lowercase().next())
- .map_or(false, |word_char| word_char == query_char)
+ word.chars().next().and_then(|c| c.to_lowercase().next())
+ == Some(query_char)
})
})
.unwrap_or(false);
@@ -1177,6 +1234,23 @@ impl CompletionsMenu {
}
});
}
+
+ pub fn scroll_aside(
+ &mut self,
+ amount: ScrollAmount,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) {
+ let mut offset = self.scroll_handle_aside.offset();
+
+ offset.y -= amount.pixels(
+ window.line_height(),
+ self.scroll_handle_aside.bounds().size.height - px(16.),
+ ) / 2.0;
+
+ cx.notify();
+ self.scroll_handle_aside.set_offset(offset);
+ }
}
#[derive(Clone)]
@@ -1428,6 +1502,7 @@ impl CodeActionsMenu {
this.child(
h_flex()
.overflow_hidden()
+ .text_sm()
.child(
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
action.lsp_action.title().replace("\n", ""),
@@ -37,13 +37,13 @@ pub use block_map::{
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
+use fold_map::FoldSnapshot;
pub use fold_map::{
ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
};
-use fold_map::{FoldMap, FoldSnapshot};
use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
pub use inlay_map::Inlay;
-use inlay_map::{InlayMap, InlaySnapshot};
+use inlay_map::InlaySnapshot;
pub use inlay_map::{InlayOffset, InlayPoint};
pub use invisibles::{is_invisible, replacement};
use language::{
@@ -66,12 +66,14 @@ use std::{
sync::Arc,
};
use sum_tree::{Bias, TreeMap};
-use tab_map::{TabMap, TabSnapshot};
+use tab_map::TabSnapshot;
use text::{BufferId, LineIndent};
use ui::{SharedString, px};
use unicode_segmentation::UnicodeSegmentation;
use wrap_map::{WrapMap, WrapSnapshot};
+pub use crate::display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap};
+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FoldStatus {
Folded,
@@ -703,9 +705,8 @@ impl<'a> HighlightedChunk<'a> {
}),
..Default::default()
};
- let invisible_style = if let Some(mut style) = style {
- style.highlight(invisible_highlight);
- style
+ let invisible_style = if let Some(style) = style {
+ style.highlight(invisible_highlight)
} else {
invisible_highlight
};
@@ -726,9 +727,8 @@ impl<'a> HighlightedChunk<'a> {
}),
..Default::default()
};
- let invisible_style = if let Some(mut style) = style {
- style.highlight(invisible_highlight);
- style
+ let invisible_style = if let Some(style) = style {
+ style.highlight(invisible_highlight)
} else {
invisible_highlight
};
@@ -962,62 +962,59 @@ impl DisplaySnapshot {
},
)
.flat_map(|chunk| {
- let mut highlight_style = chunk
+ let highlight_style = chunk
.syntax_highlight_id
.and_then(|id| id.style(&editor_style.syntax));
- if let Some(chunk_highlight) = chunk.highlight_style {
- // For color inlays, blend the color with the editor background
- let mut processed_highlight = chunk_highlight;
- if chunk.is_inlay {
- if let Some(inlay_color) = chunk_highlight.color {
- // Only blend if the color has transparency (alpha < 1.0)
- if inlay_color.a < 1.0 {
- let blended_color = editor_style.background.blend(inlay_color);
- processed_highlight.color = Some(blended_color);
+ let chunk_highlight = chunk.highlight_style.map(|chunk_highlight| {
+ HighlightStyle {
+ // For color inlays, blend the color with the editor background
+ // if the color has transparency (alpha < 1.0)
+ color: chunk_highlight.color.map(|color| {
+ if chunk.is_inlay && !color.is_opaque() {
+ editor_style.background.blend(color)
+ } else {
+ color
}
- }
+ }),
+ ..chunk_highlight
}
+ });
- if let Some(highlight_style) = highlight_style.as_mut() {
- highlight_style.highlight(processed_highlight);
- } else {
- highlight_style = Some(processed_highlight);
- }
- }
-
- let mut diagnostic_highlight = HighlightStyle::default();
-
- if let Some(severity) = chunk.diagnostic_severity.filter(|severity| {
- self.diagnostics_max_severity
- .into_lsp()
- .map_or(false, |max_severity| severity <= &max_severity)
- }) {
- if chunk.is_unnecessary {
- diagnostic_highlight.fade_out = Some(editor_style.unnecessary_code_fade);
- }
- if chunk.underline
- && editor_style.show_underlines
- && !(chunk.is_unnecessary && severity > lsp::DiagnosticSeverity::WARNING)
- {
- let diagnostic_color = super::diagnostic_style(severity, &editor_style.status);
- diagnostic_highlight.underline = Some(UnderlineStyle {
- color: Some(diagnostic_color),
- thickness: 1.0.into(),
- wavy: true,
- });
- }
- }
+ let diagnostic_highlight = chunk
+ .diagnostic_severity
+ .filter(|severity| {
+ self.diagnostics_max_severity
+ .into_lsp()
+ .is_some_and(|max_severity| severity <= &max_severity)
+ })
+ .map(|severity| HighlightStyle {
+ fade_out: chunk
+ .is_unnecessary
+ .then_some(editor_style.unnecessary_code_fade),
+ underline: (chunk.underline
+ && editor_style.show_underlines
+ && !(chunk.is_unnecessary && severity > lsp::DiagnosticSeverity::WARNING))
+ .then(|| {
+ let diagnostic_color =
+ super::diagnostic_style(severity, &editor_style.status);
+ UnderlineStyle {
+ color: Some(diagnostic_color),
+ thickness: 1.0.into(),
+ wavy: true,
+ }
+ }),
+ ..Default::default()
+ });
- if let Some(highlight_style) = highlight_style.as_mut() {
- highlight_style.highlight(diagnostic_highlight);
- } else {
- highlight_style = Some(diagnostic_highlight);
- }
+ let style = [highlight_style, chunk_highlight, diagnostic_highlight]
+ .into_iter()
+ .flatten()
+ .reduce(|acc, highlight| acc.highlight(highlight));
HighlightedChunk {
text: chunk.text,
- style: highlight_style,
+ style,
is_tab: chunk.is_tab,
is_inlay: chunk.is_inlay,
replacement: chunk.renderer.map(ChunkReplacement::Renderer),
@@ -1552,15 +1549,15 @@ pub mod tests {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
- let mut tab_size = rng.gen_range(1..=4);
- let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
- let excerpt_header_height = rng.gen_range(1..=5);
+ let mut tab_size = rng.random_range(1..=4);
+ let buffer_start_excerpt_header_height = rng.random_range(1..=5);
+ let excerpt_header_height = rng.random_range(1..=5);
let font_size = px(14.0);
let max_wrap_width = 300.0;
- let mut wrap_width = if rng.gen_bool(0.1) {
+ let mut wrap_width = if rng.random_bool(0.1) {
None
} else {
- Some(px(rng.gen_range(0.0..=max_wrap_width)))
+ Some(px(rng.random_range(0.0..=max_wrap_width)))
};
log::info!("tab size: {}", tab_size);
@@ -1571,8 +1568,8 @@ pub mod tests {
});
let buffer = cx.update(|cx| {
- if rng.r#gen() {
- let len = rng.gen_range(0..10);
+ if rng.random() {
+ let len = rng.random_range(0..10);
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
@@ -1609,12 +1606,12 @@ pub mod tests {
log::info!("display text: {:?}", snapshot.text());
for _i in 0..operations {
- match rng.gen_range(0..100) {
+ match rng.random_range(0..100) {
0..=19 => {
- wrap_width = if rng.gen_bool(0.2) {
+ wrap_width = if rng.random_bool(0.2) {
None
} else {
- Some(px(rng.gen_range(0.0..=max_wrap_width)))
+ Some(px(rng.random_range(0.0..=max_wrap_width)))
};
log::info!("setting wrap width to {:?}", wrap_width);
map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
@@ -1634,28 +1631,27 @@ pub mod tests {
}
30..=44 => {
map.update(cx, |map, cx| {
- if rng.r#gen() || blocks.is_empty() {
+ if rng.random() || blocks.is_empty() {
let buffer = map.snapshot(cx).buffer_snapshot;
- let block_properties = (0..rng.gen_range(1..=1))
+ let block_properties = (0..rng.random_range(1..=1))
.map(|_| {
- let position =
- buffer.anchor_after(buffer.clip_offset(
- rng.gen_range(0..=buffer.len()),
- Bias::Left,
- ));
+ let position = buffer.anchor_after(buffer.clip_offset(
+ rng.random_range(0..=buffer.len()),
+ Bias::Left,
+ ));
- let placement = if rng.r#gen() {
+ let placement = if rng.random() {
BlockPlacement::Above(position)
} else {
BlockPlacement::Below(position)
};
- let height = rng.gen_range(1..5);
+ let height = rng.random_range(1..5);
log::info!(
"inserting block {:?} with height {}",
placement.as_ref().map(|p| p.to_point(&buffer)),
height
);
- let priority = rng.gen_range(1..100);
+ let priority = rng.random_range(1..100);
BlockProperties {
placement,
style: BlockStyle::Fixed,
@@ -1668,9 +1664,9 @@ pub mod tests {
blocks.extend(map.insert_blocks(block_properties, cx));
} else {
blocks.shuffle(&mut rng);
- let remove_count = rng.gen_range(1..=4.min(blocks.len()));
+ let remove_count = rng.random_range(1..=4.min(blocks.len()));
let block_ids_to_remove = (0..remove_count)
- .map(|_| blocks.remove(rng.gen_range(0..blocks.len())))
+ .map(|_| blocks.remove(rng.random_range(0..blocks.len())))
.collect();
log::info!("removing block ids {:?}", block_ids_to_remove);
map.remove_blocks(block_ids_to_remove, cx);
@@ -1679,16 +1675,16 @@ pub mod tests {
}
45..=79 => {
let mut ranges = Vec::new();
- for _ in 0..rng.gen_range(1..=3) {
+ for _ in 0..rng.random_range(1..=3) {
buffer.read_with(cx, |buffer, cx| {
let buffer = buffer.read(cx);
- let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
- let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
+ let end = buffer.clip_offset(rng.random_range(0..=buffer.len()), Right);
+ let start = buffer.clip_offset(rng.random_range(0..=end), Left);
ranges.push(start..end);
});
}
- if rng.r#gen() && fold_count > 0 {
+ if rng.random() && fold_count > 0 {
log::info!("unfolding ranges: {:?}", ranges);
map.update(cx, |map, cx| {
map.unfold_intersecting(ranges, true, cx);
@@ -1727,8 +1723,8 @@ pub mod tests {
// Line boundaries
let buffer = &snapshot.buffer_snapshot;
for _ in 0..5 {
- let row = rng.gen_range(0..=buffer.max_point().row);
- let column = rng.gen_range(0..=buffer.line_len(MultiBufferRow(row)));
+ let row = rng.random_range(0..=buffer.max_point().row);
+ let column = rng.random_range(0..=buffer.line_len(MultiBufferRow(row)));
let point = buffer.clip_point(Point::new(row, column), Left);
let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point);
@@ -1776,8 +1772,8 @@ pub mod tests {
let min_point = snapshot.clip_point(DisplayPoint::new(DisplayRow(0), 0), Left);
let max_point = snapshot.clip_point(snapshot.max_point(), Right);
for _ in 0..5 {
- let row = rng.gen_range(0..=snapshot.max_point().row().0);
- let column = rng.gen_range(0..=snapshot.line_len(DisplayRow(row)));
+ let row = rng.random_range(0..=snapshot.max_point().row().0);
+ let column = rng.random_range(0..=snapshot.line_len(DisplayRow(row)));
let point = snapshot.clip_point(DisplayPoint::new(DisplayRow(row), column), Left);
log::info!("Moving from point {:?}", point);
@@ -2351,11 +2347,12 @@ pub mod tests {
.highlight_style
.and_then(|style| style.color)
.map_or(black, |color| color.to_rgb());
- if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut() {
- if *last_severity == chunk.diagnostic_severity && *last_color == color {
- last_chunk.push_str(chunk.text);
- continue;
- }
+ if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut()
+ && *last_severity == chunk.diagnostic_severity
+ && *last_color == color
+ {
+ last_chunk.push_str(chunk.text);
+ continue;
}
chunks.push((chunk.text.to_string(), chunk.diagnostic_severity, color));
@@ -2609,7 +2606,7 @@ pub mod tests {
);
language.set_theme(&theme);
- let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false);
+ let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»«:» B = "c «d»""#, false);
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
@@ -2658,7 +2655,7 @@ pub mod tests {
[
("const ".to_string(), None, None),
("a".to_string(), None, Some(Hsla::blue())),
- (":".to_string(), Some(Hsla::red()), None),
+ (":".to_string(), Some(Hsla::red()), Some(Hsla::blue())),
(" B = ".to_string(), None, None),
("\"c ".to_string(), Some(Hsla::green()), None),
("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())),
@@ -2901,11 +2898,12 @@ pub mod tests {
.syntax_highlight_id
.and_then(|id| id.style(theme)?.color);
let highlight_color = chunk.highlight_style.and_then(|style| style.color);
- if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() {
- if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color {
- last_chunk.push_str(chunk.text);
- continue;
- }
+ if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut()
+ && syntax_color == *last_syntax_color
+ && highlight_color == *last_highlight_color
+ {
+ last_chunk.push_str(chunk.text);
+ continue;
}
chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
}
@@ -128,10 +128,10 @@ impl<T> BlockPlacement<T> {
}
}
- fn sort_order(&self) -> u8 {
+ fn tie_break(&self) -> u8 {
match self {
- BlockPlacement::Above(_) => 0,
- BlockPlacement::Replace(_) => 1,
+ BlockPlacement::Replace(_) => 0,
+ BlockPlacement::Above(_) => 1,
BlockPlacement::Near(_) => 2,
BlockPlacement::Below(_) => 3,
}
@@ -143,7 +143,7 @@ impl BlockPlacement<Anchor> {
self.start()
.cmp(other.start(), buffer)
.then_with(|| other.end().cmp(self.end(), buffer))
- .then_with(|| self.sort_order().cmp(&other.sort_order()))
+ .then_with(|| self.tie_break().cmp(&other.tie_break()))
}
fn to_wrap_row(&self, wrap_snapshot: &WrapSnapshot) -> Option<BlockPlacement<WrapRow>> {
@@ -290,7 +290,10 @@ pub enum Block {
ExcerptBoundary {
excerpt: ExcerptInfo,
height: u32,
- starts_new_buffer: bool,
+ },
+ BufferHeader {
+ excerpt: ExcerptInfo,
+ height: u32,
},
}
@@ -303,27 +306,37 @@ impl Block {
..
} => BlockId::ExcerptBoundary(next_excerpt.id),
Block::FoldedBuffer { first_excerpt, .. } => BlockId::FoldedBuffer(first_excerpt.id),
+ Block::BufferHeader {
+ excerpt: next_excerpt,
+ ..
+ } => BlockId::ExcerptBoundary(next_excerpt.id),
}
}
pub fn has_height(&self) -> bool {
match self {
Block::Custom(block) => block.height.is_some(),
- Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => true,
+ Block::ExcerptBoundary { .. }
+ | Block::FoldedBuffer { .. }
+ | Block::BufferHeader { .. } => true,
}
}
pub fn height(&self) -> u32 {
match self {
Block::Custom(block) => block.height.unwrap_or(0),
- Block::ExcerptBoundary { height, .. } | Block::FoldedBuffer { height, .. } => *height,
+ Block::ExcerptBoundary { height, .. }
+ | Block::FoldedBuffer { height, .. }
+ | Block::BufferHeader { height, .. } => *height,
}
}
pub fn style(&self) -> BlockStyle {
match self {
Block::Custom(block) => block.style,
- Block::ExcerptBoundary { .. } | Block::FoldedBuffer { .. } => BlockStyle::Sticky,
+ Block::ExcerptBoundary { .. }
+ | Block::FoldedBuffer { .. }
+ | Block::BufferHeader { .. } => BlockStyle::Sticky,
}
}
@@ -332,6 +345,7 @@ impl Block {
Block::Custom(block) => matches!(block.placement, BlockPlacement::Above(_)),
Block::FoldedBuffer { .. } => false,
Block::ExcerptBoundary { .. } => true,
+ Block::BufferHeader { .. } => true,
}
}
@@ -340,6 +354,7 @@ impl Block {
Block::Custom(block) => matches!(block.placement, BlockPlacement::Near(_)),
Block::FoldedBuffer { .. } => false,
Block::ExcerptBoundary { .. } => false,
+ Block::BufferHeader { .. } => false,
}
}
@@ -351,6 +366,7 @@ impl Block {
),
Block::FoldedBuffer { .. } => false,
Block::ExcerptBoundary { .. } => false,
+ Block::BufferHeader { .. } => false,
}
}
@@ -359,6 +375,7 @@ impl Block {
Block::Custom(block) => matches!(block.placement, BlockPlacement::Replace(_)),
Block::FoldedBuffer { .. } => true,
Block::ExcerptBoundary { .. } => false,
+ Block::BufferHeader { .. } => false,
}
}
@@ -367,6 +384,7 @@ impl Block {
Block::Custom(_) => false,
Block::FoldedBuffer { .. } => true,
Block::ExcerptBoundary { .. } => true,
+ Block::BufferHeader { .. } => true,
}
}
@@ -374,9 +392,8 @@ impl Block {
match self {
Block::Custom(_) => false,
Block::FoldedBuffer { .. } => true,
- Block::ExcerptBoundary {
- starts_new_buffer, ..
- } => *starts_new_buffer,
+ Block::ExcerptBoundary { .. } => false,
+ Block::BufferHeader { .. } => true,
}
}
}
@@ -393,14 +410,14 @@ impl Debug for Block {
.field("first_excerpt", &first_excerpt)
.field("height", height)
.finish(),
- Self::ExcerptBoundary {
- starts_new_buffer,
- excerpt,
- height,
- } => f
+ Self::ExcerptBoundary { excerpt, height } => f
.debug_struct("ExcerptBoundary")
.field("excerpt", excerpt)
- .field("starts_new_buffer", starts_new_buffer)
+ .field("height", height)
+ .finish(),
+ Self::BufferHeader { excerpt, height } => f
+ .debug_struct("BufferHeader")
+ .field("excerpt", excerpt)
.field("height", height)
.finish(),
}
@@ -525,26 +542,22 @@ impl BlockMap {
// * Below blocks that end at the start of the edit
// However, if we hit a replace block that ends at the start of the edit we want to reconstruct it.
new_transforms.append(cursor.slice(&old_start, Bias::Left), &());
- if let Some(transform) = cursor.item() {
- if transform.summary.input_rows > 0
- && cursor.end() == old_start
- && transform
- .block
- .as_ref()
- .map_or(true, |b| !b.is_replacement())
- {
- // Preserve the transform (push and next)
- new_transforms.push(transform.clone(), &());
- cursor.next();
+ if let Some(transform) = cursor.item()
+ && transform.summary.input_rows > 0
+ && cursor.end() == old_start
+ && transform.block.as_ref().is_none_or(|b| !b.is_replacement())
+ {
+ // Preserve the transform (push and next)
+ new_transforms.push(transform.clone(), &());
+ cursor.next();
- // Preserve below blocks at end of edit
- while let Some(transform) = cursor.item() {
- if transform.block.as_ref().map_or(false, |b| b.place_below()) {
- new_transforms.push(transform.clone(), &());
- cursor.next();
- } else {
- break;
- }
+ // Preserve below blocks at end of edit
+ while let Some(transform) = cursor.item() {
+ if transform.block.as_ref().is_some_and(|b| b.place_below()) {
+ new_transforms.push(transform.clone(), &());
+ cursor.next();
+ } else {
+ break;
}
}
}
@@ -607,7 +620,7 @@ impl BlockMap {
// Discard below blocks at the end of the edit. They'll be reconstructed.
while let Some(transform) = cursor.item() {
- if transform.block.as_ref().map_or(false, |b| b.place_below()) {
+ if transform.block.as_ref().is_some_and(|b| b.place_below()) {
cursor.next();
} else {
break;
@@ -657,22 +670,20 @@ impl BlockMap {
.iter()
.filter_map(|block| {
let placement = block.placement.to_wrap_row(wrap_snapshot)?;
- if let BlockPlacement::Above(row) = placement {
- if row < new_start {
- return None;
- }
+ if let BlockPlacement::Above(row) = placement
+ && row < new_start
+ {
+ return None;
}
Some((placement, Block::Custom(block.clone())))
}),
);
- if buffer.show_headers() {
- blocks_in_edit.extend(self.header_and_footer_blocks(
- buffer,
- (start_bound, end_bound),
- wrap_snapshot,
- ));
- }
+ blocks_in_edit.extend(self.header_and_footer_blocks(
+ buffer,
+ (start_bound, end_bound),
+ wrap_snapshot,
+ ));
BlockMap::sort_blocks(&mut blocks_in_edit);
@@ -775,7 +786,7 @@ impl BlockMap {
if self.buffers_with_disabled_headers.contains(&new_buffer_id) {
continue;
}
- if self.folded_buffers.contains(&new_buffer_id) {
+ if self.folded_buffers.contains(&new_buffer_id) && buffer.show_headers() {
let mut last_excerpt_end_row = first_excerpt.end_row;
while let Some(next_boundary) = boundaries.peek() {
@@ -808,20 +819,24 @@ impl BlockMap {
}
}
- if new_buffer_id.is_some() {
+ let starts_new_buffer = new_buffer_id.is_some();
+ let block = if starts_new_buffer && buffer.show_headers() {
height += self.buffer_header_height;
- } else {
+ Block::BufferHeader {
+ excerpt: excerpt_boundary.next,
+ height,
+ }
+ } else if excerpt_boundary.prev.is_some() {
height += self.excerpt_header_height;
- }
-
- return Some((
- BlockPlacement::Above(WrapRow(wrap_row)),
Block::ExcerptBoundary {
excerpt: excerpt_boundary.next,
height,
- starts_new_buffer: new_buffer_id.is_some(),
- },
- ));
+ }
+ } else {
+ continue;
+ };
+
+ return Some((BlockPlacement::Above(WrapRow(wrap_row)), block));
}
})
}
@@ -832,6 +847,7 @@ impl BlockMap {
.start()
.cmp(placement_b.start())
.then_with(|| placement_b.end().cmp(placement_a.end()))
+ .then_with(|| placement_a.tie_break().cmp(&placement_b.tie_break()))
.then_with(|| {
if block_a.is_header() {
Ordering::Less
@@ -841,18 +857,29 @@ impl BlockMap {
Ordering::Equal
}
})
- .then_with(|| placement_a.sort_order().cmp(&placement_b.sort_order()))
.then_with(|| match (block_a, block_b) {
(
Block::ExcerptBoundary {
excerpt: excerpt_a, ..
+ }
+ | Block::BufferHeader {
+ excerpt: excerpt_a, ..
},
Block::ExcerptBoundary {
excerpt: excerpt_b, ..
+ }
+ | Block::BufferHeader {
+ excerpt: excerpt_b, ..
},
) => Some(excerpt_a.id).cmp(&Some(excerpt_b.id)),
- (Block::ExcerptBoundary { .. }, Block::Custom(_)) => Ordering::Less,
- (Block::Custom(_), Block::ExcerptBoundary { .. }) => Ordering::Greater,
+ (
+ Block::ExcerptBoundary { .. } | Block::BufferHeader { .. },
+ Block::Custom(_),
+ ) => Ordering::Less,
+ (
+ Block::Custom(_),
+ Block::ExcerptBoundary { .. } | Block::BufferHeader { .. },
+ ) => Ordering::Greater,
(Block::Custom(block_a), Block::Custom(block_b)) => block_a
.priority
.cmp(&block_b.priority)
@@ -977,10 +1004,10 @@ impl BlockMapReader<'_> {
break;
}
- if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id()) {
- if id == block_id {
- return Some(cursor.start().1);
- }
+ if let Some(BlockId::Custom(id)) = transform.block.as_ref().map(|block| block.id())
+ && id == block_id
+ {
+ return Some(cursor.start().1);
}
cursor.next();
}
@@ -1299,14 +1326,14 @@ impl BlockSnapshot {
let mut input_start = transform_input_start;
let mut input_end = transform_input_start;
- if let Some(transform) = cursor.item() {
- if transform.block.is_none() {
- input_start += rows.start - transform_output_start;
- input_end += cmp::min(
- rows.end - transform_output_start,
- transform.summary.input_rows,
- );
- }
+ if let Some(transform) = cursor.item()
+ && transform.block.is_none()
+ {
+ input_start += rows.start - transform_output_start;
+ input_end += cmp::min(
+ rows.end - transform_output_start,
+ transform.summary.input_rows,
+ );
}
BlockChunks {
@@ -1329,7 +1356,7 @@ impl BlockSnapshot {
let Dimensions(output_start, input_start, _) = cursor.start();
let overshoot = if cursor
.item()
- .map_or(false, |transform| transform.block.is_none())
+ .is_some_and(|transform| transform.block.is_none())
{
start_row.0 - output_start.0
} else {
@@ -1359,7 +1386,7 @@ impl BlockSnapshot {
&& transform
.block
.as_ref()
- .map_or(false, |block| block.height() > 0))
+ .is_some_and(|block| block.height() > 0))
{
break;
}
@@ -1381,7 +1408,9 @@ impl BlockSnapshot {
while let Some(transform) = cursor.item() {
match &transform.block {
- Some(Block::ExcerptBoundary { excerpt, .. }) => {
+ Some(
+ Block::ExcerptBoundary { excerpt, .. } | Block::BufferHeader { excerpt, .. },
+ ) => {
return Some(StickyHeaderExcerpt { excerpt });
}
Some(block) if block.is_buffer_header() => return None,
@@ -1472,18 +1501,18 @@ impl BlockSnapshot {
longest_row_chars = summary.longest_row_chars;
}
- if let Some(transform) = cursor.item() {
- if transform.block.is_none() {
- let Dimensions(output_start, input_start, _) = cursor.start();
- let overshoot = range.end.0 - output_start.0;
- let wrap_start_row = input_start.0;
- let wrap_end_row = input_start.0 + overshoot;
- let summary = self
- .wrap_snapshot
- .text_summary_for_range(wrap_start_row..wrap_end_row);
- if summary.longest_row_chars > longest_row_chars {
- longest_row = BlockRow(output_start.0 + summary.longest_row);
- }
+ if let Some(transform) = cursor.item()
+ && transform.block.is_none()
+ {
+ let Dimensions(output_start, input_start, _) = cursor.start();
+ let overshoot = range.end.0 - output_start.0;
+ let wrap_start_row = input_start.0;
+ let wrap_end_row = input_start.0 + overshoot;
+ let summary = self
+ .wrap_snapshot
+ .text_summary_for_range(wrap_start_row..wrap_end_row);
+ if summary.longest_row_chars > longest_row_chars {
+ longest_row = BlockRow(output_start.0 + summary.longest_row);
}
}
}
@@ -1512,7 +1541,7 @@ impl BlockSnapshot {
pub(super) fn is_block_line(&self, row: BlockRow) -> bool {
let mut cursor = self.transforms.cursor::<Dimensions<BlockRow, WrapRow>>(&());
cursor.seek(&row, Bias::Right);
- cursor.item().map_or(false, |t| t.block.is_some())
+ cursor.item().is_some_and(|t| t.block.is_some())
}
pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool {
@@ -1530,11 +1559,11 @@ impl BlockSnapshot {
.make_wrap_point(Point::new(row.0, 0), Bias::Left);
let mut cursor = self.transforms.cursor::<Dimensions<WrapRow, BlockRow>>(&());
cursor.seek(&WrapRow(wrap_point.row()), Bias::Right);
- cursor.item().map_or(false, |transform| {
+ cursor.item().is_some_and(|transform| {
transform
.block
.as_ref()
- .map_or(false, |block| block.is_replacement())
+ .is_some_and(|block| block.is_replacement())
})
}
@@ -1557,12 +1586,11 @@ impl BlockSnapshot {
match transform.block.as_ref() {
Some(block) => {
- if block.is_replacement() {
- if ((bias == Bias::Left || search_left) && output_start <= point.0)
- || (!search_left && output_start >= point.0)
- {
- return BlockPoint(output_start);
- }
+ if block.is_replacement()
+ && (((bias == Bias::Left || search_left) && output_start <= point.0)
+ || (!search_left && output_start >= point.0))
+ {
+ return BlockPoint(output_start);
}
}
None => {
@@ -1655,7 +1683,7 @@ impl BlockChunks<'_> {
if transform
.block
.as_ref()
- .map_or(false, |block| block.height() == 0)
+ .is_some_and(|block| block.height() == 0)
{
self.transforms.next();
} else {
@@ -1666,7 +1694,7 @@ impl BlockChunks<'_> {
if self
.transforms
.item()
- .map_or(false, |transform| transform.block.is_none())
+ .is_some_and(|transform| transform.block.is_none())
{
let start_input_row = self.transforms.start().1.0;
let start_output_row = self.transforms.start().0.0;
@@ -1709,6 +1737,7 @@ impl<'a> Iterator for BlockChunks<'a> {
return Some(Chunk {
text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) },
+ chars: (1 << line_count) - 1,
..Default::default()
});
}
@@ -1738,17 +1767,26 @@ impl<'a> Iterator for BlockChunks<'a> {
let (mut prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
self.input_chunk.text = suffix;
+ self.input_chunk.tabs >>= prefix_bytes.saturating_sub(1);
+ self.input_chunk.chars >>= prefix_bytes.saturating_sub(1);
+
+ let mut tabs = self.input_chunk.tabs;
+ let mut chars = self.input_chunk.chars;
if self.masked {
// Not great for multibyte text because to keep cursor math correct we
// need to have the same number of bytes in the input as output.
- let chars = prefix.chars().count();
- let bullet_len = chars;
+ let chars_count = prefix.chars().count();
+ let bullet_len = chars_count;
prefix = &BULLETS[..bullet_len];
+ chars = (1 << bullet_len) - 1;
+ tabs = 0;
}
let chunk = Chunk {
text: prefix,
+ tabs,
+ chars,
..self.input_chunk.clone()
};
@@ -1776,7 +1814,7 @@ impl Iterator for BlockRows<'_> {
if transform
.block
.as_ref()
- .map_or(false, |block| block.height() == 0)
+ .is_some_and(|block| block.height() == 0)
{
self.transforms.next();
} else {
@@ -1788,7 +1826,7 @@ impl Iterator for BlockRows<'_> {
if transform
.block
.as_ref()
- .map_or(true, |block| block.is_replacement())
+ .is_none_or(|block| block.is_replacement())
{
self.input_rows.seek(self.transforms.start().1.0);
}
@@ -2161,7 +2199,7 @@ mod tests {
}
let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
- let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone());
+ let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot);
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx);
@@ -2280,7 +2318,7 @@ mod tests {
new_heights.insert(block_ids[0], 3);
block_map_writer.resize(new_heights);
- let snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
+ let snapshot = block_map.read(wraps_snapshot, Default::default());
// Same height as before, should remain the same
assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n");
}
@@ -2365,16 +2403,14 @@ mod tests {
buffer.edit([(Point::new(2, 0)..Point::new(3, 0), "")], None, cx);
buffer.snapshot(cx)
});
- let (inlay_snapshot, inlay_edits) = inlay_map.sync(
- buffer_snapshot.clone(),
- buffer_subscription.consume().into_inner(),
- );
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot, buffer_subscription.consume().into_inner());
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
wrap_map.sync(tab_snapshot, tab_edits, cx)
});
- let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
+ let blocks_snapshot = block_map.read(wraps_snapshot, wrap_edits);
assert_eq!(blocks_snapshot.text(), "line1\n\n\n\n\nline5");
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
@@ -2459,7 +2495,7 @@ mod tests {
// Removing the replace block shows all the hidden blocks again.
let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
writer.remove(HashSet::from_iter([replace_block_id]));
- let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
+ let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
assert_eq!(
blocks_snapshot.text(),
"\nline1\n\nline2\n\n\nline 2.1\nline2.2\nline 2.3\nline 2.4\n\nline4\n\nline5"
@@ -2798,7 +2834,7 @@ mod tests {
buffer.read_with(cx, |buffer, cx| {
writer.fold_buffers([buffer_id_3], buffer, cx);
});
- let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+ let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default());
let blocks = blocks_snapshot
.blocks_in_range(0..u32::MAX)
.collect::<Vec<_>>();
@@ -2851,7 +2887,7 @@ mod tests {
assert_eq!(buffer_ids.len(), 1);
let buffer_id = buffer_ids[0];
- let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
let (_, wrap_snapshot) =
@@ -2865,7 +2901,7 @@ mod tests {
buffer.read_with(cx, |buffer, cx| {
writer.fold_buffers([buffer_id], buffer, cx);
});
- let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default());
+ let blocks_snapshot = block_map.read(wrap_snapshot, Patch::default());
let blocks = blocks_snapshot
.blocks_in_range(0..u32::MAX)
.collect::<Vec<_>>();
@@ -2873,12 +2909,7 @@ mod tests {
1,
blocks
.iter()
- .filter(|(_, block)| {
- match block {
- Block::FoldedBuffer { .. } => true,
- _ => false,
- }
- })
+ .filter(|(_, block)| { matches!(block, Block::FoldedBuffer { .. }) })
.count(),
"Should have one folded block, producing a header of the second buffer"
);
@@ -2901,21 +2932,21 @@ mod tests {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
- let wrap_width = if rng.gen_bool(0.2) {
+ let wrap_width = if rng.random_bool(0.2) {
None
} else {
- Some(px(rng.gen_range(0.0..=100.0)))
+ Some(px(rng.random_range(0.0..=100.0)))
};
let tab_size = 1.try_into().unwrap();
let font_size = px(14.0);
- let buffer_start_header_height = rng.gen_range(1..=5);
- let excerpt_header_height = rng.gen_range(1..=5);
+ let buffer_start_header_height = rng.random_range(1..=5);
+ let excerpt_header_height = rng.random_range(1..=5);
log::info!("Wrap width: {:?}", wrap_width);
log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
- let is_singleton = rng.r#gen();
+ let is_singleton = rng.random();
let buffer = if is_singleton {
- let len = rng.gen_range(0..10);
+ let len = rng.random_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
log::info!("initial singleton buffer text: {:?}", text);
cx.update(|cx| MultiBuffer::build_simple(&text, cx))
@@ -2945,30 +2976,30 @@ mod tests {
for _ in 0..operations {
let mut buffer_edits = Vec::new();
- match rng.gen_range(0..=100) {
+ match rng.random_range(0..=100) {
0..=19 => {
- let wrap_width = if rng.gen_bool(0.2) {
+ let wrap_width = if rng.random_bool(0.2) {
None
} else {
- Some(px(rng.gen_range(0.0..=100.0)))
+ Some(px(rng.random_range(0.0..=100.0)))
};
log::info!("Setting wrap width to {:?}", wrap_width);
wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
}
20..=39 => {
- let block_count = rng.gen_range(1..=5);
+ let block_count = rng.random_range(1..=5);
let block_properties = (0..block_count)
.map(|_| {
let buffer = cx.update(|cx| buffer.read(cx).read(cx).clone());
let offset =
- buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left);
+ buffer.clip_offset(rng.random_range(0..=buffer.len()), Bias::Left);
let mut min_height = 0;
- let placement = match rng.gen_range(0..3) {
+ let placement = match rng.random_range(0..3) {
0 => {
min_height = 1;
let start = buffer.anchor_after(offset);
let end = buffer.anchor_after(buffer.clip_offset(
- rng.gen_range(offset..=buffer.len()),
+ rng.random_range(offset..=buffer.len()),
Bias::Left,
));
BlockPlacement::Replace(start..=end)
@@ -2977,7 +3008,7 @@ mod tests {
_ => BlockPlacement::Below(buffer.anchor_after(offset)),
};
- let height = rng.gen_range(min_height..5);
+ let height = rng.random_range(min_height..5);
BlockProperties {
style: BlockStyle::Fixed,
placement,
@@ -3019,7 +3050,7 @@ mod tests {
}
}
40..=59 if !block_map.custom_blocks.is_empty() => {
- let block_count = rng.gen_range(1..=4.min(block_map.custom_blocks.len()));
+ let block_count = rng.random_range(1..=4.min(block_map.custom_blocks.len()));
let block_ids_to_remove = block_map
.custom_blocks
.choose_multiple(&mut rng, block_count)
@@ -3074,8 +3105,8 @@ mod tests {
let mut folded_count = folded_buffers.len();
let mut unfolded_count = unfolded_buffers.len();
- let fold = !unfolded_buffers.is_empty() && rng.gen_bool(0.5);
- let unfold = !folded_buffers.is_empty() && rng.gen_bool(0.5);
+ let fold = !unfolded_buffers.is_empty() && rng.random_bool(0.5);
+ let unfold = !folded_buffers.is_empty() && rng.random_bool(0.5);
if !fold && !unfold {
log::info!(
"Noop fold/unfold operation. Unfolded buffers: {unfolded_count}, folded buffers: {folded_count}"
@@ -3086,7 +3117,7 @@ mod tests {
buffer.update(cx, |buffer, cx| {
if fold {
let buffer_to_fold =
- unfolded_buffers[rng.gen_range(0..unfolded_buffers.len())];
+ unfolded_buffers[rng.random_range(0..unfolded_buffers.len())];
log::info!("Folding {buffer_to_fold:?}");
let related_excerpts = buffer_snapshot
.excerpts()
@@ -3112,7 +3143,7 @@ mod tests {
}
if unfold {
let buffer_to_unfold =
- folded_buffers[rng.gen_range(0..folded_buffers.len())];
+ folded_buffers[rng.random_range(0..folded_buffers.len())];
log::info!("Unfolding {buffer_to_unfold:?}");
unfolded_count += 1;
folded_count -= 1;
@@ -3125,7 +3156,7 @@ mod tests {
}
_ => {
buffer.update(cx, |buffer, cx| {
- let mutation_count = rng.gen_range(1..=5);
+ let mutation_count = rng.random_range(1..=5);
let subscription = buffer.subscribe();
buffer.randomly_mutate(&mut rng, mutation_count, cx);
buffer_snapshot = buffer.snapshot(cx);
@@ -3195,9 +3226,9 @@ mod tests {
// so we special case row 0 to assume a leading '\n'.
//
// Linehood is the birthright of strings.
- let mut input_text_lines = input_text.split('\n').enumerate().peekable();
+ let input_text_lines = input_text.split('\n').enumerate().peekable();
let mut block_row = 0;
- while let Some((wrap_row, input_line)) = input_text_lines.next() {
+ for (wrap_row, input_line) in input_text_lines {
let wrap_row = wrap_row as u32;
let multibuffer_row = wraps_snapshot
.to_point(WrapPoint::new(wrap_row, 0), Bias::Left)
@@ -3228,34 +3259,32 @@ mod tests {
let mut is_in_replace_block = false;
if let Some((BlockPlacement::Replace(replace_range), block)) =
sorted_blocks_iter.peek()
+ && wrap_row >= replace_range.start().0
{
- if wrap_row >= replace_range.start().0 {
- is_in_replace_block = true;
+ is_in_replace_block = true;
- if wrap_row == replace_range.start().0 {
- if matches!(block, Block::FoldedBuffer { .. }) {
- expected_buffer_rows.push(None);
- } else {
- expected_buffer_rows
- .push(input_buffer_rows[multibuffer_row as usize]);
- }
+ if wrap_row == replace_range.start().0 {
+ if matches!(block, Block::FoldedBuffer { .. }) {
+ expected_buffer_rows.push(None);
+ } else {
+ expected_buffer_rows.push(input_buffer_rows[multibuffer_row as usize]);
}
+ }
- if wrap_row == replace_range.end().0 {
- expected_block_positions.push((block_row, block.id()));
- let text = "\n".repeat((block.height() - 1) as usize);
- if block_row > 0 {
- expected_text.push('\n');
- }
- expected_text.push_str(&text);
-
- for _ in 1..block.height() {
- expected_buffer_rows.push(None);
- }
- block_row += block.height();
+ if wrap_row == replace_range.end().0 {
+ expected_block_positions.push((block_row, block.id()));
+ let text = "\n".repeat((block.height() - 1) as usize);
+ if block_row > 0 {
+ expected_text.push('\n');
+ }
+ expected_text.push_str(&text);
- sorted_blocks_iter.next();
+ for _ in 1..block.height() {
+ expected_buffer_rows.push(None);
}
+ block_row += block.height();
+
+ sorted_blocks_iter.next();
}
}
@@ -3312,7 +3341,7 @@ mod tests {
);
for start_row in 0..expected_row_count {
- let end_row = rng.gen_range(start_row + 1..=expected_row_count);
+ let end_row = rng.random_range(start_row + 1..=expected_row_count);
let mut expected_text = expected_lines[start_row..end_row].join("\n");
if end_row < expected_row_count {
expected_text.push('\n');
@@ -3407,8 +3436,8 @@ mod tests {
);
for _ in 0..10 {
- let end_row = rng.gen_range(1..=expected_lines.len());
- let start_row = rng.gen_range(0..end_row);
+ let end_row = rng.random_range(1..=expected_lines.len());
+ let start_row = rng.random_range(0..end_row);
let mut expected_longest_rows_in_range = vec![];
let mut longest_line_len_in_range = 0;
@@ -3539,7 +3568,7 @@ mod tests {
..buffer_snapshot.anchor_after(Point::new(1, 0))],
false,
);
- let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default());
+ let blocks_snapshot = block_map.read(wraps_snapshot, Default::default());
assert_eq!(blocks_snapshot.text(), "abc\n\ndef\nghi\njkl\nmno");
}
@@ -25,9 +25,8 @@ pub struct CustomHighlightsChunks<'a> {
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
struct HighlightEndpoint {
offset: usize,
- is_start: bool,
tag: HighlightKey,
- style: HighlightStyle,
+ style: Option<HighlightStyle>,
}
impl<'a> CustomHighlightsChunks<'a> {
@@ -77,7 +76,7 @@ fn create_highlight_endpoints(
let ranges = &text_highlights.1;
let start_ix = match ranges.binary_search_by(|probe| {
- let cmp = probe.end.cmp(&start, &buffer);
+ let cmp = probe.end.cmp(&start, buffer);
if cmp.is_gt() {
cmp::Ordering::Greater
} else {
@@ -88,21 +87,24 @@ fn create_highlight_endpoints(
};
for range in &ranges[start_ix..] {
- if range.start.cmp(&end, &buffer).is_ge() {
+ if range.start.cmp(&end, buffer).is_ge() {
break;
}
+ let start = range.start.to_offset(buffer);
+ let end = range.end.to_offset(buffer);
+ if start == end {
+ continue;
+ }
highlight_endpoints.push(HighlightEndpoint {
- offset: range.start.to_offset(&buffer),
- is_start: true,
+ offset: start,
tag,
- style,
+ style: Some(style),
});
highlight_endpoints.push(HighlightEndpoint {
- offset: range.end.to_offset(&buffer),
- is_start: false,
+ offset: end,
tag,
- style,
+ style: None,
});
}
}
@@ -118,8 +120,8 @@ impl<'a> Iterator for CustomHighlightsChunks<'a> {
let mut next_highlight_endpoint = usize::MAX;
while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
if endpoint.offset <= self.offset {
- if endpoint.is_start {
- self.active_highlights.insert(endpoint.tag, endpoint.style);
+ if let Some(style) = endpoint.style {
+ self.active_highlights.insert(endpoint.tag, style);
} else {
self.active_highlights.remove(&endpoint.tag);
}
@@ -132,27 +134,41 @@ impl<'a> Iterator for CustomHighlightsChunks<'a> {
let chunk = self
.buffer_chunk
- .get_or_insert_with(|| self.buffer_chunks.next().unwrap());
+ .get_or_insert_with(|| self.buffer_chunks.next().unwrap_or_default());
if chunk.text.is_empty() {
- *chunk = self.buffer_chunks.next().unwrap();
+ *chunk = self.buffer_chunks.next()?;
}
- let (prefix, suffix) = chunk
- .text
- .split_at(chunk.text.len().min(next_highlight_endpoint - self.offset));
+ let split_idx = chunk.text.len().min(next_highlight_endpoint - self.offset);
+ let (prefix, suffix) = chunk.text.split_at(split_idx);
+
+ let (chars, tabs) = if split_idx == 128 {
+ let output = (chunk.chars, chunk.tabs);
+ chunk.chars = 0;
+ chunk.tabs = 0;
+ output
+ } else {
+ let mask = (1 << split_idx) - 1;
+ let output = (chunk.chars & mask, chunk.tabs & mask);
+ chunk.chars = chunk.chars >> split_idx;
+ chunk.tabs = chunk.tabs >> split_idx;
+ output
+ };
chunk.text = suffix;
self.offset += prefix.len();
let mut prefix = Chunk {
text: prefix,
+ chars,
+ tabs,
..chunk.clone()
};
if !self.active_highlights.is_empty() {
- let mut highlight_style = HighlightStyle::default();
- for active_highlight in self.active_highlights.values() {
- highlight_style.highlight(*active_highlight);
- }
- prefix.highlight_style = Some(highlight_style);
+ prefix.highlight_style = self
+ .active_highlights
+ .values()
+ .copied()
+ .reduce(|acc, active_highlight| acc.highlight(active_highlight));
}
Some(prefix)
}
@@ -168,6 +184,143 @@ impl Ord for HighlightEndpoint {
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.offset
.cmp(&other.offset)
- .then_with(|| other.is_start.cmp(&self.is_start))
+ .then_with(|| self.style.is_some().cmp(&other.style.is_some()))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{any::TypeId, sync::Arc};
+
+ use super::*;
+ use crate::MultiBuffer;
+ use gpui::App;
+ use rand::prelude::*;
+ use util::RandomCharIter;
+
+ #[gpui::test(iterations = 100)]
+ fn test_random_chunk_bitmaps(cx: &mut App, mut rng: StdRng) {
+ // Generate random buffer using existing test infrastructure
+ let len = rng.random_range(10..10000);
+ let buffer = if rng.random() {
+ let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+ MultiBuffer::build_simple(&text, cx)
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ };
+
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+
+ // Create random highlights
+ let mut highlights = sum_tree::TreeMap::default();
+ let highlight_count = rng.random_range(1..10);
+
+ for _i in 0..highlight_count {
+ let style = HighlightStyle {
+ color: Some(gpui::Hsla {
+ h: rng.random::<f32>(),
+ s: rng.random::<f32>(),
+ l: rng.random::<f32>(),
+ a: 1.0,
+ }),
+ ..Default::default()
+ };
+
+ let mut ranges = Vec::new();
+ let range_count = rng.random_range(1..10);
+ let text = buffer_snapshot.text();
+ for _ in 0..range_count {
+ if buffer_snapshot.len() == 0 {
+ continue;
+ }
+
+ let mut start = rng.random_range(0..=buffer_snapshot.len().saturating_sub(10));
+
+ while !text.is_char_boundary(start) {
+ start = start.saturating_sub(1);
+ }
+
+ let end_end = buffer_snapshot.len().min(start + 100);
+ let mut end = rng.random_range(start..=end_end);
+ while !text.is_char_boundary(end) {
+ end = end.saturating_sub(1);
+ }
+
+ if start < end {
+ start = end;
+ }
+ let start_anchor = buffer_snapshot.anchor_before(start);
+ let end_anchor = buffer_snapshot.anchor_after(end);
+ ranges.push(start_anchor..end_anchor);
+ }
+
+ let type_id = TypeId::of::<()>(); // Simple type ID for testing
+ highlights.insert(HighlightKey::Type(type_id), Arc::new((style, ranges)));
+ }
+
+ // Get all chunks and verify their bitmaps
+ let chunks =
+ CustomHighlightsChunks::new(0..buffer_snapshot.len(), false, None, &buffer_snapshot);
+
+ for chunk in chunks {
+ let chunk_text = chunk.text;
+ let chars_bitmap = chunk.chars;
+ let tabs_bitmap = chunk.tabs;
+
+ // Check empty chunks have empty bitmaps
+ if chunk_text.is_empty() {
+ assert_eq!(
+ chars_bitmap, 0,
+ "Empty chunk should have empty chars bitmap"
+ );
+ assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
+ continue;
+ }
+
+ // Verify that chunk text doesn't exceed 128 bytes
+ assert!(
+ chunk_text.len() <= 128,
+ "Chunk text length {} exceeds 128 bytes",
+ chunk_text.len()
+ );
+
+ // Verify chars bitmap
+ let char_indices = chunk_text
+ .char_indices()
+ .map(|(i, _)| i)
+ .collect::<Vec<_>>();
+
+ for byte_idx in 0..chunk_text.len() {
+ let should_have_bit = char_indices.contains(&byte_idx);
+ let has_bit = chars_bitmap & (1 << byte_idx) != 0;
+
+ if has_bit != should_have_bit {
+ eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
+ eprintln!("Char indices: {:?}", char_indices);
+ eprintln!("Chars bitmap: {:#b}", chars_bitmap);
+ assert_eq!(
+ has_bit, should_have_bit,
+ "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, should_have_bit, has_bit
+ );
+ }
+ }
+
+ // Verify tabs bitmap
+ for (byte_idx, byte) in chunk_text.bytes().enumerate() {
+ let is_tab = byte == b'\t';
+ let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
+
+ if has_bit != is_tab {
+ eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
+ eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
+ assert_eq!(
+ has_bit, is_tab,
+ "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, byte as char, is_tab, has_bit
+ );
+ }
+ }
+ }
}
}
@@ -289,25 +289,25 @@ impl FoldMapWriter<'_> {
let ChunkRendererId::Fold(id) = id else {
continue;
};
- if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() {
- if Some(new_width) != metadata.width {
- let buffer_start = metadata.range.start.to_offset(buffer);
- let buffer_end = metadata.range.end.to_offset(buffer);
- let inlay_range = inlay_snapshot.to_inlay_offset(buffer_start)
- ..inlay_snapshot.to_inlay_offset(buffer_end);
- edits.push(InlayEdit {
- old: inlay_range.clone(),
- new: inlay_range.clone(),
- });
+ if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned()
+ && Some(new_width) != metadata.width
+ {
+ let buffer_start = metadata.range.start.to_offset(buffer);
+ let buffer_end = metadata.range.end.to_offset(buffer);
+ let inlay_range = inlay_snapshot.to_inlay_offset(buffer_start)
+ ..inlay_snapshot.to_inlay_offset(buffer_end);
+ edits.push(InlayEdit {
+ old: inlay_range.clone(),
+ new: inlay_range.clone(),
+ });
- self.0.snapshot.fold_metadata_by_id.insert(
- id,
- FoldMetadata {
- range: metadata.range,
- width: Some(new_width),
- },
- );
- }
+ self.0.snapshot.fold_metadata_by_id.insert(
+ id,
+ FoldMetadata {
+ range: metadata.range,
+ width: Some(new_width),
+ },
+ );
}
}
@@ -320,13 +320,13 @@ impl FoldMapWriter<'_> {
/// Decides where the fold indicators should be; also tracks parts of a source file that are currently folded.
///
/// See the [`display_map` module documentation](crate::display_map) for more information.
-pub(crate) struct FoldMap {
+pub struct FoldMap {
snapshot: FoldSnapshot,
next_fold_id: FoldId,
}
impl FoldMap {
- pub(crate) fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
+ pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
let this = Self {
snapshot: FoldSnapshot {
folds: SumTree::new(&inlay_snapshot.buffer),
@@ -360,7 +360,7 @@ impl FoldMap {
(self.snapshot.clone(), edits)
}
- pub fn write(
+ pub(crate) fn write(
&mut self,
inlay_snapshot: InlaySnapshot,
edits: Vec<InlayEdit>,
@@ -417,18 +417,18 @@ impl FoldMap {
cursor.seek(&InlayOffset(0), Bias::Right);
while let Some(mut edit) = inlay_edits_iter.next() {
- if let Some(item) = cursor.item() {
- if !item.is_fold() {
- new_transforms.update_last(
- |transform| {
- if !transform.is_fold() {
- transform.summary.add_summary(&item.summary, &());
- cursor.next();
- }
- },
- &(),
- );
- }
+ if let Some(item) = cursor.item()
+ && !item.is_fold()
+ {
+ new_transforms.update_last(
+ |transform| {
+ if !transform.is_fold() {
+ transform.summary.add_summary(&item.summary, &());
+ cursor.next();
+ }
+ },
+ &(),
+ );
}
new_transforms.append(cursor.slice(&edit.old.start, Bias::Left), &());
edit.new.start -= edit.old.start - *cursor.start();
@@ -491,14 +491,14 @@ impl FoldMap {
while folds
.peek()
- .map_or(false, |(_, fold_range)| fold_range.start < edit.new.end)
+ .is_some_and(|(_, fold_range)| fold_range.start < edit.new.end)
{
let (fold, mut fold_range) = folds.next().unwrap();
let sum = new_transforms.summary();
assert!(fold_range.start.0 >= sum.input.len);
- while folds.peek().map_or(false, |(next_fold, next_fold_range)| {
+ while folds.peek().is_some_and(|(next_fold, next_fold_range)| {
next_fold_range.start < fold_range.end
|| (next_fold_range.start == fold_range.end
&& fold.placeholder.merge_adjacent
@@ -529,6 +529,7 @@ impl FoldMap {
},
placeholder: Some(TransformPlaceholder {
text: ELLIPSIS,
+ chars: 1,
renderer: ChunkRenderer {
id: ChunkRendererId::Fold(fold.id),
render: Arc::new(move |cx| {
@@ -575,14 +576,14 @@ impl FoldMap {
for mut edit in inlay_edits {
old_transforms.seek(&edit.old.start, Bias::Left);
- if old_transforms.item().map_or(false, |t| t.is_fold()) {
+ if old_transforms.item().is_some_and(|t| t.is_fold()) {
edit.old.start = old_transforms.start().0;
}
let old_start =
old_transforms.start().1.0 + (edit.old.start - old_transforms.start().0).0;
old_transforms.seek_forward(&edit.old.end, Bias::Right);
- if old_transforms.item().map_or(false, |t| t.is_fold()) {
+ if old_transforms.item().is_some_and(|t| t.is_fold()) {
old_transforms.next();
edit.old.end = old_transforms.start().0;
}
@@ -590,14 +591,14 @@ impl FoldMap {
old_transforms.start().1.0 + (edit.old.end - old_transforms.start().0).0;
new_transforms.seek(&edit.new.start, Bias::Left);
- if new_transforms.item().map_or(false, |t| t.is_fold()) {
+ if new_transforms.item().is_some_and(|t| t.is_fold()) {
edit.new.start = new_transforms.start().0;
}
let new_start =
new_transforms.start().1.0 + (edit.new.start - new_transforms.start().0).0;
new_transforms.seek_forward(&edit.new.end, Bias::Right);
- if new_transforms.item().map_or(false, |t| t.is_fold()) {
+ if new_transforms.item().is_some_and(|t| t.is_fold()) {
new_transforms.next();
edit.new.end = new_transforms.start().0;
}
@@ -709,7 +710,7 @@ impl FoldSnapshot {
.transforms
.cursor::<Dimensions<InlayPoint, FoldPoint>>(&());
cursor.seek(&point, Bias::Right);
- if cursor.item().map_or(false, |t| t.is_fold()) {
+ if cursor.item().is_some_and(|t| t.is_fold()) {
if bias == Bias::Left || point == cursor.start().0 {
cursor.start().1
} else {
@@ -788,7 +789,7 @@ impl FoldSnapshot {
let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
let mut cursor = self.transforms.cursor::<InlayOffset>(&());
cursor.seek(&inlay_offset, Bias::Right);
- cursor.item().map_or(false, |t| t.placeholder.is_some())
+ cursor.item().is_some_and(|t| t.placeholder.is_some())
}
pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
@@ -839,7 +840,7 @@ impl FoldSnapshot {
let inlay_end = if transform_cursor
.item()
- .map_or(true, |transform| transform.is_fold())
+ .is_none_or(|transform| transform.is_fold())
{
inlay_start
} else if range.end < transform_end.0 {
@@ -872,6 +873,14 @@ impl FoldSnapshot {
.flat_map(|chunk| chunk.text.chars())
}
+ pub fn chunks_at(&self, start: FoldPoint) -> FoldChunks<'_> {
+ self.chunks(
+ start.to_offset(self)..self.len(),
+ false,
+ Highlights::default(),
+ )
+ }
+
#[cfg(test)]
pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset {
if offset > self.len() {
@@ -1034,6 +1043,7 @@ struct Transform {
#[derive(Clone, Debug)]
struct TransformPlaceholder {
text: &'static str,
+ chars: u128,
renderer: ChunkRenderer,
}
@@ -1274,6 +1284,10 @@ pub struct Chunk<'a> {
pub is_inlay: bool,
/// An optional recipe for how the chunk should be presented.
pub renderer: Option<ChunkRenderer>,
+ /// Bitmap of tab character locations in chunk
+ pub tabs: u128,
+ /// Bitmap of character locations in chunk
+ pub chars: u128,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
@@ -1348,7 +1362,7 @@ impl FoldChunks<'_> {
let inlay_end = if self
.transform_cursor
.item()
- .map_or(true, |transform| transform.is_fold())
+ .is_none_or(|transform| transform.is_fold())
{
inlay_start
} else if range.end < transform_end.0 {
@@ -1391,6 +1405,7 @@ impl<'a> Iterator for FoldChunks<'a> {
self.output_offset.0 += placeholder.text.len();
return Some(Chunk {
text: placeholder.text,
+ chars: placeholder.chars,
renderer: Some(placeholder.renderer.clone()),
..Default::default()
});
@@ -1429,6 +1444,16 @@ impl<'a> Iterator for FoldChunks<'a> {
chunk.text = &chunk.text
[(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0];
+ let bit_end = (chunk_end - buffer_chunk_start).0;
+ let mask = if bit_end >= 128 {
+ u128::MAX
+ } else {
+ (1u128 << bit_end) - 1
+ };
+
+ chunk.tabs = (chunk.tabs >> (self.inlay_offset - buffer_chunk_start).0) & mask;
+ chunk.chars = (chunk.chars >> (self.inlay_offset - buffer_chunk_start).0) & mask;
+
if chunk_end == transform_end {
self.transform_cursor.next();
} else if chunk_end == buffer_chunk_end {
@@ -1439,6 +1464,8 @@ impl<'a> Iterator for FoldChunks<'a> {
self.output_offset.0 += chunk.text.len();
return Some(Chunk {
text: chunk.text,
+ tabs: chunk.tabs,
+ chars: chunk.chars,
syntax_highlight_id: chunk.syntax_highlight_id,
highlight_style: chunk.highlight_style,
diagnostic_severity: chunk.diagnostic_severity,
@@ -1463,7 +1490,7 @@ impl FoldOffset {
.transforms
.cursor::<Dimensions<FoldOffset, TransformSummary>>(&());
cursor.seek(&self, Bias::Right);
- let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) {
+ let overshoot = if cursor.item().is_none_or(|t| t.is_fold()) {
Point::new(0, (self.0 - cursor.start().0.0) as u32)
} else {
let inlay_offset = cursor.start().1.input.len + self.0 - cursor.start().0.0;
@@ -1557,7 +1584,7 @@ mod tests {
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot, vec![]);
@@ -1636,7 +1663,7 @@ mod tests {
let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
{
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
@@ -1712,7 +1739,7 @@ mod tests {
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
@@ -1720,7 +1747,7 @@ mod tests {
(Point::new(0, 2)..Point::new(2, 2), FoldPlaceholder::test()),
(Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
]);
- let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+ let (snapshot, _) = map.read(inlay_snapshot, vec![]);
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee");
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
@@ -1747,7 +1774,7 @@ mod tests {
(Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()),
(Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
]);
- let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+ let (snapshot, _) = map.read(inlay_snapshot, vec![]);
let fold_ranges = snapshot
.folds_in_range(Point::new(1, 0)..Point::new(1, 3))
.map(|fold| {
@@ -1771,9 +1798,9 @@ mod tests {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
- let len = rng.gen_range(0..10);
+ let len = rng.random_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
- let buffer = if rng.r#gen() {
+ let buffer = if rng.random() {
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)
@@ -1782,7 +1809,7 @@ mod tests {
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let mut map = FoldMap::new(inlay_snapshot.clone()).0;
- let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+ let (mut initial_snapshot, _) = map.read(inlay_snapshot, vec![]);
let mut snapshot_edits = Vec::new();
let mut next_inlay_id = 0;
@@ -1790,7 +1817,7 @@ mod tests {
log::info!("text: {:?}", buffer_snapshot.text());
let mut buffer_edits = Vec::new();
let mut inlay_edits = Vec::new();
- match rng.gen_range(0..=100) {
+ match rng.random_range(0..=100) {
0..=39 => {
snapshot_edits.extend(map.randomly_mutate(&mut rng));
}
@@ -1800,7 +1827,7 @@ mod tests {
}
_ => buffer.update(cx, |buffer, cx| {
let subscription = buffer.subscribe();
- let edit_count = rng.gen_range(1..=5);
+ let edit_count = rng.random_range(1..=5);
buffer.randomly_mutate(&mut rng, edit_count, cx);
buffer_snapshot = buffer.snapshot(cx);
let edits = subscription.consume().into_inner();
@@ -1917,10 +1944,14 @@ mod tests {
}
for _ in 0..5 {
- let mut start = snapshot
- .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Left);
- let mut end = snapshot
- .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Right);
+ let mut start = snapshot.clip_offset(
+ FoldOffset(rng.random_range(0..=snapshot.len().0)),
+ Bias::Left,
+ );
+ let mut end = snapshot.clip_offset(
+ FoldOffset(rng.random_range(0..=snapshot.len().0)),
+ Bias::Right,
+ );
if start > end {
mem::swap(&mut start, &mut end);
}
@@ -1975,8 +2006,8 @@ mod tests {
for _ in 0..5 {
let end =
- buffer_snapshot.clip_offset(rng.gen_range(0..=buffer_snapshot.len()), Right);
- let start = buffer_snapshot.clip_offset(rng.gen_range(0..=end), Left);
+ buffer_snapshot.clip_offset(rng.random_range(0..=buffer_snapshot.len()), Right);
+ let start = buffer_snapshot.clip_offset(rng.random_range(0..=end), Left);
let expected_folds = map
.snapshot
.folds
@@ -2001,10 +2032,10 @@ mod tests {
let text = snapshot.text();
for _ in 0..5 {
- let start_row = rng.gen_range(0..=snapshot.max_point().row());
- let start_column = rng.gen_range(0..=snapshot.line_len(start_row));
- let end_row = rng.gen_range(0..=snapshot.max_point().row());
- let end_column = rng.gen_range(0..=snapshot.line_len(end_row));
+ let start_row = rng.random_range(0..=snapshot.max_point().row());
+ let start_column = rng.random_range(0..=snapshot.line_len(start_row));
+ let end_row = rng.random_range(0..=snapshot.max_point().row());
+ let end_column = rng.random_range(0..=snapshot.line_len(end_row));
let mut start =
snapshot.clip_point(FoldPoint::new(start_row, start_column), Bias::Left);
let mut end = snapshot.clip_point(FoldPoint::new(end_row, end_column), Bias::Right);
@@ -2068,6 +2099,97 @@ mod tests {
);
}
+ #[gpui::test(iterations = 100)]
+ fn test_random_chunk_bitmaps(cx: &mut gpui::App, mut rng: StdRng) {
+ init_test(cx);
+
+ // Generate random buffer using existing test infrastructure
+ let text_len = rng.random_range(0..10000);
+ let buffer = if rng.random() {
+ let text = RandomCharIter::new(&mut rng)
+ .take(text_len)
+ .collect::<String>();
+ MultiBuffer::build_simple(&text, cx)
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ };
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
+
+ // Perform random mutations
+ let mutation_count = rng.random_range(1..10);
+ for _ in 0..mutation_count {
+ fold_map.randomly_mutate(&mut rng);
+ }
+
+ let (snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
+
+ // Get all chunks and verify their bitmaps
+ let chunks = snapshot.chunks(
+ FoldOffset(0)..FoldOffset(snapshot.len().0),
+ false,
+ Highlights::default(),
+ );
+
+ for chunk in chunks {
+ let chunk_text = chunk.text;
+ let chars_bitmap = chunk.chars;
+ let tabs_bitmap = chunk.tabs;
+
+ // Check empty chunks have empty bitmaps
+ if chunk_text.is_empty() {
+ assert_eq!(
+ chars_bitmap, 0,
+ "Empty chunk should have empty chars bitmap"
+ );
+ assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
+ continue;
+ }
+
+ // Verify that chunk text doesn't exceed 128 bytes
+ assert!(
+ chunk_text.len() <= 128,
+ "Chunk text length {} exceeds 128 bytes",
+ chunk_text.len()
+ );
+
+ // Verify chars bitmap
+ let char_indices = chunk_text
+ .char_indices()
+ .map(|(i, _)| i)
+ .collect::<Vec<_>>();
+
+ for byte_idx in 0..chunk_text.len() {
+ let should_have_bit = char_indices.contains(&byte_idx);
+ let has_bit = chars_bitmap & (1 << byte_idx) != 0;
+
+ if has_bit != should_have_bit {
+ eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
+ eprintln!("Char indices: {:?}", char_indices);
+ eprintln!("Chars bitmap: {:#b}", chars_bitmap);
+ assert_eq!(
+ has_bit, should_have_bit,
+ "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, should_have_bit, has_bit
+ );
+ }
+ }
+
+ // Verify tabs bitmap
+ for (byte_idx, byte) in chunk_text.bytes().enumerate() {
+ let is_tab = byte == b'\t';
+ let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
+
+ assert_eq!(
+ has_bit, is_tab,
+ "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, byte as char, is_tab, has_bit
+ );
+ }
+ }
+ }
+
fn init_test(cx: &mut gpui::App) {
let store = SettingsStore::test(cx);
cx.set_global(store);
@@ -2109,17 +2231,17 @@ mod tests {
rng: &mut impl Rng,
) -> Vec<(FoldSnapshot, Vec<FoldEdit>)> {
let mut snapshot_edits = Vec::new();
- match rng.gen_range(0..=100) {
+ match rng.random_range(0..=100) {
0..=39 if !self.snapshot.folds.is_empty() => {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let mut to_unfold = Vec::new();
- for _ in 0..rng.gen_range(1..=3) {
- let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
- let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
+ for _ in 0..rng.random_range(1..=3) {
+ let end = buffer.clip_offset(rng.random_range(0..=buffer.len()), Right);
+ let start = buffer.clip_offset(rng.random_range(0..=end), Left);
to_unfold.push(start..end);
}
- let inclusive = rng.r#gen();
+ let inclusive = rng.random();
log::info!("unfolding {:?} (inclusive: {})", to_unfold, inclusive);
let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
snapshot_edits.push((snapshot, edits));
@@ -2130,9 +2252,9 @@ mod tests {
let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
let mut to_fold = Vec::new();
- for _ in 0..rng.gen_range(1..=2) {
- let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
- let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
+ for _ in 0..rng.random_range(1..=2) {
+ let end = buffer.clip_offset(rng.random_range(0..=buffer.len()), Right);
+ let start = buffer.clip_offset(rng.random_range(0..=end), Left);
to_fold.push((start..end, FoldPlaceholder::test()));
}
log::info!("folding {:?}", to_fold);
@@ -11,7 +11,7 @@ use std::{
sync::Arc,
};
use sum_tree::{Bias, Cursor, Dimensions, SumTree};
-use text::{Patch, Rope};
+use text::{ChunkBitmaps, Patch, Rope};
use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
@@ -48,7 +48,7 @@ pub struct Inlay {
impl Inlay {
pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self {
let mut text = hint.text();
- if hint.padding_right && text.chars_at(text.len().saturating_sub(1)).next() != Some(' ') {
+ if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') {
text.push(" ");
}
if hint.padding_left && text.chars_at(0).next() != Some(' ') {
@@ -245,8 +245,9 @@ pub struct InlayChunks<'a> {
transforms: Cursor<'a, Transform, Dimensions<InlayOffset, usize>>,
buffer_chunks: CustomHighlightsChunks<'a>,
buffer_chunk: Option<Chunk<'a>>,
- inlay_chunks: Option<text::Chunks<'a>>,
- inlay_chunk: Option<&'a str>,
+ inlay_chunks: Option<text::ChunkWithBitmaps<'a>>,
+ /// text, char bitmap, tabs bitmap
+ inlay_chunk: Option<ChunkBitmaps<'a>>,
output_offset: InlayOffset,
max_output_offset: InlayOffset,
highlight_styles: HighlightStyles,
@@ -316,11 +317,25 @@ impl<'a> Iterator for InlayChunks<'a> {
let (prefix, suffix) = chunk.text.split_at(split_index);
+ let (chars, tabs) = if split_index == 128 {
+ let output = (chunk.chars, chunk.tabs);
+ chunk.chars = 0;
+ chunk.tabs = 0;
+ output
+ } else {
+ let mask = (1 << split_index) - 1;
+ let output = (chunk.chars & mask, chunk.tabs & mask);
+ chunk.chars = chunk.chars >> split_index;
+ chunk.tabs = chunk.tabs >> split_index;
+ output
+ };
chunk.text = suffix;
self.output_offset.0 += prefix.len();
InlayChunk {
chunk: Chunk {
text: prefix,
+ chars,
+ tabs,
..chunk.clone()
},
renderer: None,
@@ -385,9 +400,9 @@ impl<'a> Iterator for InlayChunks<'a> {
next_inlay_highlight_endpoint = usize::MAX;
} else {
next_inlay_highlight_endpoint = range.end - offset_in_inlay.0;
- highlight_style
- .get_or_insert_with(Default::default)
- .highlight(*style);
+ highlight_style = highlight_style
+ .map(|highlight| highlight.highlight(*style))
+ .or_else(|| Some(*style));
}
} else {
next_inlay_highlight_endpoint = usize::MAX;
@@ -397,9 +412,14 @@ impl<'a> Iterator for InlayChunks<'a> {
let start = offset_in_inlay;
let end = cmp::min(self.max_output_offset, self.transforms.end().0)
- self.transforms.start().0;
- inlay.text.chunks_in_range(start.0..end.0)
+ let chunks = inlay.text.chunks_in_range(start.0..end.0);
+ text::ChunkWithBitmaps(chunks)
});
- let inlay_chunk = self
+ let ChunkBitmaps {
+ text: inlay_chunk,
+ chars,
+ tabs,
+ } = self
.inlay_chunk
.get_or_insert_with(|| inlay_chunks.next().unwrap());
@@ -421,6 +441,20 @@ impl<'a> Iterator for InlayChunks<'a> {
let (chunk, remainder) = inlay_chunk.split_at(split_index);
*inlay_chunk = remainder;
+
+ let (chars, tabs) = if split_index == 128 {
+ let output = (*chars, *tabs);
+ *chars = 0;
+ *tabs = 0;
+ output
+ } else {
+ let mask = (1 << split_index as u32) - 1;
+ let output = (*chars & mask, *tabs & mask);
+ *chars = *chars >> split_index;
+ *tabs = *tabs >> split_index;
+ output
+ };
+
if inlay_chunk.is_empty() {
self.inlay_chunk = None;
}
@@ -430,6 +464,8 @@ impl<'a> Iterator for InlayChunks<'a> {
InlayChunk {
chunk: Chunk {
text: chunk,
+ chars,
+ tabs,
highlight_style,
is_inlay: true,
..Chunk::default()
@@ -557,11 +593,11 @@ impl InlayMap {
let mut buffer_edits_iter = buffer_edits.iter().peekable();
while let Some(buffer_edit) = buffer_edits_iter.next() {
new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left), &());
- if let Some(Transform::Isomorphic(transform)) = cursor.item() {
- if cursor.end().0 == buffer_edit.old.start {
- push_isomorphic(&mut new_transforms, *transform);
- cursor.next();
- }
+ if let Some(Transform::Isomorphic(transform)) = cursor.item()
+ && cursor.end().0 == buffer_edit.old.start
+ {
+ push_isomorphic(&mut new_transforms, *transform);
+ cursor.next();
}
// Remove all the inlays and transforms contained by the edit.
@@ -625,7 +661,7 @@ impl InlayMap {
// we can push its remainder.
if buffer_edits_iter
.peek()
- .map_or(true, |edit| edit.old.start >= cursor.end().0)
+ .is_none_or(|edit| edit.old.start >= cursor.end().0)
{
let transform_start = new_transforms.summary().input.len;
let transform_end =
@@ -719,14 +755,18 @@ impl InlayMap {
let mut to_remove = Vec::new();
let mut to_insert = Vec::new();
let snapshot = &mut self.snapshot;
- for i in 0..rng.gen_range(1..=5) {
- if self.inlays.is_empty() || rng.r#gen() {
+ for i in 0..rng.random_range(1..=5) {
+ if self.inlays.is_empty() || rng.random() {
let position = snapshot.buffer.random_byte_range(0, rng).start;
- let bias = if rng.r#gen() { Bias::Left } else { Bias::Right };
- let len = if rng.gen_bool(0.01) {
+ let bias = if rng.random() {
+ Bias::Left
+ } else {
+ Bias::Right
+ };
+ let len = if rng.random_bool(0.01) {
0
} else {
- rng.gen_range(1..=5)
+ rng.random_range(1..=5)
};
let text = util::RandomCharIter::new(&mut *rng)
.filter(|ch| *ch != '\r')
@@ -1220,6 +1260,7 @@ mod tests {
use std::{any::TypeId, cmp::Reverse, env, sync::Arc};
use sum_tree::TreeMap;
use text::Patch;
+ use util::RandomCharIter;
use util::post_inc;
#[test]
@@ -1305,6 +1346,29 @@ mod tests {
);
}
+ #[gpui::test]
+ fn test_inlay_hint_padding_with_multibyte_chars() {
+ assert_eq!(
+ Inlay::hint(
+ 0,
+ Anchor::min(),
+ &InlayHint {
+ label: InlayHintLabel::String("🎨".to_string()),
+ position: text::Anchor::default(),
+ padding_left: true,
+ padding_right: true,
+ tooltip: None,
+ kind: None,
+ resolve_state: ResolveState::Resolved,
+ },
+ )
+ .text
+ .to_string(),
+ " 🎨 ",
+ "Should pad single emoji correctly"
+ );
+ }
+
#[gpui::test]
fn test_basic_inlays(cx: &mut App) {
let buffer = MultiBuffer::build_simple("abcdefghi", cx);
@@ -1642,8 +1706,8 @@ mod tests {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
- let len = rng.gen_range(0..30);
- let buffer = if rng.r#gen() {
+ let len = rng.random_range(0..30);
+ let buffer = if rng.random() {
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
@@ -1660,7 +1724,7 @@ mod tests {
let mut prev_inlay_text = inlay_snapshot.text();
let mut buffer_edits = Vec::new();
- match rng.gen_range(0..=100) {
+ match rng.random_range(0..=100) {
0..=50 => {
let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
log::info!("mutated text: {:?}", snapshot.text());
@@ -1668,7 +1732,7 @@ mod tests {
}
_ => buffer.update(cx, |buffer, cx| {
let subscription = buffer.subscribe();
- let edit_count = rng.gen_range(1..=5);
+ let edit_count = rng.random_range(1..=5);
buffer.randomly_mutate(&mut rng, edit_count, cx);
buffer_snapshot = buffer.snapshot(cx);
let edits = subscription.consume().into_inner();
@@ -1717,7 +1781,7 @@ mod tests {
}
let mut text_highlights = TextHighlights::default();
- let text_highlight_count = rng.gen_range(0_usize..10);
+ let text_highlight_count = rng.random_range(0_usize..10);
let mut text_highlight_ranges = (0..text_highlight_count)
.map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
.collect::<Vec<_>>();
@@ -1739,10 +1803,10 @@ mod tests {
let mut inlay_highlights = InlayHighlights::default();
if !inlays.is_empty() {
- let inlay_highlight_count = rng.gen_range(0..inlays.len());
+ let inlay_highlight_count = rng.random_range(0..inlays.len());
let mut inlay_indices = BTreeSet::default();
while inlay_indices.len() < inlay_highlight_count {
- inlay_indices.insert(rng.gen_range(0..inlays.len()));
+ inlay_indices.insert(rng.random_range(0..inlays.len()));
}
let new_highlights = TreeMap::from_ordered_entries(
inlay_indices
@@ -1759,8 +1823,8 @@ mod tests {
}),
n => {
let inlay_text = inlay.text.to_string();
- let mut highlight_end = rng.gen_range(1..n);
- let mut highlight_start = rng.gen_range(0..highlight_end);
+ let mut highlight_end = rng.random_range(1..n);
+ let mut highlight_start = rng.random_range(0..highlight_end);
while !inlay_text.is_char_boundary(highlight_end) {
highlight_end += 1;
}
@@ -1782,9 +1846,9 @@ mod tests {
}
for _ in 0..5 {
- let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
+ let mut end = rng.random_range(0..=inlay_snapshot.len().0);
end = expected_text.clip_offset(end, Bias::Right);
- let mut start = rng.gen_range(0..=end);
+ let mut start = rng.random_range(0..=end);
start = expected_text.clip_offset(start, Bias::Right);
let range = InlayOffset(start)..InlayOffset(end);
@@ -1939,6 +2003,102 @@ mod tests {
}
}
+ #[gpui::test(iterations = 100)]
+ fn test_random_chunk_bitmaps(cx: &mut gpui::App, mut rng: StdRng) {
+ init_test(cx);
+
+ // Generate random buffer using existing test infrastructure
+ let text_len = rng.random_range(0..10000);
+ let buffer = if rng.random() {
+ let text = RandomCharIter::new(&mut rng)
+ .take(text_len)
+ .collect::<String>();
+ MultiBuffer::build_simple(&text, cx)
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ };
+
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (mut inlay_map, _) = InlayMap::new(buffer_snapshot.clone());
+
+ // Perform random mutations to add inlays
+ let mut next_inlay_id = 0;
+ let mutation_count = rng.random_range(1..10);
+ for _ in 0..mutation_count {
+ inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+ }
+
+ let (snapshot, _) = inlay_map.sync(buffer_snapshot, vec![]);
+
+ // Get all chunks and verify their bitmaps
+ let chunks = snapshot.chunks(
+ InlayOffset(0)..InlayOffset(snapshot.len().0),
+ false,
+ Highlights::default(),
+ );
+
+ for chunk in chunks.into_iter().map(|inlay_chunk| inlay_chunk.chunk) {
+ let chunk_text = chunk.text;
+ let chars_bitmap = chunk.chars;
+ let tabs_bitmap = chunk.tabs;
+
+ // Check empty chunks have empty bitmaps
+ if chunk_text.is_empty() {
+ assert_eq!(
+ chars_bitmap, 0,
+ "Empty chunk should have empty chars bitmap"
+ );
+ assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
+ continue;
+ }
+
+ // Verify that chunk text doesn't exceed 128 bytes
+ assert!(
+ chunk_text.len() <= 128,
+ "Chunk text length {} exceeds 128 bytes",
+ chunk_text.len()
+ );
+
+ // Verify chars bitmap
+ let char_indices = chunk_text
+ .char_indices()
+ .map(|(i, _)| i)
+ .collect::<Vec<_>>();
+
+ for byte_idx in 0..chunk_text.len() {
+ let should_have_bit = char_indices.contains(&byte_idx);
+ let has_bit = chars_bitmap & (1 << byte_idx) != 0;
+
+ if has_bit != should_have_bit {
+ eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
+ eprintln!("Char indices: {:?}", char_indices);
+ eprintln!("Chars bitmap: {:#b}", chars_bitmap);
+ assert_eq!(
+ has_bit, should_have_bit,
+ "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, should_have_bit, has_bit
+ );
+ }
+ }
+
+ // Verify tabs bitmap
+ for (byte_idx, byte) in chunk_text.bytes().enumerate() {
+ let is_tab = byte == b'\t';
+ let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
+
+ if has_bit != is_tab {
+ eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
+ eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
+ assert_eq!(
+ has_bit, is_tab,
+ "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, byte as char, is_tab, has_bit
+ );
+ }
+ }
+ }
+ }
+
fn init_test(cx: &mut App) {
let store = SettingsStore::test(cx);
cx.set_global(store);
@@ -36,8 +36,8 @@ pub fn is_invisible(c: char) -> bool {
} else if c >= '\u{7f}' {
c <= '\u{9f}'
|| (c.is_whitespace() && c != IDEOGRAPHIC_SPACE)
- || contains(c, &FORMAT)
- || contains(c, &OTHER)
+ || contains(c, FORMAT)
+ || contains(c, OTHER)
} else {
false
}
@@ -50,7 +50,7 @@ pub fn replacement(c: char) -> Option<&'static str> {
Some(C0_SYMBOLS[c as usize])
} else if c == '\x7f' {
Some(DEL)
- } else if contains(c, &PRESERVE) {
+ } else if contains(c, PRESERVE) {
None
} else {
Some("\u{2007}") // fixed width space
@@ -61,14 +61,14 @@ pub fn replacement(c: char) -> Option<&'static str> {
// but could if we tracked state in the classifier.
const IDEOGRAPHIC_SPACE: char = '\u{3000}';
-const C0_SYMBOLS: &'static [&'static str] = &[
+const C0_SYMBOLS: &[&str] = &[
"␀", "␁", "␂", "␃", "␄", "␅", "␆", "␇", "␈", "␉", "␊", "␋", "␌", "␍", "␎", "␏", "␐", "␑", "␒",
"␓", "␔", "␕", "␖", "␗", "␘", "␙", "␚", "␛", "␜", "␝", "␞", "␟",
];
-const DEL: &'static str = "␡";
+const DEL: &str = "␡";
// generated using ucd-generate: ucd-generate general-category --include Format --chars ucd-16.0.0
-pub const FORMAT: &'static [(char, char)] = &[
+pub const FORMAT: &[(char, char)] = &[
('\u{ad}', '\u{ad}'),
('\u{600}', '\u{605}'),
('\u{61c}', '\u{61c}'),
@@ -93,7 +93,7 @@ pub const FORMAT: &'static [(char, char)] = &[
];
// hand-made base on https://invisible-characters.com (Excluding Cf)
-pub const OTHER: &'static [(char, char)] = &[
+pub const OTHER: &[(char, char)] = &[
('\u{034f}', '\u{034f}'),
('\u{115F}', '\u{1160}'),
('\u{17b4}', '\u{17b5}'),
@@ -107,7 +107,7 @@ pub const OTHER: &'static [(char, char)] = &[
];
// a subset of FORMAT/OTHER that may appear within glyphs
-const PRESERVE: &'static [(char, char)] = &[
+const PRESERVE: &[(char, char)] = &[
('\u{034f}', '\u{034f}'),
('\u{200d}', '\u{200d}'),
('\u{17b4}', '\u{17b5}'),
@@ -2,6 +2,7 @@ use super::{
Highlights,
fold_map::{self, Chunk, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
};
+
use language::Point;
use multi_buffer::MultiBufferSnapshot;
use std::{cmp, mem, num::NonZeroU32, ops::Range};
@@ -72,6 +73,7 @@ impl TabMap {
false,
Highlights::default(),
) {
+ // todo(performance use tabs bitmask)
for (ix, _) in chunk.text.match_indices('\t') {
let offset_from_edit = offset_from_edit + (ix as u32);
if first_tab_offset.is_none() {
@@ -116,7 +118,7 @@ impl TabMap {
state.new.end = edit.new.end;
Some(None) // Skip this edit, it's merged
} else {
- let new_state = edit.clone();
+ let new_state = edit;
let result = Some(Some(state.clone())); // Yield the previous edit
**state = new_state;
result
@@ -230,7 +232,7 @@ impl TabSnapshot {
}
}
- pub fn chunks<'a>(
+ pub(crate) fn chunks<'a>(
&'a self,
range: Range<TabPoint>,
language_aware: bool,
@@ -299,21 +301,29 @@ impl TabSnapshot {
}
pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
- let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
- let expanded = self.expand_tabs(chars, input.column());
+ let chunks = self.fold_snapshot.chunks_at(FoldPoint::new(input.row(), 0));
+ let tab_cursor = TabStopCursor::new(chunks);
+ let expanded = self.expand_tabs(tab_cursor, input.column());
TabPoint::new(input.row(), expanded)
}
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
- let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
+ let chunks = self
+ .fold_snapshot
+ .chunks_at(FoldPoint::new(output.row(), 0));
+
+ let tab_cursor = TabStopCursor::new(chunks);
let expanded = output.column();
let (collapsed, expanded_char_column, to_next_stop) =
- self.collapse_tabs(chars, expanded, bias);
- (
+ self.collapse_tabs(tab_cursor, expanded, bias);
+
+ let result = (
FoldPoint::new(output.row(), collapsed),
expanded_char_column,
to_next_stop,
- )
+ );
+
+ result
}
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
@@ -330,72 +340,90 @@ impl TabSnapshot {
.to_buffer_point(inlay_point)
}
- fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
+ fn expand_tabs<'a, I>(&self, mut cursor: TabStopCursor<'a, I>, column: u32) -> u32
+ where
+ I: Iterator<Item = Chunk<'a>>,
+ {
let tab_size = self.tab_size.get();
- let mut expanded_chars = 0;
- let mut expanded_bytes = 0;
- let mut collapsed_bytes = 0;
let end_column = column.min(self.max_expansion_column);
- for c in chars {
- if collapsed_bytes >= end_column {
- break;
- }
- if c == '\t' {
- let tab_len = tab_size - expanded_chars % tab_size;
- expanded_bytes += tab_len;
- expanded_chars += tab_len;
- } else {
- expanded_bytes += c.len_utf8() as u32;
- expanded_chars += 1;
- }
- collapsed_bytes += c.len_utf8() as u32;
+ let mut seek_target = end_column;
+ let mut tab_count = 0;
+ let mut expanded_tab_len = 0;
+
+ while let Some(tab_stop) = cursor.seek(seek_target) {
+ let expanded_chars_old = tab_stop.char_offset + expanded_tab_len - tab_count;
+ let tab_len = tab_size - ((expanded_chars_old - 1) % tab_size);
+ tab_count += 1;
+ expanded_tab_len += tab_len;
+
+ seek_target = end_column - cursor.byte_offset;
}
+
+ let left_over_char_bytes = if !cursor.is_char_boundary() {
+ cursor.bytes_until_next_char().unwrap_or(0) as u32
+ } else {
+ 0
+ };
+
+ let collapsed_bytes = cursor.byte_offset() + left_over_char_bytes;
+ let expanded_bytes =
+ cursor.byte_offset() + expanded_tab_len - tab_count + left_over_char_bytes;
+
expanded_bytes + column.saturating_sub(collapsed_bytes)
}
- fn collapse_tabs(
+ fn collapse_tabs<'a, I>(
&self,
- chars: impl Iterator<Item = char>,
+ mut cursor: TabStopCursor<'a, I>,
column: u32,
bias: Bias,
- ) -> (u32, u32, u32) {
+ ) -> (u32, u32, u32)
+ where
+ I: Iterator<Item = Chunk<'a>>,
+ {
let tab_size = self.tab_size.get();
-
- let mut expanded_bytes = 0;
- let mut expanded_chars = 0;
- let mut collapsed_bytes = 0;
- for c in chars {
- if expanded_bytes >= column {
- break;
- }
- if collapsed_bytes >= self.max_expansion_column {
- break;
- }
-
- if c == '\t' {
- let tab_len = tab_size - (expanded_chars % tab_size);
- expanded_chars += tab_len;
- expanded_bytes += tab_len;
- if expanded_bytes > column {
- expanded_chars -= expanded_bytes - column;
- return match bias {
- Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
- Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
- };
- }
+ let mut collapsed_column = column;
+ let mut seek_target = column.min(self.max_expansion_column);
+ let mut tab_count = 0;
+ let mut expanded_tab_len = 0;
+
+ while let Some(tab_stop) = cursor.seek(seek_target) {
+ // Calculate how much we want to expand this tab stop (into spaces)
+ let expanded_chars_old = tab_stop.char_offset + expanded_tab_len - tab_count;
+ let tab_len = tab_size - ((expanded_chars_old - 1) % tab_size);
+ // Increment tab count
+ tab_count += 1;
+ // The count of how many spaces we've added to this line in place of tab bytes
+ expanded_tab_len += tab_len;
+
+ // The count of bytes at this point in the iteration while considering tab_count and previous expansions
+ let expanded_bytes = tab_stop.byte_offset + expanded_tab_len - tab_count;
+
+ // Did we expand past the search target?
+ if expanded_bytes > column {
+ let mut expanded_chars = tab_stop.char_offset + expanded_tab_len - tab_count;
+ // We expanded past the search target, so need to account for the offshoot
+ expanded_chars -= expanded_bytes - column;
+ return match bias {
+ Bias::Left => (
+ cursor.byte_offset() - 1,
+ expanded_chars,
+ expanded_bytes - column,
+ ),
+ Bias::Right => (cursor.byte_offset(), expanded_chars, 0),
+ };
} else {
- expanded_chars += 1;
- expanded_bytes += c.len_utf8() as u32;
- }
-
- if expanded_bytes > column && matches!(bias, Bias::Left) {
- expanded_chars -= 1;
- break;
+ // otherwise we only want to move the cursor collapse column forward
+ collapsed_column = collapsed_column - tab_len + 1;
+ seek_target = (collapsed_column - cursor.byte_offset)
+ .min(self.max_expansion_column - cursor.byte_offset);
}
-
- collapsed_bytes += c.len_utf8() as u32;
}
+
+ let collapsed_bytes = cursor.byte_offset();
+ let expanded_bytes = cursor.byte_offset() + expanded_tab_len - tab_count;
+ let expanded_chars = cursor.char_offset() + expanded_tab_len - tab_count;
(
collapsed_bytes + column.saturating_sub(expanded_bytes),
expanded_chars,
@@ -523,6 +551,7 @@ impl TabChunks<'_> {
self.chunk = Chunk {
text: &SPACES[0..(to_next_stop as usize)],
is_tab: true,
+ chars: (1u128 << to_next_stop) - 1,
..Default::default()
};
self.inside_leading_tab = to_next_stop > 0;
@@ -546,18 +575,37 @@ impl<'a> Iterator for TabChunks<'a> {
}
}
+ //todo(improve performance by using tab cursor)
for (ix, c) in self.chunk.text.char_indices() {
match c {
'\t' => {
if ix > 0 {
let (prefix, suffix) = self.chunk.text.split_at(ix);
+
+ let (chars, tabs) = if ix == 128 {
+ let output = (self.chunk.chars, self.chunk.tabs);
+ self.chunk.chars = 0;
+ self.chunk.tabs = 0;
+ output
+ } else {
+ let mask = (1 << ix) - 1;
+ let output = (self.chunk.chars & mask, self.chunk.tabs & mask);
+ self.chunk.chars = self.chunk.chars >> ix;
+ self.chunk.tabs = self.chunk.tabs >> ix;
+ output
+ };
+
self.chunk.text = suffix;
return Some(Chunk {
text: prefix,
+ chars,
+ tabs,
..self.chunk.clone()
});
} else {
self.chunk.text = &self.chunk.text[1..];
+ self.chunk.tabs >>= 1;
+ self.chunk.chars >>= 1;
let tab_size = if self.input_column < self.max_expansion_column {
self.tab_size.get()
} else {
@@ -575,6 +623,8 @@ impl<'a> Iterator for TabChunks<'a> {
return Some(Chunk {
text: &SPACES[..len as usize],
is_tab: true,
+ chars: (1 << len) - 1,
+ tabs: 0,
..self.chunk.clone()
});
}
@@ -603,21 +653,270 @@ mod tests {
use super::*;
use crate::{
MultiBuffer,
- display_map::{fold_map::FoldMap, inlay_map::InlayMap},
+ display_map::{
+ fold_map::{FoldMap, FoldOffset},
+ inlay_map::InlayMap,
+ },
};
use rand::{Rng, prelude::StdRng};
+ use util;
+
+ impl TabSnapshot {
+ fn expected_collapse_tabs(
+ &self,
+ chars: impl Iterator<Item = char>,
+ column: u32,
+ bias: Bias,
+ ) -> (u32, u32, u32) {
+ let tab_size = self.tab_size.get();
+
+ let mut expanded_bytes = 0;
+ let mut expanded_chars = 0;
+ let mut collapsed_bytes = 0;
+ for c in chars {
+ if expanded_bytes >= column {
+ break;
+ }
+ if collapsed_bytes >= self.max_expansion_column {
+ break;
+ }
+
+ if c == '\t' {
+ let tab_len = tab_size - (expanded_chars % tab_size);
+ expanded_chars += tab_len;
+ expanded_bytes += tab_len;
+ if expanded_bytes > column {
+ expanded_chars -= expanded_bytes - column;
+ return match bias {
+ Bias::Left => {
+ (collapsed_bytes, expanded_chars, expanded_bytes - column)
+ }
+ Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
+ };
+ }
+ } else {
+ expanded_chars += 1;
+ expanded_bytes += c.len_utf8() as u32;
+ }
+
+ if expanded_bytes > column && matches!(bias, Bias::Left) {
+ expanded_chars -= 1;
+ break;
+ }
+
+ collapsed_bytes += c.len_utf8() as u32;
+ }
+
+ (
+ collapsed_bytes + column.saturating_sub(expanded_bytes),
+ expanded_chars,
+ 0,
+ )
+ }
+
+ pub fn expected_to_tab_point(&self, input: FoldPoint) -> TabPoint {
+ let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
+ let expanded = self.expected_expand_tabs(chars, input.column());
+ TabPoint::new(input.row(), expanded)
+ }
+
+ fn expected_expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
+ let tab_size = self.tab_size.get();
+
+ let mut expanded_chars = 0;
+ let mut expanded_bytes = 0;
+ let mut collapsed_bytes = 0;
+ let end_column = column.min(self.max_expansion_column);
+ for c in chars {
+ if collapsed_bytes >= end_column {
+ break;
+ }
+ if c == '\t' {
+ let tab_len = tab_size - expanded_chars % tab_size;
+ expanded_bytes += tab_len;
+ expanded_chars += tab_len;
+ } else {
+ expanded_bytes += c.len_utf8() as u32;
+ expanded_chars += 1;
+ }
+ collapsed_bytes += c.len_utf8() as u32;
+ }
+
+ expanded_bytes + column.saturating_sub(collapsed_bytes)
+ }
+
+ fn expected_to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
+ let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
+ let expanded = output.column();
+ let (collapsed, expanded_char_column, to_next_stop) =
+ self.expected_collapse_tabs(chars, expanded, bias);
+ (
+ FoldPoint::new(output.row(), collapsed),
+ expanded_char_column,
+ to_next_stop,
+ )
+ }
+ }
#[gpui::test]
fn test_expand_tabs(cx: &mut gpui::App) {
+ let test_values = [
+ ("κg🏀 f\nwo🏀❌by🍐❎β🍗c\tβ❎ \ncλ🎉", 17),
+ (" \twςe", 4),
+ ("fε", 1),
+ ("i❎\t", 3),
+ ];
let buffer = MultiBuffer::build_simple("", cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
- assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
- assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
- assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
+ for (text, column) in test_values {
+ let mut tabs = 0u128;
+ let mut chars = 0u128;
+ for (idx, c) in text.char_indices() {
+ if c == '\t' {
+ tabs |= 1 << idx;
+ }
+ chars |= 1 << idx;
+ }
+
+ let chunks = [Chunk {
+ text,
+ tabs,
+ chars,
+ ..Default::default()
+ }];
+
+ let cursor = TabStopCursor::new(chunks);
+
+ assert_eq!(
+ tab_snapshot.expected_expand_tabs(text.chars(), column),
+ tab_snapshot.expand_tabs(cursor, column)
+ );
+ }
+ }
+
+ #[gpui::test]
+ fn test_collapse_tabs(cx: &mut gpui::App) {
+ let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
+
+ let buffer = MultiBuffer::build_simple(input, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+
+ for (ix, _) in input.char_indices() {
+ let range = TabPoint::new(0, ix as u32)..tab_snapshot.max_point();
+
+ assert_eq!(
+ tab_snapshot.expected_to_fold_point(range.start, Bias::Left),
+ tab_snapshot.to_fold_point(range.start, Bias::Left),
+ "Failed with tab_point at column {ix}"
+ );
+ assert_eq!(
+ tab_snapshot.expected_to_fold_point(range.start, Bias::Right),
+ tab_snapshot.to_fold_point(range.start, Bias::Right),
+ "Failed with tab_point at column {ix}"
+ );
+
+ assert_eq!(
+ tab_snapshot.expected_to_fold_point(range.end, Bias::Left),
+ tab_snapshot.to_fold_point(range.end, Bias::Left),
+ "Failed with tab_point at column {ix}"
+ );
+ assert_eq!(
+ tab_snapshot.expected_to_fold_point(range.end, Bias::Right),
+ tab_snapshot.to_fold_point(range.end, Bias::Right),
+ "Failed with tab_point at column {ix}"
+ );
+ }
+ }
+
+ #[gpui::test]
+ fn test_to_fold_point_panic_reproduction(cx: &mut gpui::App) {
+ // This test reproduces a specific panic where to_fold_point returns incorrect results
+ let _text = "use macro_rules_attribute::apply;\nuse serde_json::Value;\nuse smol::{\n io::AsyncReadExt,\n process::{Command, Stdio},\n};\nuse smol_macros::main;\nuse std::io;\n\nfn test_random() {\n // Generate a random value\n let random_value = std::time::SystemTime::now()\n .duration_since(std::time::UNIX_EPOCH)\n .unwrap()\n .as_secs()\n % 100;\n\n // Create some complex nested data structures\n let mut vector = Vec::new();\n for i in 0..random_value {\n vector.push(i);\n }\n ";
+
+ let text = "γ\tw⭐\n🍐🍗 \t";
+ let buffer = MultiBuffer::build_simple(text, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+
+ // This should panic with the expected vs actual mismatch
+ let tab_point = TabPoint::new(0, 9);
+ let result = tab_snapshot.to_fold_point(tab_point, Bias::Left);
+ let expected = tab_snapshot.expected_to_fold_point(tab_point, Bias::Left);
+
+ assert_eq!(result, expected);
+ }
+
+ #[gpui::test(iterations = 100)]
+ fn test_collapse_tabs_random(cx: &mut gpui::App, mut rng: StdRng) {
+ // Generate random input string with up to 200 characters including tabs
+ // to stay within the MAX_EXPANSION_COLUMN limit of 256
+ let len = rng.random_range(0..=2048);
+ let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap();
+ let mut input = String::with_capacity(len);
+
+ for _ in 0..len {
+ if rng.random_bool(0.1) {
+ // 10% chance of inserting a tab
+ input.push('\t');
+ } else {
+ // 90% chance of inserting a random ASCII character (excluding tab, newline, carriage return)
+ let ch = loop {
+ let ascii_code = rng.random_range(32..=126); // printable ASCII range
+ let ch = ascii_code as u8 as char;
+ if ch != '\t' {
+ break ch;
+ }
+ };
+ input.push(ch);
+ }
+ }
+
+ let buffer = MultiBuffer::build_simple(&input, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+ tab_snapshot.max_expansion_column = rng.random_range(0..323);
+ tab_snapshot.tab_size = tab_size;
+
+ for (ix, _) in input.char_indices() {
+ let range = TabPoint::new(0, ix as u32)..tab_snapshot.max_point();
+
+ assert_eq!(
+ tab_snapshot.expected_to_fold_point(range.start, Bias::Left),
+ tab_snapshot.to_fold_point(range.start, Bias::Left),
+ "Failed with input: {}, with idx: {ix}",
+ input
+ );
+ assert_eq!(
+ tab_snapshot.expected_to_fold_point(range.start, Bias::Right),
+ tab_snapshot.to_fold_point(range.start, Bias::Right),
+ "Failed with input: {}, with idx: {ix}",
+ input
+ );
+
+ assert_eq!(
+ tab_snapshot.expected_to_fold_point(range.end, Bias::Left),
+ tab_snapshot.to_fold_point(range.end, Bias::Left),
+ "Failed with input: {}, with idx: {ix}",
+ input
+ );
+ assert_eq!(
+ tab_snapshot.expected_to_fold_point(range.end, Bias::Right),
+ tab_snapshot.to_fold_point(range.end, Bias::Right),
+ "Failed with input: {}, with idx: {ix}",
+ input
+ );
+ }
}
#[gpui::test]
@@ -628,7 +927,7 @@ mod tests {
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
@@ -675,7 +974,7 @@ mod tests {
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
@@ -689,7 +988,7 @@ mod tests {
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
- let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
@@ -736,9 +1035,9 @@ mod tests {
#[gpui::test(iterations = 100)]
fn test_random_tabs(cx: &mut gpui::App, mut rng: StdRng) {
- let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
- let len = rng.gen_range(0..30);
- let buffer = if rng.r#gen() {
+ let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap();
+ let len = rng.random_range(0..30);
+ let buffer = if rng.random() {
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
@@ -749,7 +1048,7 @@ mod tests {
let buffer_snapshot = buffer.read(cx).snapshot(cx);
log::info!("Buffer text: {:?}", buffer_snapshot.text());
- let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
fold_map.randomly_mutate(&mut rng);
@@ -758,7 +1057,7 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
- let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
+ let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
let text = text::Rope::from(tabs_snapshot.text().as_str());
@@ -769,11 +1068,11 @@ mod tests {
);
for _ in 0..5 {
- let end_row = rng.gen_range(0..=text.max_point().row);
- let end_column = rng.gen_range(0..=text.line_len(end_row));
+ let end_row = rng.random_range(0..=text.max_point().row);
+ let end_column = rng.random_range(0..=text.line_len(end_row));
let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
- let start_row = rng.gen_range(0..=text.max_point().row);
- let start_column = rng.gen_range(0..=text.line_len(start_row));
+ let start_row = rng.random_range(0..=text.max_point().row);
+ let start_column = rng.random_range(0..=text.line_len(start_row));
let mut start =
TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
if start > end {
@@ -811,4 +1110,479 @@ mod tests {
);
}
}
+
+ #[gpui::test(iterations = 100)]
+ fn test_to_tab_point_random(cx: &mut gpui::App, mut rng: StdRng) {
+ let tab_size = NonZeroU32::new(rng.random_range(1..=16)).unwrap();
+ let len = rng.random_range(0..=2000);
+
+ // Generate random text using RandomCharIter
+ let text = util::RandomCharIter::new(&mut rng)
+ .take(len)
+ .collect::<String>();
+
+ // Create buffer and tab map
+ let buffer = MultiBuffer::build_simple(&text, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (mut tab_map, _) = TabMap::new(fold_snapshot, tab_size);
+
+ let mut next_inlay_id = 0;
+ let (inlay_snapshot, inlay_edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let max_fold_point = fold_snapshot.max_point();
+ let (mut tab_snapshot, _) = tab_map.sync(fold_snapshot.clone(), fold_edits, tab_size);
+
+ // Test random fold points
+ for _ in 0..50 {
+ tab_snapshot.max_expansion_column = rng.random_range(0..=256);
+ // Generate random fold point
+ let row = rng.random_range(0..=max_fold_point.row());
+ let max_column = if row < max_fold_point.row() {
+ fold_snapshot.line_len(row)
+ } else {
+ max_fold_point.column()
+ };
+ let column = rng.random_range(0..=max_column + 10);
+ let fold_point = FoldPoint::new(row, column);
+
+ let actual = tab_snapshot.to_tab_point(fold_point);
+ let expected = tab_snapshot.expected_to_tab_point(fold_point);
+
+ assert_eq!(
+ actual, expected,
+ "to_tab_point mismatch for fold_point {:?} in text {:?}",
+ fold_point, text
+ );
+ }
+ }
+
+ #[gpui::test]
+ fn test_tab_stop_cursor_utf8(cx: &mut gpui::App) {
+ let text = "\tfoo\tbarbarbar\t\tbaz\n";
+ let buffer = MultiBuffer::build_simple(text, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let chunks = fold_snapshot.chunks(
+ FoldOffset(0)..fold_snapshot.len(),
+ false,
+ Default::default(),
+ );
+ let mut cursor = TabStopCursor::new(chunks);
+ assert!(cursor.seek(0).is_none());
+ let mut tab_stops = Vec::new();
+
+ let mut all_tab_stops = Vec::new();
+ let mut byte_offset = 0;
+ for (offset, ch) in buffer.read(cx).snapshot(cx).text().char_indices() {
+ byte_offset += ch.len_utf8() as u32;
+
+ if ch == '\t' {
+ all_tab_stops.push(TabStop {
+ byte_offset,
+ char_offset: offset as u32 + 1,
+ });
+ }
+ }
+
+ while let Some(tab_stop) = cursor.seek(u32::MAX) {
+ tab_stops.push(tab_stop);
+ }
+ pretty_assertions::assert_eq!(tab_stops.as_slice(), all_tab_stops.as_slice(),);
+
+ assert_eq!(cursor.byte_offset(), byte_offset);
+ }
+
+ #[gpui::test]
+ fn test_tab_stop_with_end_range_utf8(cx: &mut gpui::App) {
+ let input = "A\tBC\t"; // DEF\tG\tHI\tJ\tK\tL\tM
+
+ let buffer = MultiBuffer::build_simple(input, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+
+ let chunks = fold_snapshot.chunks_at(FoldPoint::new(0, 0));
+ let mut cursor = TabStopCursor::new(chunks);
+
+ let mut actual_tab_stops = Vec::new();
+
+ let mut expected_tab_stops = Vec::new();
+ let mut byte_offset = 0;
+ for (offset, ch) in buffer.read(cx).snapshot(cx).text().char_indices() {
+ byte_offset += ch.len_utf8() as u32;
+
+ if ch == '\t' {
+ expected_tab_stops.push(TabStop {
+ byte_offset,
+ char_offset: offset as u32 + 1,
+ });
+ }
+ }
+
+ while let Some(tab_stop) = cursor.seek(u32::MAX) {
+ actual_tab_stops.push(tab_stop);
+ }
+ pretty_assertions::assert_eq!(actual_tab_stops.as_slice(), expected_tab_stops.as_slice(),);
+
+ assert_eq!(cursor.byte_offset(), byte_offset);
+ }
+
+ #[gpui::test(iterations = 100)]
+ fn test_tab_stop_cursor_random_utf8(cx: &mut gpui::App, mut rng: StdRng) {
+ // Generate random input string with up to 512 characters including tabs
+ let len = rng.random_range(0..=2048);
+ let mut input = String::with_capacity(len);
+
+ let mut skip_tabs = rng.random_bool(0.10);
+ for idx in 0..len {
+ if idx % 128 == 0 {
+ skip_tabs = rng.random_bool(0.10);
+ }
+
+ if rng.random_bool(0.15) && !skip_tabs {
+ input.push('\t');
+ } else {
+ let ch = loop {
+ let ascii_code = rng.random_range(32..=126); // printable ASCII range
+ let ch = ascii_code as u8 as char;
+ if ch != '\t' {
+ break ch;
+ }
+ };
+ input.push(ch);
+ }
+ }
+
+ // Build the buffer and create cursor
+ let buffer = MultiBuffer::build_simple(&input, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+
+ // First, collect all expected tab positions
+ let mut all_tab_stops = Vec::new();
+ let mut byte_offset = 1;
+ let mut char_offset = 1;
+ for ch in buffer_snapshot.text().chars() {
+ if ch == '\t' {
+ all_tab_stops.push(TabStop {
+ byte_offset,
+ char_offset,
+ });
+ }
+ byte_offset += ch.len_utf8() as u32;
+ char_offset += 1;
+ }
+
+ // Test with various distances
+ let distances = vec![1, 5, 10, 50, 100, u32::MAX];
+ // let distances = vec![150];
+
+ for distance in distances {
+ let chunks = fold_snapshot.chunks_at(FoldPoint::new(0, 0));
+ let mut cursor = TabStopCursor::new(chunks);
+
+ let mut found_tab_stops = Vec::new();
+ let mut position = distance;
+ while let Some(tab_stop) = cursor.seek(position) {
+ found_tab_stops.push(tab_stop);
+ position = distance - tab_stop.byte_offset;
+ }
+
+ let expected_found_tab_stops: Vec<_> = all_tab_stops
+ .iter()
+ .take_while(|tab_stop| tab_stop.byte_offset <= distance)
+ .cloned()
+ .collect();
+
+ pretty_assertions::assert_eq!(
+ found_tab_stops,
+ expected_found_tab_stops,
+ "TabStopCursor output mismatch for distance {}. Input: {:?}",
+ distance,
+ input
+ );
+
+ let final_position = cursor.byte_offset();
+ if !found_tab_stops.is_empty() {
+ let last_tab_stop = found_tab_stops.last().unwrap();
+ assert!(
+ final_position >= last_tab_stop.byte_offset,
+ "Cursor final position {} is before last tab stop {}. Input: {:?}",
+ final_position,
+ last_tab_stop.byte_offset,
+ input
+ );
+ }
+ }
+ }
+
+ #[gpui::test]
+ fn test_tab_stop_cursor_utf16(cx: &mut gpui::App) {
+ let text = "\r\t😁foo\tb😀arbar🤯bar\t\tbaz\n";
+ let buffer = MultiBuffer::build_simple(text, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let chunks = fold_snapshot.chunks(
+ FoldOffset(0)..fold_snapshot.len(),
+ false,
+ Default::default(),
+ );
+ let mut cursor = TabStopCursor::new(chunks);
+ assert!(cursor.seek(0).is_none());
+
+ let mut expected_tab_stops = Vec::new();
+ let mut byte_offset = 0;
+ for (i, ch) in fold_snapshot.chars_at(FoldPoint::new(0, 0)).enumerate() {
+ byte_offset += ch.len_utf8() as u32;
+
+ if ch == '\t' {
+ expected_tab_stops.push(TabStop {
+ byte_offset,
+ char_offset: i as u32 + 1,
+ });
+ }
+ }
+
+ let mut actual_tab_stops = Vec::new();
+ while let Some(tab_stop) = cursor.seek(u32::MAX) {
+ actual_tab_stops.push(tab_stop);
+ }
+
+ pretty_assertions::assert_eq!(actual_tab_stops.as_slice(), expected_tab_stops.as_slice(),);
+
+ assert_eq!(cursor.byte_offset(), byte_offset);
+ }
+
+ #[gpui::test(iterations = 100)]
+ fn test_tab_stop_cursor_random_utf16(cx: &mut gpui::App, mut rng: StdRng) {
+ // Generate random input string with up to 512 characters including tabs
+ let len = rng.random_range(0..=2048);
+ let input = util::RandomCharIter::new(&mut rng)
+ .take(len)
+ .collect::<String>();
+
+ // Build the buffer and create cursor
+ let buffer = MultiBuffer::build_simple(&input, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+
+ // First, collect all expected tab positions
+ let mut all_tab_stops = Vec::new();
+ let mut byte_offset = 0;
+ for (i, ch) in buffer_snapshot.text().chars().enumerate() {
+ byte_offset += ch.len_utf8() as u32;
+ if ch == '\t' {
+ all_tab_stops.push(TabStop {
+ byte_offset,
+ char_offset: i as u32 + 1,
+ });
+ }
+ }
+
+ // Test with various distances
+ // let distances = vec![1, 5, 10, 50, 100, u32::MAX];
+ let distances = vec![150];
+
+ for distance in distances {
+ let chunks = fold_snapshot.chunks_at(FoldPoint::new(0, 0));
+ let mut cursor = TabStopCursor::new(chunks);
+
+ let mut found_tab_stops = Vec::new();
+ let mut position = distance;
+ while let Some(tab_stop) = cursor.seek(position) {
+ found_tab_stops.push(tab_stop);
+ position = distance - tab_stop.byte_offset;
+ }
+
+ let expected_found_tab_stops: Vec<_> = all_tab_stops
+ .iter()
+ .take_while(|tab_stop| tab_stop.byte_offset <= distance)
+ .cloned()
+ .collect();
+
+ pretty_assertions::assert_eq!(
+ found_tab_stops,
+ expected_found_tab_stops,
+ "TabStopCursor output mismatch for distance {}. Input: {:?}",
+ distance,
+ input
+ );
+
+ let final_position = cursor.byte_offset();
+ if !found_tab_stops.is_empty() {
+ let last_tab_stop = found_tab_stops.last().unwrap();
+ assert!(
+ final_position >= last_tab_stop.byte_offset,
+ "Cursor final position {} is before last tab stop {}. Input: {:?}",
+ final_position,
+ last_tab_stop.byte_offset,
+ input
+ );
+ }
+ }
+ }
+}
+
+struct TabStopCursor<'a, I>
+where
+ I: Iterator<Item = Chunk<'a>>,
+{
+ chunks: I,
+ byte_offset: u32,
+ char_offset: u32,
+ /// Chunk
+ /// last tab position iterated through
+ current_chunk: Option<(Chunk<'a>, u32)>,
+}
+
+impl<'a, I> TabStopCursor<'a, I>
+where
+ I: Iterator<Item = Chunk<'a>>,
+{
+ fn new(chunks: impl IntoIterator<Item = Chunk<'a>, IntoIter = I>) -> Self {
+ Self {
+ chunks: chunks.into_iter(),
+ byte_offset: 0,
+ char_offset: 0,
+ current_chunk: None,
+ }
+ }
+
+ fn bytes_until_next_char(&self) -> Option<usize> {
+ self.current_chunk.as_ref().and_then(|(chunk, idx)| {
+ let mut idx = *idx;
+ let mut diff = 0;
+ while idx > 0 && chunk.chars & (1 << idx) == 0 {
+ idx -= 1;
+ diff += 1;
+ }
+
+ if chunk.chars & (1 << idx) != 0 {
+ Some(
+ (chunk.text[idx as usize..].chars().next()?)
+ .len_utf8()
+ .saturating_sub(diff),
+ )
+ } else {
+ None
+ }
+ })
+ }
+
+ fn is_char_boundary(&self) -> bool {
+ self.current_chunk
+ .as_ref()
+ .is_some_and(|(chunk, idx)| (chunk.chars & (1 << *idx.min(&127))) != 0)
+ }
+
+ /// distance: length to move forward while searching for the next tab stop
+ fn seek(&mut self, distance: u32) -> Option<TabStop> {
+ if distance == 0 {
+ return None;
+ }
+
+ let mut distance_traversed = 0;
+
+ while let Some((mut chunk, chunk_position)) = self
+ .current_chunk
+ .take()
+ .or_else(|| self.chunks.next().zip(Some(0)))
+ {
+ if chunk.tabs == 0 {
+ let chunk_distance = chunk.text.len() as u32 - chunk_position;
+ if chunk_distance + distance_traversed >= distance {
+ let overshoot = distance_traversed.abs_diff(distance);
+
+ self.byte_offset += overshoot;
+ self.char_offset += get_char_offset(
+ chunk_position..(chunk_position + overshoot).saturating_sub(1).min(127),
+ chunk.chars,
+ );
+
+ self.current_chunk = Some((chunk, chunk_position + overshoot));
+
+ return None;
+ }
+
+ self.byte_offset += chunk_distance;
+ self.char_offset += get_char_offset(
+ chunk_position..(chunk_position + chunk_distance).saturating_sub(1).min(127),
+ chunk.chars,
+ );
+ distance_traversed += chunk_distance;
+ continue;
+ }
+ let tab_position = chunk.tabs.trailing_zeros() + 1;
+
+ if distance_traversed + tab_position - chunk_position > distance {
+ let cursor_position = distance_traversed.abs_diff(distance);
+
+ self.char_offset += get_char_offset(
+ chunk_position..(chunk_position + cursor_position - 1),
+ chunk.chars,
+ );
+ self.current_chunk = Some((chunk, cursor_position + chunk_position));
+ self.byte_offset += cursor_position;
+
+ return None;
+ }
+
+ self.byte_offset += tab_position - chunk_position;
+ self.char_offset += get_char_offset(chunk_position..(tab_position - 1), chunk.chars);
+
+ let tabstop = TabStop {
+ char_offset: self.char_offset,
+ byte_offset: self.byte_offset,
+ };
+
+ chunk.tabs = (chunk.tabs - 1) & chunk.tabs;
+
+ if tab_position as usize != chunk.text.len() {
+ self.current_chunk = Some((chunk, tab_position));
+ }
+
+ return Some(tabstop);
+ }
+
+ None
+ }
+
+ fn byte_offset(&self) -> u32 {
+ self.byte_offset
+ }
+
+ fn char_offset(&self) -> u32 {
+ self.char_offset
+ }
+}
+
+#[inline(always)]
+fn get_char_offset(range: Range<u32>, bit_map: u128) -> u32 {
+ // This edge case can happen when we're at chunk position 128
+
+ if range.start == range.end {
+ return if (1u128 << range.start) & bit_map == 0 {
+ 0
+ } else {
+ 1
+ };
+ }
+ let end_shift: u128 = 127u128 - range.end.min(127) as u128;
+ let mut bit_mask = (u128::MAX >> range.start) << range.start;
+ bit_mask = (bit_mask << end_shift) >> end_shift;
+ let bit_map = bit_map & bit_mask;
+
+ bit_map.count_ones()
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+struct TabStop {
+ char_offset: u32,
+ byte_offset: u32,
}
@@ -74,10 +74,10 @@ impl WrapRows<'_> {
self.transforms
.seek(&WrapPoint::new(start_row, 0), Bias::Left);
let mut input_row = self.transforms.start().1.row();
- if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
+ if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
input_row += start_row - self.transforms.start().0.row();
}
- self.soft_wrapped = self.transforms.item().map_or(false, |t| !t.is_isomorphic());
+ self.soft_wrapped = self.transforms.item().is_some_and(|t| !t.is_isomorphic());
self.input_buffer_rows.seek(input_row);
self.input_buffer_row = self.input_buffer_rows.next().unwrap();
self.output_row = start_row;
@@ -249,48 +249,48 @@ impl WrapMap {
return;
}
- if let Some(wrap_width) = self.wrap_width {
- if self.background_task.is_none() {
- let pending_edits = self.pending_edits.clone();
- let mut snapshot = self.snapshot.clone();
- let text_system = cx.text_system().clone();
- let (font, font_size) = self.font_with_size.clone();
- let update_task = cx.background_spawn(async move {
- let mut edits = Patch::default();
- let mut line_wrapper = text_system.line_wrapper(font, font_size);
- for (tab_snapshot, tab_edits) in pending_edits {
- let wrap_edits = snapshot
- .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
- .await;
- edits = edits.compose(&wrap_edits);
- }
- (snapshot, edits)
- });
+ if let Some(wrap_width) = self.wrap_width
+ && self.background_task.is_none()
+ {
+ let pending_edits = self.pending_edits.clone();
+ let mut snapshot = self.snapshot.clone();
+ let text_system = cx.text_system().clone();
+ let (font, font_size) = self.font_with_size.clone();
+ let update_task = cx.background_spawn(async move {
+ let mut edits = Patch::default();
+ let mut line_wrapper = text_system.line_wrapper(font, font_size);
+ for (tab_snapshot, tab_edits) in pending_edits {
+ let wrap_edits = snapshot
+ .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
+ .await;
+ edits = edits.compose(&wrap_edits);
+ }
+ (snapshot, edits)
+ });
- match cx
- .background_executor()
- .block_with_timeout(Duration::from_millis(1), update_task)
- {
- Ok((snapshot, output_edits)) => {
- self.snapshot = snapshot;
- self.edits_since_sync = self.edits_since_sync.compose(&output_edits);
- }
- Err(update_task) => {
- self.background_task = Some(cx.spawn(async move |this, cx| {
- let (snapshot, edits) = update_task.await;
- this.update(cx, |this, cx| {
- this.snapshot = snapshot;
- this.edits_since_sync = this
- .edits_since_sync
- .compose(mem::take(&mut this.interpolated_edits).invert())
- .compose(&edits);
- this.background_task = None;
- this.flush_edits(cx);
- cx.notify();
- })
- .ok();
- }));
- }
+ match cx
+ .background_executor()
+ .block_with_timeout(Duration::from_millis(1), update_task)
+ {
+ Ok((snapshot, output_edits)) => {
+ self.snapshot = snapshot;
+ self.edits_since_sync = self.edits_since_sync.compose(&output_edits);
+ }
+ Err(update_task) => {
+ self.background_task = Some(cx.spawn(async move |this, cx| {
+ let (snapshot, edits) = update_task.await;
+ this.update(cx, |this, cx| {
+ this.snapshot = snapshot;
+ this.edits_since_sync = this
+ .edits_since_sync
+ .compose(mem::take(&mut this.interpolated_edits).invert())
+ .compose(&edits);
+ this.background_task = None;
+ this.flush_edits(cx);
+ cx.notify();
+ })
+ .ok();
+ }));
}
}
}
@@ -603,7 +603,7 @@ impl WrapSnapshot {
.cursor::<Dimensions<WrapPoint, TabPoint>>(&());
transforms.seek(&output_start, Bias::Right);
let mut input_start = TabPoint(transforms.start().1.0);
- if transforms.item().map_or(false, |t| t.is_isomorphic()) {
+ if transforms.item().is_some_and(|t| t.is_isomorphic()) {
input_start.0 += output_start.0 - transforms.start().0.0;
}
let input_end = self
@@ -634,7 +634,7 @@ impl WrapSnapshot {
cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left);
if cursor
.item()
- .map_or(false, |transform| transform.is_isomorphic())
+ .is_some_and(|transform| transform.is_isomorphic())
{
let overshoot = row - cursor.start().0.row();
let tab_row = cursor.start().1.row() + overshoot;
@@ -732,10 +732,10 @@ impl WrapSnapshot {
.cursor::<Dimensions<WrapPoint, TabPoint>>(&());
transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left);
let mut input_row = transforms.start().1.row();
- if transforms.item().map_or(false, |t| t.is_isomorphic()) {
+ if transforms.item().is_some_and(|t| t.is_isomorphic()) {
input_row += start_row - transforms.start().0.row();
}
- let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic());
+ let soft_wrapped = transforms.item().is_some_and(|t| !t.is_isomorphic());
let mut input_buffer_rows = self.tab_snapshot.rows(input_row);
let input_buffer_row = input_buffer_rows.next().unwrap();
WrapRows {
@@ -754,7 +754,7 @@ impl WrapSnapshot {
.cursor::<Dimensions<WrapPoint, TabPoint>>(&());
cursor.seek(&point, Bias::Right);
let mut tab_point = cursor.start().1.0;
- if cursor.item().map_or(false, |t| t.is_isomorphic()) {
+ if cursor.item().is_some_and(|t| t.is_isomorphic()) {
tab_point += point.0 - cursor.start().0.0;
}
TabPoint(tab_point)
@@ -780,7 +780,7 @@ impl WrapSnapshot {
if bias == Bias::Left {
let mut cursor = self.transforms.cursor::<WrapPoint>(&());
cursor.seek(&point, Bias::Right);
- if cursor.item().map_or(false, |t| !t.is_isomorphic()) {
+ if cursor.item().is_some_and(|t| !t.is_isomorphic()) {
point = *cursor.start();
*point.column_mut() -= 1;
}
@@ -901,7 +901,7 @@ impl WrapChunks<'_> {
let output_end = WrapPoint::new(rows.end, 0);
self.transforms.seek(&output_start, Bias::Right);
let mut input_start = TabPoint(self.transforms.start().1.0);
- if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
+ if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
input_start.0 += output_start.0 - self.transforms.start().0.0;
}
let input_end = self
@@ -970,9 +970,25 @@ impl<'a> Iterator for WrapChunks<'a> {
}
let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
+
+ let (chars, tabs) = if input_len == 128 {
+ let output = (self.input_chunk.chars, self.input_chunk.tabs);
+ self.input_chunk.chars = 0;
+ self.input_chunk.tabs = 0;
+ output
+ } else {
+ let mask = (1 << input_len) - 1;
+ let output = (self.input_chunk.chars & mask, self.input_chunk.tabs & mask);
+ self.input_chunk.chars = self.input_chunk.chars >> input_len;
+ self.input_chunk.tabs = self.input_chunk.tabs >> input_len;
+ output
+ };
+
self.input_chunk.text = suffix;
Some(Chunk {
text: prefix,
+ chars,
+ tabs,
..self.input_chunk.clone()
})
}
@@ -993,7 +1009,7 @@ impl Iterator for WrapRows<'_> {
self.output_row += 1;
self.transforms
.seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left);
- if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
+ if self.transforms.item().is_some_and(|t| t.is_isomorphic()) {
self.input_buffer_row = self.input_buffer_rows.next().unwrap();
self.soft_wrapped = false;
} else {
@@ -1065,12 +1081,12 @@ impl sum_tree::Item for Transform {
}
fn push_isomorphic(transforms: &mut Vec<Transform>, summary: TextSummary) {
- if let Some(last_transform) = transforms.last_mut() {
- if last_transform.is_isomorphic() {
- last_transform.summary.input += &summary;
- last_transform.summary.output += &summary;
- return;
- }
+ if let Some(last_transform) = transforms.last_mut()
+ && last_transform.is_isomorphic()
+ {
+ last_transform.summary.input += &summary;
+ last_transform.summary.output += &summary;
+ return;
}
transforms.push(Transform::isomorphic(summary));
}
@@ -1215,12 +1231,12 @@ mod tests {
.unwrap_or(10);
let text_system = cx.read(|cx| cx.text_system().clone());
- let mut wrap_width = if rng.gen_bool(0.1) {
+ let mut wrap_width = if rng.random_bool(0.1) {
None
} else {
- Some(px(rng.gen_range(0.0..=1000.0)))
+ Some(px(rng.random_range(0.0..=1000.0)))
};
- let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
+ let tab_size = NonZeroU32::new(rng.random_range(1..=4)).unwrap();
let font = test_font();
let _font_id = text_system.resolve_font(&font);
@@ -1230,10 +1246,10 @@ mod tests {
log::info!("Wrap width: {:?}", wrap_width);
let buffer = cx.update(|cx| {
- if rng.r#gen() {
+ if rng.random() {
MultiBuffer::build_random(&mut rng, cx)
} else {
- let len = rng.gen_range(0..10);
+ let len = rng.random_range(0..10);
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
@@ -1281,12 +1297,12 @@ mod tests {
log::info!("{} ==============================================", _i);
let mut buffer_edits = Vec::new();
- match rng.gen_range(0..=100) {
+ match rng.random_range(0..=100) {
0..=19 => {
- wrap_width = if rng.gen_bool(0.2) {
+ wrap_width = if rng.random_bool(0.2) {
None
} else {
- Some(px(rng.gen_range(0.0..=1000.0)))
+ Some(px(rng.random_range(0.0..=1000.0)))
};
log::info!("Setting wrap width to {:?}", wrap_width);
wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
@@ -1317,7 +1333,7 @@ mod tests {
_ => {
buffer.update(cx, |buffer, cx| {
let subscription = buffer.subscribe();
- let edit_count = rng.gen_range(1..=5);
+ let edit_count = rng.random_range(1..=5);
buffer.randomly_mutate(&mut rng, edit_count, cx);
buffer_snapshot = buffer.snapshot(cx);
buffer_edits.extend(subscription.consume());
@@ -1341,7 +1357,7 @@ mod tests {
snapshot.verify_chunks(&mut rng);
edits.push((snapshot, wrap_edits));
- if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
+ if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.random_bool(0.4) {
log::info!("Waiting for wrapping to finish");
while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
notifications.next().await.unwrap();
@@ -1461,7 +1477,7 @@ mod tests {
}
let mut prev_ix = 0;
- for boundary in line_wrapper.wrap_line(&[LineFragment::text(&line)], wrap_width) {
+ for boundary in line_wrapper.wrap_line(&[LineFragment::text(line)], wrap_width) {
wrapped_text.push_str(&line[prev_ix..boundary.ix]);
wrapped_text.push('\n');
wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
@@ -1479,8 +1495,8 @@ mod tests {
impl WrapSnapshot {
fn verify_chunks(&mut self, rng: &mut impl Rng) {
for _ in 0..5 {
- let mut end_row = rng.gen_range(0..=self.max_point().row());
- let start_row = rng.gen_range(0..=end_row);
+ let mut end_row = rng.random_range(0..=self.max_point().row());
+ let start_row = rng.random_range(0..=end_row);
end_row += 1;
let mut expected_text = self.text_chunks(start_row).collect::<String>();
@@ -7,7 +7,9 @@ use std::ops::Range;
use text::{Point, ToOffset};
use crate::{
- EditPrediction, editor_tests::init_test, test::editor_test_context::EditorTestContext,
+ EditPrediction,
+ editor_tests::{init_test, update_test_language_settings},
+ test::editor_test_context::EditorTestContext,
};
#[gpui::test]
@@ -271,6 +273,44 @@ async fn test_edit_prediction_jump_disabled_for_non_zed_providers(cx: &mut gpui:
});
}
+#[gpui::test]
+async fn test_edit_predictions_disabled_in_scope(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ update_test_language_settings(cx, |settings| {
+ settings.defaults.edit_predictions_disabled_in = Some(vec!["string".to_string()]);
+ });
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let provider = cx.new(|_| FakeEditPredictionProvider::default());
+ assign_editor_completion_provider(provider.clone(), &mut cx);
+
+ let language = languages::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into());
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+ // Test disabled inside of string
+ cx.set_state("const x = \"hello ˇworld\";");
+ propose_edits(&provider, vec![(17..17, "beautiful ")], &mut cx);
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+ cx.editor(|editor, _, _| {
+ assert!(
+ editor.active_edit_prediction.is_none(),
+ "Edit predictions should be disabled in string scopes when configured in edit_predictions_disabled_in"
+ );
+ });
+
+ // Test enabled outside of string
+ cx.set_state("const x = \"hello world\"; ˇ");
+ propose_edits(&provider, vec![(24..24, "// comment")], &mut cx);
+ cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
+ cx.editor(|editor, _, _| {
+ assert!(
+ editor.active_edit_prediction.is_some(),
+ "Edit predictions should work outside of disabled scopes"
+ );
+ });
+}
+
fn assert_editor_active_edit_completion(
cx: &mut EditorTestContext,
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
@@ -147,23 +147,24 @@ use multi_buffer::{
use parking_lot::Mutex;
use persistence::DB;
use project::{
- BreakpointWithPosition, CodeAction, Completion, CompletionIntent, CompletionResponse,
- CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, Location, LocationLink,
- PrepareRenameResponse, Project, ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind,
- debugger::breakpoint_store::Breakpoint,
+ BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent,
+ CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint,
+ Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectPath,
+ ProjectTransaction, TaskSourceKind,
debugger::{
breakpoint_store::{
- BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
- BreakpointStoreEvent,
+ Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState,
+ BreakpointStore, BreakpointStoreEvent,
},
session::{Session, SessionEvent},
},
git_store::{GitStoreEvent, RepositoryEvent},
lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
- project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter},
- project_settings::{GitGutterSetting, ProjectSettings},
+ project_settings::{
+ DiagnosticSeverity, GitGutterSetting, GoToDiagnosticSeverityFilter, ProjectSettings,
+ },
};
-use rand::{seq::SliceRandom, thread_rng};
+use rand::seq::SliceRandom;
use rpc::{ErrorCode, ErrorExt, proto::PeerId};
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
use selections_collection::{
@@ -189,7 +190,6 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
-use sum_tree::TreeMap;
use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
use text::{BufferId, FromAnchor, OffsetUtf16, Rope};
use theme::{
@@ -219,7 +219,6 @@ use crate::{
pub const FILE_HEADER_HEIGHT: u32 = 2;
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1;
-pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
const MAX_LINE_LEN: usize = 1024;
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
@@ -227,7 +226,7 @@ const MAX_SELECTION_HISTORY_LEN: usize = 1024;
pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000);
#[doc(hidden)]
pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
-const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
+pub const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5);
@@ -253,7 +252,6 @@ pub type RenderDiffHunkControlsFn = Arc<
enum ReportEditorEvent {
Saved { auto_saved: bool },
EditorOpened,
- ZetaTosClicked,
Closed,
}
@@ -262,7 +260,6 @@ impl ReportEditorEvent {
match self {
Self::Saved { .. } => "Editor Saved",
Self::EditorOpened => "Editor Opened",
- Self::ZetaTosClicked => "Edit Prediction Provider ToS Clicked",
Self::Closed => "Editor Closed",
}
}
@@ -782,10 +779,7 @@ impl MinimapVisibility {
}
fn disabled(&self) -> bool {
- match *self {
- Self::Disabled => true,
- _ => false,
- }
+ matches!(*self, Self::Disabled)
}
fn settings_visibility(&self) -> bool {
@@ -942,10 +936,10 @@ impl ChangeList {
}
pub fn invert_last_group(&mut self) {
- if let Some(last) = self.changes.last_mut() {
- if let Some(current) = last.current.as_mut() {
- mem::swap(&mut last.original, current);
- }
+ if let Some(last) = self.changes.last_mut()
+ && let Some(current) = last.current.as_mut()
+ {
+ mem::swap(&mut last.original, current);
}
}
}
@@ -1014,6 +1008,7 @@ pub struct Editor {
/// Map of how text in the buffer should be displayed.
/// Handles soft wraps, folds, fake inlay text insertions, etc.
pub display_map: Entity<DisplayMap>,
+ placeholder_display_map: Option<Entity<DisplayMap>>,
pub selections: SelectionsCollection,
pub scroll_manager: ScrollManager,
/// When inline assist editors are linked, they all render cursors because
@@ -1036,12 +1031,11 @@ pub struct Editor {
inline_diagnostics_update: Task<()>,
inline_diagnostics_enabled: bool,
diagnostics_enabled: bool,
+ word_completions_enabled: bool,
inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
hard_wrap: Option<usize>,
-
- // TODO: make this a access method
- pub project: Option<Entity<Project>>,
+ project: Option<Entity<Project>>,
semantics_provider: Option<Rc<dyn SemanticsProvider>>,
completion_provider: Option<Rc<dyn CompletionProvider>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>,
@@ -1064,11 +1058,10 @@ pub struct Editor {
show_breakpoints: Option<bool>,
show_wrap_guides: Option<bool>,
show_indent_guides: Option<bool>,
- placeholder_text: Option<Arc<str>>,
highlight_order: usize,
highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
- background_highlights: TreeMap<HighlightKey, BackgroundHighlight>,
- gutter_highlights: TreeMap<TypeId, GutterHighlight>,
+ background_highlights: HashMap<HighlightKey, BackgroundHighlight>,
+ gutter_highlights: HashMap<TypeId, GutterHighlight>,
scrollbar_marker_state: ScrollbarMarkerState,
active_indent_guides_state: ActiveIndentGuidesState,
nav_history: Option<ItemNavHistory>,
@@ -1216,7 +1209,7 @@ pub struct EditorSnapshot {
show_breakpoints: Option<bool>,
git_blame_gutter_max_author_length: Option<usize>,
pub display_snapshot: DisplaySnapshot,
- pub placeholder_text: Option<Arc<str>>,
+ pub placeholder_display_snapshot: Option<DisplaySnapshot>,
is_focused: bool,
scroll_anchor: ScrollAnchor,
ongoing_scroll: OngoingScroll,
@@ -1431,7 +1424,7 @@ impl SelectionHistory {
if self
.undo_stack
.back()
- .map_or(true, |e| e.selections != entry.selections)
+ .is_none_or(|e| e.selections != entry.selections)
{
self.undo_stack.push_back(entry);
if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN {
@@ -1444,7 +1437,7 @@ impl SelectionHistory {
if self
.redo_stack
.back()
- .map_or(true, |e| e.selections != entry.selections)
+ .is_none_or(|e| e.selections != entry.selections)
{
self.redo_stack.push_back(entry);
if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN {
@@ -1802,7 +1795,7 @@ impl Editor {
let font_size = style.font_size.to_pixels(window.rem_size());
let editor = cx.entity().downgrade();
let fold_placeholder = FoldPlaceholder {
- constrain_width: true,
+ constrain_width: false,
render: Arc::new(move |fold_id, fold_range, cx| {
let editor = editor.clone();
div()
@@ -1859,118 +1852,166 @@ impl Editor {
blink_manager
});
- let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. })
- .then(|| language_settings::SoftWrap::None);
+ let soft_wrap_mode_override =
+ matches!(mode, EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
let mut project_subscriptions = Vec::new();
- if full_mode {
- if let Some(project) = project.as_ref() {
- project_subscriptions.push(cx.subscribe_in(
- project,
- window,
- |editor, _, event, window, cx| match event {
- project::Event::RefreshCodeLens => {
- // we always query lens with actions, without storing them, always refreshing them
- }
- project::Event::RefreshInlayHints => {
- editor
- .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
- }
- project::Event::LanguageServerAdded(..)
- | project::Event::LanguageServerRemoved(..) => {
- if editor.tasks_update_task.is_none() {
- editor.tasks_update_task =
- Some(editor.refresh_runnables(window, cx));
- }
+ if full_mode && let Some(project) = project.as_ref() {
+ project_subscriptions.push(cx.subscribe_in(
+ project,
+ window,
+ |editor, _, event, window, cx| match event {
+ project::Event::RefreshCodeLens => {
+ // we always query lens with actions, without storing them, always refreshing them
+ }
+ project::Event::RefreshInlayHints => {
+ editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
+ }
+ project::Event::LanguageServerAdded(..)
+ | project::Event::LanguageServerRemoved(..) => {
+ if editor.tasks_update_task.is_none() {
+ editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
}
- project::Event::SnippetEdit(id, snippet_edits) => {
- if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
- let focus_handle = editor.focus_handle(cx);
- if focus_handle.is_focused(window) {
- let snapshot = buffer.read(cx).snapshot();
- for (range, snippet) in snippet_edits {
- let editor_range =
- language::range_from_lsp(*range).to_offset(&snapshot);
- editor
- .insert_snippet(
- &[editor_range],
- snippet.clone(),
- window,
- cx,
- )
- .ok();
- }
+ }
+ project::Event::SnippetEdit(id, snippet_edits) => {
+ if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
+ let focus_handle = editor.focus_handle(cx);
+ if focus_handle.is_focused(window) {
+ let snapshot = buffer.read(cx).snapshot();
+ for (range, snippet) in snippet_edits {
+ let editor_range =
+ language::range_from_lsp(*range).to_offset(&snapshot);
+ editor
+ .insert_snippet(
+ &[editor_range],
+ snippet.clone(),
+ window,
+ cx,
+ )
+ .ok();
}
}
}
- project::Event::LanguageServerBufferRegistered { buffer_id, .. } => {
- if editor.buffer().read(cx).buffer(*buffer_id).is_some() {
- editor.update_lsp_data(false, Some(*buffer_id), window, cx);
- }
+ }
+ project::Event::LanguageServerBufferRegistered { buffer_id, .. } => {
+ if editor.buffer().read(cx).buffer(*buffer_id).is_some() {
+ editor.update_lsp_data(false, Some(*buffer_id), window, cx);
}
- _ => {}
- },
- ));
- if let Some(task_inventory) = project
- .read(cx)
- .task_store()
- .read(cx)
- .task_inventory()
- .cloned()
- {
- project_subscriptions.push(cx.observe_in(
- &task_inventory,
- window,
- |editor, _, window, cx| {
- editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
- },
- ));
- };
+ }
- project_subscriptions.push(cx.subscribe_in(
- &project.read(cx).breakpoint_store(),
- window,
- |editor, _, event, window, cx| match event {
- BreakpointStoreEvent::ClearDebugLines => {
- editor.clear_row_highlights::<ActiveDebugLine>();
- editor.refresh_inline_values(cx);
- }
- BreakpointStoreEvent::SetDebugLine => {
- if editor.go_to_active_debug_line(window, cx) {
- cx.stop_propagation();
- }
+ project::Event::EntryRenamed(transaction) => {
+ let Some(workspace) = editor.workspace() else {
+ return;
+ };
+ let Some(active_editor) = workspace.read(cx).active_item_as::<Self>(cx)
+ else {
+ return;
+ };
+ if active_editor.entity_id() == cx.entity_id() {
+ let edited_buffers_already_open = {
+ let other_editors: Vec<Entity<Editor>> = workspace
+ .read(cx)
+ .panes()
+ .iter()
+ .flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
+ .filter(|editor| editor.entity_id() != cx.entity_id())
+ .collect();
+
+ transaction.0.keys().all(|buffer| {
+ other_editors.iter().any(|editor| {
+ let multi_buffer = editor.read(cx).buffer();
+ multi_buffer.read(cx).is_singleton()
+ && multi_buffer.read(cx).as_singleton().map_or(
+ false,
+ |singleton| {
+ singleton.entity_id() == buffer.entity_id()
+ },
+ )
+ })
+ })
+ };
- editor.refresh_inline_values(cx);
+ if !edited_buffers_already_open {
+ let workspace = workspace.downgrade();
+ let transaction = transaction.clone();
+ cx.defer_in(window, move |_, window, cx| {
+ cx.spawn_in(window, async move |editor, cx| {
+ Self::open_project_transaction(
+ &editor,
+ workspace,
+ transaction,
+ "Rename".to_string(),
+ cx,
+ )
+ .await
+ .ok()
+ })
+ .detach();
+ });
+ }
}
- _ => {}
+ }
+
+ _ => {}
+ },
+ ));
+ if let Some(task_inventory) = project
+ .read(cx)
+ .task_store()
+ .read(cx)
+ .task_inventory()
+ .cloned()
+ {
+ project_subscriptions.push(cx.observe_in(
+ &task_inventory,
+ window,
+ |editor, _, window, cx| {
+ editor.tasks_update_task = Some(editor.refresh_runnables(window, cx));
},
));
- let git_store = project.read(cx).git_store().clone();
- let project = project.clone();
- project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| {
- match event {
- GitStoreEvent::RepositoryUpdated(
- _,
- RepositoryEvent::Updated {
- new_instance: true, ..
- },
- _,
- ) => {
- this.load_diff_task = Some(
- update_uncommitted_diff_for_buffer(
- cx.entity(),
- &project,
- this.buffer.read(cx).all_buffers(),
- this.buffer.clone(),
- cx,
- )
- .shared(),
- );
+ };
+
+ project_subscriptions.push(cx.subscribe_in(
+ &project.read(cx).breakpoint_store(),
+ window,
+ |editor, _, event, window, cx| match event {
+ BreakpointStoreEvent::ClearDebugLines => {
+ editor.clear_row_highlights::<ActiveDebugLine>();
+ editor.refresh_inline_values(cx);
+ }
+ BreakpointStoreEvent::SetDebugLine => {
+ if editor.go_to_active_debug_line(window, cx) {
+ cx.stop_propagation();
}
- _ => {}
+
+ editor.refresh_inline_values(cx);
}
- }));
- }
+ _ => {}
+ },
+ ));
+ let git_store = project.read(cx).git_store().clone();
+ let project = project.clone();
+ project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| {
+ if let GitStoreEvent::RepositoryUpdated(
+ _,
+ RepositoryEvent::Updated {
+ new_instance: true, ..
+ },
+ _,
+ ) = event
+ {
+ this.load_diff_task = Some(
+ update_uncommitted_diff_for_buffer(
+ cx.entity(),
+ &project,
+ this.buffer.read(cx).all_buffers(),
+ this.buffer.clone(),
+ cx,
+ )
+ .shared(),
+ );
+ }
+ }));
}
let buffer_snapshot = buffer.read(cx).snapshot(cx);
@@ -1991,14 +2032,12 @@ impl Editor {
.detach();
}
- let show_indent_guides = if matches!(
- mode,
- EditorMode::SingleLine { .. } | EditorMode::Minimap { .. }
- ) {
- Some(false)
- } else {
- None
- };
+ let show_indent_guides =
+ if matches!(mode, EditorMode::SingleLine | EditorMode::Minimap { .. }) {
+ Some(false)
+ } else {
+ None
+ };
let breakpoint_store = match (&mode, project.as_ref()) {
(EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()),
@@ -2027,6 +2066,7 @@ impl Editor {
last_focused_descendant: None,
buffer: buffer.clone(),
display_map: display_map.clone(),
+ placeholder_display_map: None,
selections,
scroll_manager: ScrollManager::new(cx),
columnar_selection_state: None,
@@ -2058,7 +2098,7 @@ impl Editor {
vertical: full_mode,
},
minimap_visibility: MinimapVisibility::for_mode(&mode, cx),
- offset_content: !matches!(mode, EditorMode::SingleLine { .. }),
+ offset_content: !matches!(mode, EditorMode::SingleLine),
show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs,
show_gutter: full_mode,
show_line_numbers: (!full_mode).then_some(false),
@@ -2070,11 +2110,10 @@ impl Editor {
show_breakpoints: None,
show_wrap_guides: None,
show_indent_guides,
- placeholder_text: None,
highlight_order: 0,
highlighted_rows: HashMap::default(),
- background_highlights: TreeMap::default(),
- gutter_highlights: TreeMap::default(),
+ background_highlights: HashMap::default(),
+ gutter_highlights: HashMap::default(),
scrollbar_marker_state: ScrollbarMarkerState::default(),
active_indent_guides_state: ActiveIndentGuidesState::default(),
nav_history: None,
@@ -2125,6 +2164,7 @@ impl Editor {
},
inline_diagnostics_enabled: full_mode,
diagnostics_enabled: full_mode,
+ word_completions_enabled: full_mode,
inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints),
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
@@ -2325,15 +2365,15 @@ impl Editor {
editor.go_to_active_debug_line(window, cx);
- if let Some(buffer) = buffer.read(cx).as_singleton() {
- if let Some(project) = editor.project.as_ref() {
- let handle = project.update(cx, |project, cx| {
- project.register_buffer_with_language_servers(&buffer, cx)
- });
- editor
- .registered_buffers
- .insert(buffer.read(cx).remote_id(), handle);
- }
+ if let Some(buffer) = buffer.read(cx).as_singleton()
+ && let Some(project) = editor.project()
+ {
+ let handle = project.update(cx, |project, cx| {
+ project.register_buffer_with_language_servers(&buffer, cx)
+ });
+ editor
+ .registered_buffers
+ .insert(buffer.read(cx).remote_id(), handle);
}
editor.minimap =
@@ -2371,6 +2411,34 @@ impl Editor {
.is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window))
}
+ pub fn is_range_selected(&mut self, range: &Range<Anchor>, cx: &mut Context<Self>) -> bool {
+ if self
+ .selections
+ .pending
+ .as_ref()
+ .is_some_and(|pending_selection| {
+ let snapshot = self.buffer().read(cx).snapshot(cx);
+ pending_selection
+ .selection
+ .range()
+ .includes(range, &snapshot)
+ })
+ {
+ return true;
+ }
+
+ self.selections
+ .disjoint_in_range::<usize>(range.clone(), cx)
+ .into_iter()
+ .any(|selection| {
+ // This is needed to cover a corner case, if we just check for an existing
+ // selection in the fold range, having a cursor at the start of the fold
+ // marks it as selected. Non-empty selections don't cause this.
+ let length = selection.end - selection.start;
+ length > 0
+ })
+ }
+
pub fn key_context(&self, window: &Window, cx: &App) -> KeyContext {
self.key_context_internal(self.has_active_edit_prediction(), window, cx)
}
@@ -2384,7 +2452,7 @@ impl Editor {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("Editor");
let mode = match self.mode {
- EditorMode::SingleLine { .. } => "single_line",
+ EditorMode::SingleLine => "single_line",
EditorMode::AutoHeight { .. } => "auto_height",
EditorMode::Minimap { .. } => "minimap",
EditorMode::Full { .. } => "full",
@@ -2490,9 +2558,7 @@ impl Editor {
.context_menu
.borrow()
.as_ref()
- .map_or(false, |context| {
- matches!(context, CodeContextMenu::Completions(_))
- });
+ .is_some_and(|context| matches!(context, CodeContextMenu::Completions(_)));
showing_completions
|| self.edit_prediction_requires_modifier()
@@ -2523,7 +2589,7 @@ impl Editor {
|| binding
.keystrokes()
.first()
- .map_or(false, |keystroke| keystroke.modifiers.modified())
+ .is_some_and(|keystroke| keystroke.modifiers().modified())
}))
}
@@ -2553,7 +2619,7 @@ impl Editor {
cx: &mut Context<Workspace>,
) -> Task<Result<Entity<Editor>>> {
let project = workspace.project().clone();
- let create = project.update(cx, |project, cx| project.create_buffer(cx));
+ let create = project.update(cx, |project, cx| project.create_buffer(true, cx));
cx.spawn_in(window, async move |workspace, cx| {
let buffer = create.await?;
@@ -2591,7 +2657,7 @@ impl Editor {
cx: &mut Context<Workspace>,
) {
let project = workspace.project().clone();
- let create = project.update(cx, |project, cx| project.create_buffer(cx));
+ let create = project.update(cx, |project, cx| project.create_buffer(true, cx));
cx.spawn_in(window, async move |workspace, cx| {
let buffer = create.await?;
@@ -2626,6 +2692,10 @@ impl Editor {
&self.buffer
}
+ pub fn project(&self) -> Option<&Entity<Project>> {
+ self.project.as_ref()
+ }
+
pub fn workspace(&self) -> Option<Entity<Workspace>> {
self.workspace.as_ref()?.0.upgrade()
}
@@ -2658,9 +2728,12 @@ impl Editor {
show_breakpoints: self.show_breakpoints,
git_blame_gutter_max_author_length,
display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
+ placeholder_display_snapshot: self
+ .placeholder_display_map
+ .as_ref()
+ .map(|display_map| display_map.update(cx, |map, cx| map.snapshot(cx))),
scroll_anchor: self.scroll_manager.anchor(),
ongoing_scroll: self.scroll_manager.ongoing_scroll(),
- placeholder_text: self.placeholder_text.clone(),
is_focused: self.focus_handle.is_focused(window),
current_line_highlight: self
.current_line_highlight
@@ -2756,20 +2829,37 @@ impl Editor {
self.refresh_edit_prediction(false, false, window, cx);
}
- pub fn placeholder_text(&self) -> Option<&str> {
- self.placeholder_text.as_deref()
+ pub fn placeholder_text(&self, cx: &mut App) -> Option<String> {
+ self.placeholder_display_map
+ .as_ref()
+ .map(|display_map| display_map.update(cx, |map, cx| map.snapshot(cx)).text())
}
pub fn set_placeholder_text(
&mut self,
- placeholder_text: impl Into<Arc<str>>,
+ placeholder_text: &str,
+ window: &mut Window,
cx: &mut Context<Self>,
) {
- let placeholder_text = Some(placeholder_text.into());
- if self.placeholder_text != placeholder_text {
- self.placeholder_text = placeholder_text;
- cx.notify();
- }
+ let multibuffer = cx
+ .new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local(placeholder_text, cx)), cx));
+
+ let style = window.text_style();
+
+ self.placeholder_display_map = Some(cx.new(|cx| {
+ DisplayMap::new(
+ multibuffer,
+ style.font(),
+ style.font_size.to_pixels(window.rem_size()),
+ None,
+ FILE_HEADER_HEIGHT,
+ MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
+ Default::default(),
+ DiagnosticSeverity::Off,
+ cx,
+ )
+ }));
+ cx.notify();
}
pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut Context<Self>) {
@@ -2915,7 +3005,7 @@ impl Editor {
return false;
};
- scope.override_name().map_or(false, |scope_name| {
+ scope.override_name().is_some_and(|scope_name| {
settings
.edit_predictions_disabled_in
.iter()
@@ -3005,20 +3095,19 @@ impl Editor {
}
if local {
- if let Some(buffer_id) = new_cursor_position.buffer_id {
- if !self.registered_buffers.contains_key(&buffer_id) {
- if let Some(project) = self.project.as_ref() {
- project.update(cx, |project, cx| {
- let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else {
- return;
- };
- self.registered_buffers.insert(
- buffer_id,
- project.register_buffer_with_language_servers(&buffer, cx),
- );
- })
- }
- }
+ if let Some(buffer_id) = new_cursor_position.buffer_id
+ && !self.registered_buffers.contains_key(&buffer_id)
+ && let Some(project) = self.project.as_ref()
+ {
+ project.update(cx, |project, cx| {
+ let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else {
+ return;
+ };
+ self.registered_buffers.insert(
+ buffer_id,
+ project.register_buffer_with_language_servers(&buffer, cx),
+ );
+ })
}
let mut context_menu = self.context_menu.borrow_mut();
@@ -3033,28 +3122,28 @@ impl Editor {
let completion_position = completion_menu.map(|menu| menu.initial_position);
drop(context_menu);
- if effects.completions {
- if let Some(completion_position) = completion_position {
- let start_offset = selection_start.to_offset(buffer);
- let position_matches = start_offset == completion_position.to_offset(buffer);
- let continue_showing = if position_matches {
- if self.snippet_stack.is_empty() {
- buffer.char_kind_before(start_offset, true) == Some(CharKind::Word)
- } else {
- // Snippet choices can be shown even when the cursor is in whitespace.
- // Dismissing the menu with actions like backspace is handled by
- // invalidation regions.
- true
- }
- } else {
- false
- };
-
- if continue_showing {
- self.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ if effects.completions
+ && let Some(completion_position) = completion_position
+ {
+ let start_offset = selection_start.to_offset(buffer);
+ let position_matches = start_offset == completion_position.to_offset(buffer);
+ let continue_showing = if position_matches {
+ if self.snippet_stack.is_empty() {
+ buffer.char_kind_before(start_offset, true) == Some(CharKind::Word)
} else {
- self.hide_context_menu(window, cx);
+ // Snippet choices can be shown even when the cursor is in whitespace.
+ // Dismissing the menu with actions like backspace is handled by
+ // invalidation regions.
+ true
}
+ } else {
+ false
+ };
+
+ if continue_showing {
+ self.show_completions(&ShowCompletions { trigger: None }, window, cx);
+ } else {
+ self.hide_context_menu(window, cx);
}
}
@@ -3085,30 +3174,27 @@ impl Editor {
if selections.len() == 1 {
cx.emit(SearchEvent::ActiveMatchChanged)
}
- if local {
- if let Some((_, _, buffer_snapshot)) = buffer.as_singleton() {
- let inmemory_selections = selections
- .iter()
- .map(|s| {
- text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot)
- ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot)
- })
- .collect();
- self.update_restoration_data(cx, |data| {
- data.selections = inmemory_selections;
- });
+ if local && let Some((_, _, buffer_snapshot)) = buffer.as_singleton() {
+ let inmemory_selections = selections
+ .iter()
+ .map(|s| {
+ text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot)
+ ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot)
+ })
+ .collect();
+ self.update_restoration_data(cx, |data| {
+ data.selections = inmemory_selections;
+ });
- if WorkspaceSettings::get(None, cx).restore_on_startup
- != RestoreOnStartupBehavior::None
- {
- if let Some(workspace_id) =
- self.workspace.as_ref().and_then(|workspace| workspace.1)
- {
- let snapshot = self.buffer().read(cx).snapshot(cx);
- let selections = selections.clone();
- let background_executor = cx.background_executor().clone();
- let editor_id = cx.entity().entity_id().as_u64() as ItemId;
- self.serialize_selections = cx.background_spawn(async move {
+ if WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None
+ && let Some(workspace_id) =
+ self.workspace.as_ref().and_then(|workspace| workspace.1)
+ {
+ let snapshot = self.buffer().read(cx).snapshot(cx);
+ let selections = selections.clone();
+ let background_executor = cx.background_executor().clone();
+ let editor_id = cx.entity().entity_id().as_u64() as ItemId;
+ self.serialize_selections = cx.background_spawn(async move {
background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
let db_selections = selections
.iter()
@@ -3125,8 +3211,6 @@ impl Editor {
.with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}"))
.log_err();
});
- }
- }
}
}
@@ -3203,35 +3287,31 @@ impl Editor {
selections.select_anchors(other_selections);
});
- let other_subscription =
- cx.subscribe(&other, |this, other, other_evt, cx| match other_evt {
- EditorEvent::SelectionsChanged { local: true } => {
- let other_selections = other.read(cx).selections.disjoint.to_vec();
- if other_selections.is_empty() {
- return;
- }
- this.selections.change_with(cx, |selections| {
- selections.select_anchors(other_selections);
- });
+ let other_subscription = cx.subscribe(&other, |this, other, other_evt, cx| {
+ if let EditorEvent::SelectionsChanged { local: true } = other_evt {
+ let other_selections = other.read(cx).selections.disjoint.to_vec();
+ if other_selections.is_empty() {
+ return;
}
- _ => {}
- });
+ this.selections.change_with(cx, |selections| {
+ selections.select_anchors(other_selections);
+ });
+ }
+ });
- let this_subscription =
- cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| match this_evt {
- EditorEvent::SelectionsChanged { local: true } => {
- let these_selections = this.selections.disjoint.to_vec();
- if these_selections.is_empty() {
- return;
- }
- other.update(cx, |other_editor, cx| {
- other_editor.selections.change_with(cx, |selections| {
- selections.select_anchors(these_selections);
- })
- });
+ let this_subscription = cx.subscribe_self::<EditorEvent>(move |this, this_evt, cx| {
+ if let EditorEvent::SelectionsChanged { local: true } = this_evt {
+ let these_selections = this.selections.disjoint.to_vec();
+ if these_selections.is_empty() {
+ return;
}
- _ => {}
- });
+ other.update(cx, |other_editor, cx| {
+ other_editor.selections.change_with(cx, |selections| {
+ selections.select_anchors(these_selections);
+ })
+ });
+ }
+ });
Subscription::join(other_subscription, this_subscription)
}
@@ -3312,9 +3392,9 @@ impl Editor {
let old_cursor_position = &state.old_cursor_position;
- self.selections_did_change(true, &old_cursor_position, state.effects, window, cx);
+ self.selections_did_change(true, old_cursor_position, state.effects, window, cx);
- if self.should_open_signature_help_automatically(&old_cursor_position, cx) {
+ if self.should_open_signature_help_automatically(old_cursor_position, cx) {
self.show_signature_help(&ShowSignatureHelp, window, cx);
}
}
@@ -3734,9 +3814,9 @@ impl Editor {
ColumnarSelectionState::FromMouse {
selection_tail,
display_point,
- } => display_point.unwrap_or_else(|| selection_tail.to_display_point(&display_map)),
+ } => display_point.unwrap_or_else(|| selection_tail.to_display_point(display_map)),
ColumnarSelectionState::FromSelection { selection_tail } => {
- selection_tail.to_display_point(&display_map)
+ selection_tail.to_display_point(display_map)
}
};
@@ -4013,18 +4093,18 @@ impl Editor {
let following_text_allows_autoclose = snapshot
.chars_at(selection.start)
.next()
- .map_or(true, |c| scope.should_autoclose_before(c));
+ .is_none_or(|c| scope.should_autoclose_before(c));
let preceding_text_allows_autoclose = selection.start.column == 0
- || snapshot.reversed_chars_at(selection.start).next().map_or(
- true,
- |c| {
+ || snapshot
+ .reversed_chars_at(selection.start)
+ .next()
+ .is_none_or(|c| {
bracket_pair.start != bracket_pair.end
|| !snapshot
.char_classifier_at(selection.start)
.is_word(c)
- },
- );
+ });
let is_closing_quote = if bracket_pair.end == bracket_pair.start
&& bracket_pair.start.len() == 1
@@ -4124,42 +4204,38 @@ impl Editor {
if self.auto_replace_emoji_shortcode
&& selection.is_empty()
&& text.as_ref().ends_with(':')
- {
- if let Some(possible_emoji_short_code) =
+ && let Some(possible_emoji_short_code) =
Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start)
- {
- if !possible_emoji_short_code.is_empty() {
- if let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) {
- let emoji_shortcode_start = Point::new(
- selection.start.row,
- selection.start.column - possible_emoji_short_code.len() as u32 - 1,
- );
+ && !possible_emoji_short_code.is_empty()
+ && let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code)
+ {
+ let emoji_shortcode_start = Point::new(
+ selection.start.row,
+ selection.start.column - possible_emoji_short_code.len() as u32 - 1,
+ );
- // Remove shortcode from buffer
- edits.push((
- emoji_shortcode_start..selection.start,
- "".to_string().into(),
- ));
- new_selections.push((
- Selection {
- id: selection.id,
- start: snapshot.anchor_after(emoji_shortcode_start),
- end: snapshot.anchor_before(selection.start),
- reversed: selection.reversed,
- goal: selection.goal,
- },
- 0,
- ));
+ // Remove shortcode from buffer
+ edits.push((
+ emoji_shortcode_start..selection.start,
+ "".to_string().into(),
+ ));
+ new_selections.push((
+ Selection {
+ id: selection.id,
+ start: snapshot.anchor_after(emoji_shortcode_start),
+ end: snapshot.anchor_before(selection.start),
+ reversed: selection.reversed,
+ goal: selection.goal,
+ },
+ 0,
+ ));
- // Insert emoji
- let selection_start_anchor = snapshot.anchor_after(selection.start);
- new_selections.push((selection.map(|_| selection_start_anchor), 0));
- edits.push((selection.start..selection.end, emoji.to_string().into()));
+ // Insert emoji
+ let selection_start_anchor = snapshot.anchor_after(selection.start);
+ new_selections.push((selection.map(|_| selection_start_anchor), 0));
+ edits.push((selection.start..selection.end, emoji.to_string().into()));
- continue;
- }
- }
- }
+ continue;
}
// If not handling any auto-close operation, then just replace the selected
@@ -6,7 +6,7 @@ use language::CursorShape;
use project::project_settings::DiagnosticSeverity;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources, VsCodeSettings};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi, VsCodeSettings};
use util::serde::default_true;
/// Imports from the VSCode settings at
@@ -17,6 +17,7 @@ pub struct EditorSettings {
pub cursor_shape: Option<CursorShape>,
pub current_line_highlight: CurrentLineHighlight,
pub selection_highlight: bool,
+ pub rounded_selection: bool,
pub lsp_highlight_debounce: u64,
pub hover_popover_enabled: bool,
pub hover_popover_delay: u64,
@@ -37,6 +38,7 @@ pub struct EditorSettings {
pub multi_cursor_modifier: MultiCursorModifier,
pub redact_private_values: bool,
pub expand_excerpt_lines: u32,
+ pub excerpt_context_lines: u32,
pub middle_click_paste: bool,
#[serde(default)]
pub double_click_in_multibuffer: DoubleClickInMultibuffer,
@@ -55,10 +57,13 @@ pub struct EditorSettings {
pub inline_code_actions: bool,
pub drag_and_drop_selection: DragAndDropSelection,
pub lsp_document_colors: DocumentColorsRenderMode,
+ pub minimum_contrast_for_highlights: f32,
}
/// How to render LSP `textDocument/documentColor` colors in the editor.
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+ Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub enum DocumentColorsRenderMode {
/// Do not query and render document colors.
@@ -72,7 +77,7 @@ pub enum DocumentColorsRenderMode {
Background,
}
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum CurrentLineHighlight {
// Don't highlight the current line.
@@ -86,7 +91,7 @@ pub enum CurrentLineHighlight {
}
/// When to populate a new search's query based on the text under the cursor.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum SeedQuerySetting {
/// Always populate the search query with the word under the cursor.
@@ -98,7 +103,9 @@ pub enum SeedQuerySetting {
}
/// What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers).
-#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+ Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub enum DoubleClickInMultibuffer {
/// Behave as a regular buffer and select the whole word.
@@ -117,7 +124,9 @@ pub struct Jupyter {
pub enabled: bool,
}
-#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+ Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub struct JupyterContent {
/// Whether the Jupyter feature is enabled.
@@ -132,6 +141,10 @@ pub struct StatusBar {
///
/// Default: true
pub active_language_button: bool,
+ /// Whether to show the cursor position button in the status bar.
+ ///
+ /// Default: true
+ pub cursor_position_button: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -285,7 +298,9 @@ pub struct ScrollbarAxes {
}
/// Whether to allow drag and drop text selection in buffer.
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(
+ Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
+)]
pub struct DragAndDropSelection {
/// When true, enables drag and drop text selection in buffer.
///
@@ -325,7 +340,7 @@ pub enum ScrollbarDiagnostics {
/// The key to use for adding multiple cursors
///
/// Default: alt
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum MultiCursorModifier {
Alt,
@@ -336,7 +351,7 @@ pub enum MultiCursorModifier {
/// Whether the editor will scroll beyond the last line.
///
/// Default: one_page
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum ScrollBeyondLastLine {
/// The editor will not scroll beyond the last line.
@@ -350,7 +365,9 @@ pub enum ScrollBeyondLastLine {
}
/// Default options for buffer and project search items.
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(
+ Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
+)]
pub struct SearchSettings {
/// Whether to show the project search button in the status bar.
#[serde(default = "default_true")]
@@ -366,7 +383,9 @@ pub struct SearchSettings {
}
/// What to do when go to definition yields no results.
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+ Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub enum GoToDefinitionFallback {
/// Disables the fallback.
@@ -379,7 +398,9 @@ pub enum GoToDefinitionFallback {
/// Determines when the mouse cursor should be hidden in an editor or input box.
///
/// Default: on_typing_and_movement
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+ Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub enum HideMouseMode {
/// Never hide the mouse cursor
@@ -394,7 +415,9 @@ pub enum HideMouseMode {
/// Determines how snippets are sorted relative to other completion items.
///
/// Default: inline
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+ Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub enum SnippetSortOrder {
/// Place snippets at the top of the completion list
@@ -408,7 +431,9 @@ pub enum SnippetSortOrder {
None,
}
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_ui(group = "Editor")]
+#[settings_key(None)]
pub struct EditorSettingsContent {
/// Whether the cursor blinks in the editor.
///
@@ -417,7 +442,7 @@ pub struct EditorSettingsContent {
/// Cursor shape for the default editor.
/// Can be "bar", "block", "underline", or "hollow".
///
- /// Default: None
+ /// Default: bar
pub cursor_shape: Option<CursorShape>,
/// Determines when the mouse cursor should be hidden in an editor or input box.
///
@@ -435,6 +460,10 @@ pub struct EditorSettingsContent {
///
/// Default: true
pub selection_highlight: Option<bool>,
+ /// Whether the text selection should have rounded corners.
+ ///
+ /// Default: true
+ pub rounded_selection: Option<bool>,
/// The debounce delay before querying highlights from the language
/// server based on the current cursor location.
///
@@ -511,6 +540,11 @@ pub struct EditorSettingsContent {
/// Default: 3
pub expand_excerpt_lines: Option<u32>,
+ /// How many lines of context to provide in multibuffer excerpts by default
+ ///
+ /// Default: 2
+ pub excerpt_context_lines: Option<u32>,
+
/// Whether to enable middle-click paste on Linux
///
/// Default: true
@@ -540,6 +574,12 @@ pub struct EditorSettingsContent {
///
/// Default: false
pub show_signature_help_after_edits: Option<bool>,
+ /// The minimum APCA perceptual contrast to maintain when
+ /// rendering text over highlight backgrounds in the editor.
+ ///
+ /// Values range from 0 to 106. Set to 0 to disable adjustments.
+ /// Default: 45
+ pub minimum_contrast_for_highlights: Option<f32>,
/// Whether to follow-up empty go to definition responses from the language server or not.
/// `FindAllReferences` allows to look up references of the same symbol instead.
@@ -579,16 +619,20 @@ pub struct EditorSettingsContent {
}
// Status bar related settings
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
pub struct StatusBarContent {
/// Whether to display the active language button in the status bar.
///
/// Default: true
pub active_language_button: Option<bool>,
+ /// Whether to show the cursor position button in the status bar.
+ ///
+ /// Default: true
+ pub cursor_position_button: Option<bool>,
}
// Toolbar related settings
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
pub struct ToolbarContent {
/// Whether to display breadcrumbs in the editor toolbar.
///
@@ -614,7 +658,9 @@ pub struct ToolbarContent {
}
/// Scrollbar related settings
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
+#[derive(
+ Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default, SettingsUi,
+)]
pub struct ScrollbarContent {
/// When to show the scrollbar in the editor.
///
@@ -649,7 +695,9 @@ pub struct ScrollbarContent {
}
/// Minimap related settings
-#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(
+ Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi,
+)]
pub struct MinimapContent {
/// When to show the minimap in the editor.
///
@@ -697,7 +745,10 @@ pub struct ScrollbarAxesContent {
}
/// Gutter related settings
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(
+ Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
+)]
+#[settings_ui(group = "Gutter")]
pub struct GutterContent {
/// Whether to show line numbers in the gutter.
///
@@ -728,8 +779,6 @@ impl EditorSettings {
}
impl Settings for EditorSettings {
- const KEY: Option<&'static str> = None;
-
type FileContent = EditorSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
@@ -773,6 +822,7 @@ impl Settings for EditorSettings {
"editor.selectionHighlight",
&mut current.selection_highlight,
);
+ vscode.bool_setting("editor.roundedSelection", &mut current.rounded_selection);
vscode.bool_setting("editor.hover.enabled", &mut current.hover_popover_enabled);
vscode.u64_setting("editor.hover.delay", &mut current.hover_popover_delay);
@@ -802,10 +852,8 @@ impl Settings for EditorSettings {
if gutter.line_numbers.is_some() {
old_gutter.line_numbers = gutter.line_numbers
}
- } else {
- if gutter != GutterContent::default() {
- current.gutter = Some(gutter)
- }
+ } else if gutter != GutterContent::default() {
+ current.gutter = Some(gutter)
}
if let Some(b) = vscode.read_bool("editor.scrollBeyondLastLine") {
current.scroll_beyond_last_line = Some(if b {
@@ -88,7 +88,7 @@ impl RenderOnce for BufferFontFamilyControl {
.child(Icon::new(IconName::Font))
.child(DropdownMenu::new(
"buffer-font-family",
- value.clone(),
+ value,
ContextMenu::build(window, cx, |mut menu, _, cx| {
let font_family_cache = FontFamilyCache::global(cx);
@@ -57,7 +57,9 @@ use util::{
use workspace::{
CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
OpenOptions, ViewId,
+ invalid_buffer_view::InvalidBufferView,
item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
+ register_project_item,
};
#[gpui::test]
@@ -74,7 +76,7 @@ fn test_edit_events(cx: &mut TestAppContext) {
let editor1 = cx.add_window({
let events = events.clone();
|window, cx| {
- let entity = cx.entity().clone();
+ let entity = cx.entity();
cx.subscribe_in(
&entity,
window,
@@ -95,7 +97,7 @@ fn test_edit_events(cx: &mut TestAppContext) {
let events = events.clone();
|window, cx| {
cx.subscribe_in(
- &cx.entity().clone(),
+ &cx.entity(),
window,
move |_, _, event: &EditorEvent, _, _| match event {
EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
@@ -708,7 +710,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
_ = workspace.update(cx, |_v, window, cx| {
cx.new(|cx| {
let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
- let mut editor = build_editor(buffer.clone(), window, cx);
+ let mut editor = build_editor(buffer, window, cx);
let handle = cx.entity();
editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
@@ -898,7 +900,7 @@ fn test_fold_action(cx: &mut TestAppContext) {
.unindent(),
cx,
);
- build_editor(buffer.clone(), window, cx)
+ build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
@@ -989,7 +991,7 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
.unindent(),
cx,
);
- build_editor(buffer.clone(), window, cx)
+ build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
@@ -1074,7 +1076,7 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
.unindent(),
cx,
);
- build_editor(buffer.clone(), window, cx)
+ build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
@@ -1173,7 +1175,7 @@ fn test_fold_at_level(cx: &mut TestAppContext) {
.unindent(),
cx,
);
- build_editor(buffer.clone(), window, cx)
+ build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
@@ -1335,7 +1337,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
let editor = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
- build_editor(buffer.clone(), window, cx)
+ build_editor(buffer, window, cx)
});
assert_eq!('🟥'.len_utf8(), 4);
@@ -1452,7 +1454,7 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
let editor = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
- build_editor(buffer.clone(), window, cx)
+ build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
@@ -2474,154 +2476,488 @@ async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
}
#[gpui::test]
-fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
+async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
init_test(cx, |_| {});
- let editor = cx.add_window(|window, cx| {
- let buffer = MultiBuffer::build_simple("one two three four", cx);
- build_editor(buffer.clone(), window, cx)
- });
+ let mut cx = EditorTestContext::new(cx).await;
- _ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
- s.select_display_ranges([
- // an empty selection - the preceding word fragment is deleted
- DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
- // characters selected - they are deleted
- DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
- ])
- });
+ // For an empty selection, the preceding word fragment is deleted.
+ // For non-empty selections, only selected characters are deleted.
+ cx.set_state("onˇe two t«hreˇ»e four");
+ cx.update_editor(|editor, window, cx| {
editor.delete_to_previous_word_start(
&DeleteToPreviousWordStart {
ignore_newlines: false,
+ ignore_brackets: false,
},
window,
cx,
);
- assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
});
+ cx.assert_editor_state("ˇe two tˇe four");
- _ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
- s.select_display_ranges([
- // an empty selection - the following word fragment is deleted
- DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
- // characters selected - they are deleted
- DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
- ])
- });
+ cx.set_state("e tˇwo te «fˇ»our");
+ cx.update_editor(|editor, window, cx| {
editor.delete_to_next_word_end(
&DeleteToNextWordEnd {
ignore_newlines: false,
+ ignore_brackets: false,
},
window,
cx,
);
- assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
});
+ cx.assert_editor_state("e tˇ te ˇour");
}
#[gpui::test]
-fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
+async fn test_delete_whitespaces(cx: &mut TestAppContext) {
init_test(cx, |_| {});
- let editor = cx.add_window(|window, cx| {
- let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
- build_editor(buffer.clone(), window, cx)
+ let mut cx = EditorTestContext::new(cx).await;
+
+ cx.set_state("here is some text ˇwith a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: false,
+ ignore_brackets: true,
+ },
+ window,
+ cx,
+ );
});
- let del_to_prev_word_start = DeleteToPreviousWordStart {
- ignore_newlines: false,
- };
- let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
- ignore_newlines: true,
- };
+ // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
+ cx.assert_editor_state("here is some textˇwith a space");
- _ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
- s.select_display_ranges([
- DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
- ])
- });
- editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
- assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
- editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
- assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
- editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
- assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
- editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
- assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
- editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
- assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
- editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
- assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
+ cx.set_state("here is some text ˇwith a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: false,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
});
-}
+ cx.assert_editor_state("here is some textˇwith a space");
-#[gpui::test]
-fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
- init_test(cx, |_| {});
+ cx.set_state("here is some textˇ with a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_next_word_end(
+ &DeleteToNextWordEnd {
+ ignore_newlines: false,
+ ignore_brackets: true,
+ },
+ window,
+ cx,
+ );
+ });
+ // Same happens in the other direction.
+ cx.assert_editor_state("here is some textˇwith a space");
- let editor = cx.add_window(|window, cx| {
- let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
- build_editor(buffer.clone(), window, cx)
+ cx.set_state("here is some textˇ with a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_next_word_end(
+ &DeleteToNextWordEnd {
+ ignore_newlines: false,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
});
- let del_to_next_word_end = DeleteToNextWordEnd {
- ignore_newlines: false,
- };
- let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
- ignore_newlines: true,
- };
+ cx.assert_editor_state("here is some textˇwith a space");
- _ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
- s.select_display_ranges([
- DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
- ])
- });
- editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
- assert_eq!(
- editor.buffer.read(cx).read(cx).text(),
- "one\n two\nthree\n four"
+ cx.set_state("here is some textˇ with a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_next_word_end(
+ &DeleteToNextWordEnd {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
);
- editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
- assert_eq!(
- editor.buffer.read(cx).read(cx).text(),
- "\n two\nthree\n four"
+ });
+ cx.assert_editor_state("here is some textˇwith a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
);
- editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
- assert_eq!(
- editor.buffer.read(cx).read(cx).text(),
- "two\nthree\n four"
+ });
+ cx.assert_editor_state("here is some ˇwith a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ // Single whitespaces are removed with the word behind them.
+ cx.assert_editor_state("here is ˇwith a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state("here ˇwith a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state("ˇwith a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state("ˇwith a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_next_word_end(
+ &DeleteToNextWordEnd {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ // Same happens in the other direction.
+ cx.assert_editor_state("ˇ a space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_next_word_end(
+ &DeleteToNextWordEnd {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state("ˇ space");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_next_word_end(
+ &DeleteToNextWordEnd {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state("ˇ");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_next_word_end(
+ &DeleteToNextWordEnd {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state("ˇ");
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
);
- editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
- assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
- editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
- assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
- editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
- assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
});
+ cx.assert_editor_state("ˇ");
}
#[gpui::test]
-fn test_newline(cx: &mut TestAppContext) {
+async fn test_delete_to_bracket(cx: &mut TestAppContext) {
init_test(cx, |_| {});
- let editor = cx.add_window(|window, cx| {
- let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
- build_editor(buffer.clone(), window, cx)
- });
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ brackets: BracketPairConfig {
+ pairs: vec![
+ BracketPair {
+ start: "\"".to_string(),
+ end: "\"".to_string(),
+ close: true,
+ surround: true,
+ newline: false,
+ },
+ BracketPair {
+ start: "(".to_string(),
+ end: ")".to_string(),
+ close: true,
+ surround: true,
+ newline: true,
+ },
+ ],
+ ..BracketPairConfig::default()
+ },
+ ..LanguageConfig::default()
+ },
+ Some(tree_sitter_rust::LANGUAGE.into()),
+ )
+ .with_brackets_query(
+ r#"
+ ("(" @open ")" @close)
+ ("\"" @open "\"" @close)
+ "#,
+ )
+ .unwrap(),
+ );
- _ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
- s.select_display_ranges([
- DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
- DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
- DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
- ])
- });
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
- editor.newline(&Newline, window, cx);
- assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
+ cx.set_state(r#"macro!("// ˇCOMMENT");"#);
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ // Deletion stops before brackets if asked to not ignore them.
+ cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ // Deletion has to remove a single bracket and then stop again.
+ cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
+
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
+
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state(r#"ˇCOMMENT");"#);
+
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state(r#"ˇCOMMENT");"#);
+
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_next_word_end(
+ &DeleteToNextWordEnd {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ // Brackets on the right are not paired anymore, hence deletion does not stop at them
+ cx.assert_editor_state(r#"ˇ");"#);
+
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_next_word_end(
+ &DeleteToNextWordEnd {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state(r#"ˇ"#);
+
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_next_word_end(
+ &DeleteToNextWordEnd {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state(r#"ˇ"#);
+
+ cx.set_state(r#"macro!("// ˇCOMMENT");"#);
+ cx.update_editor(|editor, window, cx| {
+ editor.delete_to_previous_word_start(
+ &DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: true,
+ },
+ window,
+ cx,
+ );
+ });
+ cx.assert_editor_state(r#"macroˇCOMMENT");"#);
+}
+
+#[gpui::test]
+fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx.add_window(|window, cx| {
+ let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
+ build_editor(buffer, window, cx)
+ });
+ let del_to_prev_word_start = DeleteToPreviousWordStart {
+ ignore_newlines: false,
+ ignore_brackets: false,
+ };
+ let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ };
+
+ _ = editor.update(cx, |editor, window, cx| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
+ ])
+ });
+ editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
+ assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
+ editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
+ assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
+ editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
+ assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
+ editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
+ assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
+ editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
+ assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
+ editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
+ assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
+ });
+}
+
+#[gpui::test]
+fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx.add_window(|window, cx| {
+ let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
+ build_editor(buffer, window, cx)
+ });
+ let del_to_next_word_end = DeleteToNextWordEnd {
+ ignore_newlines: false,
+ ignore_brackets: false,
+ };
+ let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
+ ignore_newlines: true,
+ ignore_brackets: false,
+ };
+
+ _ = editor.update(cx, |editor, window, cx| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
+ ])
+ });
+ editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
+ assert_eq!(
+ editor.buffer.read(cx).read(cx).text(),
+ "one\n two\nthree\n four"
+ );
+ editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
+ assert_eq!(
+ editor.buffer.read(cx).read(cx).text(),
+ "\n two\nthree\n four"
+ );
+ editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
+ assert_eq!(
+ editor.buffer.read(cx).read(cx).text(),
+ "two\nthree\n four"
+ );
+ editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
+ assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
+ editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
+ assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
+ editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
+ assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
+ editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
+ assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
+ });
+}
+
+#[gpui::test]
+fn test_newline(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx.add_window(|window, cx| {
+ let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
+ build_editor(buffer, window, cx)
+ });
+
+ _ = editor.update(cx, |editor, window, cx| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
+ DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
+ DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
+ ])
+ });
+
+ editor.newline(&Newline, window, cx);
+ assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
});
}
@@ -2644,7 +2980,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
.as_str(),
cx,
);
- let mut editor = build_editor(buffer.clone(), window, cx);
+ let mut editor = build_editor(buffer, window, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([
Point::new(2, 4)..Point::new(2, 5),
@@ -3175,7 +3511,7 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) {
let editor = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
- let mut editor = build_editor(buffer.clone(), window, cx);
+ let mut editor = build_editor(buffer, window, cx);
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([3..4, 11..12, 19..20])
});
@@ -4401,6 +4737,129 @@ async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
"});
}
+#[gpui::test]
+async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let js_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "JavaScript".into(),
+ wrap_characters: Some(language::WrapCharactersConfig {
+ start_prefix: "<".into(),
+ start_suffix: ">".into(),
+ end_prefix: "</".into(),
+ end_suffix: ">".into(),
+ }),
+ ..LanguageConfig::default()
+ },
+ None,
+ ));
+
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
+
+ cx.set_state(indoc! {"
+ «testˇ»
+ "});
+ cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
+ cx.assert_editor_state(indoc! {"
+ <«ˇ»>test</«ˇ»>
+ "});
+
+ cx.set_state(indoc! {"
+ «test
+ testˇ»
+ "});
+ cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
+ cx.assert_editor_state(indoc! {"
+ <«ˇ»>test
+ test</«ˇ»>
+ "});
+
+ cx.set_state(indoc! {"
+ teˇst
+ "});
+ cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
+ cx.assert_editor_state(indoc! {"
+ te<«ˇ»></«ˇ»>st
+ "});
+}
+
+#[gpui::test]
+async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let js_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "JavaScript".into(),
+ wrap_characters: Some(language::WrapCharactersConfig {
+ start_prefix: "<".into(),
+ start_suffix: ">".into(),
+ end_prefix: "</".into(),
+ end_suffix: ">".into(),
+ }),
+ ..LanguageConfig::default()
+ },
+ None,
+ ));
+
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
+
+ cx.set_state(indoc! {"
+ «testˇ»
+ «testˇ» «testˇ»
+ «testˇ»
+ "});
+ cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
+ cx.assert_editor_state(indoc! {"
+ <«ˇ»>test</«ˇ»>
+ <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
+ <«ˇ»>test</«ˇ»>
+ "});
+
+ cx.set_state(indoc! {"
+ «test
+ testˇ»
+ «test
+ testˇ»
+ "});
+ cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
+ cx.assert_editor_state(indoc! {"
+ <«ˇ»>test
+ test</«ˇ»>
+ <«ˇ»>test
+ test</«ˇ»>
+ "});
+}
+
+#[gpui::test]
+async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let plaintext_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "Plain Text".into(),
+ ..LanguageConfig::default()
+ },
+ None,
+ ));
+
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
+
+ cx.set_state(indoc! {"
+ «testˇ»
+ "});
+ cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
+ cx.assert_editor_state(indoc! {"
+ «testˇ»
+ "});
+}
+
#[gpui::test]
async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -4904,10 +5363,24 @@ async fn test_manipulate_text(cx: &mut TestAppContext) {
cx.assert_editor_state(indoc! {"
«HeLlO, wOrLD!ˇ»
"});
-}
-#[gpui::test]
-fn test_duplicate_line(cx: &mut TestAppContext) {
+ // Test selections with `line_mode = true`.
+ cx.update_editor(|editor, _window, _cx| editor.selections.line_mode = true);
+ cx.set_state(indoc! {"
+ «The quick brown
+ fox jumps over
+ tˇ»he lazy dog
+ "});
+ cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
+ cx.assert_editor_state(indoc! {"
+ «THE QUICK BROWN
+ FOX JUMPS OVER
+ THE LAZY DOGˇ»
+ "});
+}
+
+#[gpui::test]
+fn test_duplicate_line(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let editor = cx.add_window(|window, cx| {
@@ -5436,14 +5909,18 @@ async fn test_rewrap(cx: &mut TestAppContext) {
},
None,
));
- let rust_language = Arc::new(Language::new(
- LanguageConfig {
- name: "Rust".into(),
- line_comments: vec!["// ".into(), "/// ".into()],
- ..LanguageConfig::default()
- },
- Some(tree_sitter_rust::LANGUAGE.into()),
- ));
+ let rust_language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ line_comments: vec!["// ".into(), "/// ".into()],
+ ..LanguageConfig::default()
+ },
+ Some(tree_sitter_rust::LANGUAGE.into()),
+ )
+ .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
+ .unwrap(),
+ );
let plaintext_language = Arc::new(Language::new(
LanguageConfig {
@@ -5562,7 +6039,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
# ˇThis is a long comment using a pound
# sign.
"},
- python_language.clone(),
+ python_language,
&mut cx,
);
@@ -5669,7 +6146,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
also very long and should not merge
with the numbered item.ˇ»
"},
- markdown_language.clone(),
+ markdown_language,
&mut cx,
);
@@ -5700,7 +6177,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
// This is the second long comment block
// to be wrapped.ˇ»
"},
- rust_language.clone(),
+ rust_language,
&mut cx,
);
@@ -5723,7 +6200,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
«\tThis is a very long indented line
\tthat will be wrapped.ˇ»
"},
- plaintext_language.clone(),
+ plaintext_language,
&mut cx,
);
@@ -5759,6 +6236,411 @@ async fn test_rewrap(cx: &mut TestAppContext) {
}
}
+#[gpui::test]
+async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
+ init_test(cx, |settings| {
+ settings.languages.0.extend([(
+ "Rust".into(),
+ LanguageSettingsContent {
+ allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
+ preferred_line_length: Some(40),
+ ..Default::default()
+ },
+ )])
+ });
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let rust_lang = Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ line_comments: vec!["// ".into()],
+ block_comment: Some(BlockCommentConfig {
+ start: "/*".into(),
+ end: "*/".into(),
+ prefix: "* ".into(),
+ tab_size: 1,
+ }),
+ documentation_comment: Some(BlockCommentConfig {
+ start: "/**".into(),
+ end: "*/".into(),
+ prefix: "* ".into(),
+ tab_size: 1,
+ }),
+
+ ..LanguageConfig::default()
+ },
+ Some(tree_sitter_rust::LANGUAGE.into()),
+ )
+ .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
+ .unwrap(),
+ );
+
+ // regular block comment
+ assert_rewrap(
+ indoc! {"
+ /*
+ *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ */
+ /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
+ "},
+ indoc! {"
+ /*
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ /*
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // indent is respected
+ assert_rewrap(
+ indoc! {"
+ {}
+ /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
+ "},
+ indoc! {"
+ {}
+ /*
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // short block comments with inline delimiters
+ assert_rewrap(
+ indoc! {"
+ /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
+ /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ */
+ /*
+ *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
+ "},
+ indoc! {"
+ /*
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ /*
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ /*
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // multiline block comment with inline start/end delimiters
+ assert_rewrap(
+ indoc! {"
+ /*ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit. */
+ "},
+ indoc! {"
+ /*
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // block comment rewrap still respects paragraph bounds
+ assert_rewrap(
+ indoc! {"
+ /*
+ *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ *
+ * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ */
+ "},
+ indoc! {"
+ /*
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ *
+ * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ */
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // documentation comments
+ assert_rewrap(
+ indoc! {"
+ /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
+ /**
+ *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ */
+ "},
+ indoc! {"
+ /**
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ /**
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // different, adjacent comments
+ assert_rewrap(
+ indoc! {"
+ /**
+ *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ */
+ /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
+ //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ "},
+ indoc! {"
+ /**
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ /*
+ *ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ //ˇ Lorem ipsum dolor sit amet,
+ // consectetur adipiscing elit.
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // selection w/ single short block comment
+ assert_rewrap(
+ indoc! {"
+ «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
+ "},
+ indoc! {"
+ «/*
+ * Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */ˇ»
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // rewrapping a single comment w/ abutting comments
+ assert_rewrap(
+ indoc! {"
+ /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
+ /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
+ "},
+ indoc! {"
+ /*
+ * ˇLorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // selection w/ non-abutting short block comments
+ assert_rewrap(
+ indoc! {"
+ «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
+
+ /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
+ "},
+ indoc! {"
+ «/*
+ * Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+
+ /*
+ * Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */ˇ»
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // selection of multiline block comments
+ assert_rewrap(
+ indoc! {"
+ «/* Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit. */ˇ»
+ "},
+ indoc! {"
+ «/*
+ * Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */ˇ»
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // partial selection of multiline block comments
+ assert_rewrap(
+ indoc! {"
+ «/* Lorem ipsum dolor sit amet,ˇ»
+ * consectetur adipiscing elit. */
+ /* Lorem ipsum dolor sit amet,
+ «* consectetur adipiscing elit. */ˇ»
+ "},
+ indoc! {"
+ «/*
+ * Lorem ipsum dolor sit amet,ˇ»
+ * consectetur adipiscing elit. */
+ /* Lorem ipsum dolor sit amet,
+ «* consectetur adipiscing elit.
+ */ˇ»
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // selection w/ abutting short block comments
+ // TODO: should not be combined; should rewrap as 2 comments
+ assert_rewrap(
+ indoc! {"
+ «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
+ /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
+ "},
+ // desired behavior:
+ // indoc! {"
+ // «/*
+ // * Lorem ipsum dolor sit amet,
+ // * consectetur adipiscing elit.
+ // */
+ // /*
+ // * Lorem ipsum dolor sit amet,
+ // * consectetur adipiscing elit.
+ // */ˇ»
+ // "},
+ // actual behaviour:
+ indoc! {"
+ «/*
+ * Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit. Lorem
+ * ipsum dolor sit amet, consectetur
+ * adipiscing elit.
+ */ˇ»
+ "},
+ rust_lang.clone(),
+ &mut cx,
+ );
+
+ // TODO: same as above, but with delimiters on separate line
+ // assert_rewrap(
+ // indoc! {"
+ // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ // */
+ // /*
+ // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
+ // "},
+ // // desired:
+ // // indoc! {"
+ // // «/*
+ // // * Lorem ipsum dolor sit amet,
+ // // * consectetur adipiscing elit.
+ // // */
+ // // /*
+ // // * Lorem ipsum dolor sit amet,
+ // // * consectetur adipiscing elit.
+ // // */ˇ»
+ // // "},
+ // // actual: (but with trailing w/s on the empty lines)
+ // indoc! {"
+ // «/*
+ // * Lorem ipsum dolor sit amet,
+ // * consectetur adipiscing elit.
+ // *
+ // */
+ // /*
+ // *
+ // * Lorem ipsum dolor sit amet,
+ // * consectetur adipiscing elit.
+ // */ˇ»
+ // "},
+ // rust_lang.clone(),
+ // &mut cx,
+ // );
+
+ // TODO these are unhandled edge cases; not correct, just documenting known issues
+ assert_rewrap(
+ indoc! {"
+ /*
+ //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ */
+ /*
+ //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
+ /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
+ "},
+ // desired:
+ // indoc! {"
+ // /*
+ // *ˇ Lorem ipsum dolor sit amet,
+ // * consectetur adipiscing elit.
+ // */
+ // /*
+ // *ˇ Lorem ipsum dolor sit amet,
+ // * consectetur adipiscing elit.
+ // */
+ // /*
+ // *ˇ Lorem ipsum dolor sit amet
+ // */ /* consectetur adipiscing elit. */
+ // "},
+ // actual:
+ indoc! {"
+ /*
+ //ˇ Lorem ipsum dolor sit amet,
+ // consectetur adipiscing elit.
+ */
+ /*
+ * //ˇ Lorem ipsum dolor sit amet,
+ * consectetur adipiscing elit.
+ */
+ /*
+ *ˇ Lorem ipsum dolor sit amet */ /*
+ * consectetur adipiscing elit.
+ */
+ "},
+ rust_lang,
+ &mut cx,
+ );
+
+ #[track_caller]
+ fn assert_rewrap(
+ unwrapped_text: &str,
+ wrapped_text: &str,
+ language: Arc<Language>,
+ cx: &mut EditorTestContext,
+ ) {
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+ cx.set_state(unwrapped_text);
+ cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
+ cx.assert_editor_state(wrapped_text);
+ }
+}
+
#[gpui::test]
async fn test_hard_wrap(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -40,14 +40,15 @@ use git::{
};
use gpui::{
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
- Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges,
- Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
- HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length,
- ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent,
- MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent,
- ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun,
- TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop,
- linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black,
+ Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
+ DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
+ GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
+ KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
+ MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
+ ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
+ Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
+ linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
+ transparent_black,
};
use itertools::Itertools;
use language::language_settings::{
@@ -60,7 +61,7 @@ use multi_buffer::{
};
use project::{
- ProjectPath,
+ Entry, ProjectPath,
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
};
@@ -73,6 +74,7 @@ use std::{
fmt::{self, Write},
iter, mem,
ops::{Deref, Range},
+ path::{self, Path},
rc::Rc,
sync::Arc,
time::{Duration, Instant},
@@ -80,11 +82,18 @@ use std::{
use sum_tree::Bias;
use text::{BufferId, SelectionGoal};
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
-use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
+use ui::utils::ensure_minimum_contrast;
+use ui::{
+ ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*,
+ right_click_menu,
+};
use unicode_segmentation::UnicodeSegmentation;
use util::post_inc;
use util::{RangeExt, ResultExt, debug_panic};
-use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
+use workspace::{
+ CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace,
+ item::Item, notifications::NotifyTaskExt,
+};
/// Determines what kinds of highlights should be applied to a lines background.
#[derive(Clone, Copy, Default)]
@@ -108,6 +117,7 @@ struct SelectionLayout {
struct InlineBlameLayout {
element: AnyElement,
bounds: Bounds<Pixels>,
+ buffer_id: BufferId,
entry: BlameEntry,
}
@@ -355,6 +365,8 @@ impl EditorElement {
register_action(editor, window, Editor::toggle_comments);
register_action(editor, window, Editor::select_larger_syntax_node);
register_action(editor, window, Editor::select_smaller_syntax_node);
+ register_action(editor, window, Editor::select_next_syntax_node);
+ register_action(editor, window, Editor::select_prev_syntax_node);
register_action(editor, window, Editor::unwrap_syntax_node);
register_action(editor, window, Editor::select_enclosing_symbol);
register_action(editor, window, Editor::move_to_enclosing_bracket);
@@ -369,6 +381,8 @@ impl EditorElement {
register_action(editor, window, Editor::go_to_prev_diagnostic);
register_action(editor, window, Editor::go_to_next_hunk);
register_action(editor, window, Editor::go_to_prev_hunk);
+ register_action(editor, window, Editor::go_to_next_document_highlight);
+ register_action(editor, window, Editor::go_to_prev_document_highlight);
register_action(editor, window, |editor, action, window, cx| {
editor
.go_to_definition(action, window, cx)
@@ -577,6 +591,9 @@ impl EditorElement {
register_action(editor, window, Editor::edit_log_breakpoint);
register_action(editor, window, Editor::enable_breakpoint);
register_action(editor, window, Editor::disable_breakpoint);
+ if editor.read(cx).enable_wrap_selections_in_tag(cx) {
+ register_action(editor, window, Editor::wrap_selections_in_tag);
+ }
}
fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) {
@@ -717,7 +734,7 @@ impl EditorElement {
ColumnarMode::FromMouse => true,
ColumnarMode::FromSelection => false,
},
- mode: mode,
+ mode,
goal_column: point_for_position.exact_unclipped.column(),
},
window,
@@ -910,6 +927,11 @@ impl EditorElement {
} else if cfg!(any(target_os = "linux", target_os = "freebsd"))
&& event.button == MouseButton::Middle
{
+ #[allow(
+ clippy::collapsible_if,
+ clippy::needless_return,
+ reason = "The cfg-block below makes this a false positive"
+ )]
if !text_hitbox.is_hovered(window) || editor.read_only(cx) {
return;
}
@@ -1115,26 +1137,24 @@ impl EditorElement {
let hovered_diff_hunk_row = if let Some(control_row) = hovered_diff_control {
Some(control_row)
- } else {
- if text_hovered {
- let current_row = valid_point.row();
- position_map.display_hunks.iter().find_map(|(hunk, _)| {
- if let DisplayDiffHunk::Unfolded {
- display_row_range, ..
- } = hunk
- {
- if display_row_range.contains(¤t_row) {
- Some(display_row_range.start)
- } else {
- None
- }
+ } else if text_hovered {
+ let current_row = valid_point.row();
+ position_map.display_hunks.iter().find_map(|(hunk, _)| {
+ if let DisplayDiffHunk::Unfolded {
+ display_row_range, ..
+ } = hunk
+ {
+ if display_row_range.contains(¤t_row) {
+ Some(display_row_range.start)
} else {
None
}
- })
- } else {
- None
- }
+ } else {
+ None
+ }
+ })
+ } else {
+ None
};
if hovered_diff_hunk_row != editor.hovered_diff_hunk_row {
@@ -1142,20 +1162,20 @@ impl EditorElement {
cx.notify();
}
- if let Some((bounds, blame_entry)) = &position_map.inline_blame_bounds {
+ if let Some((bounds, buffer_id, blame_entry)) = &position_map.inline_blame_bounds {
let mouse_over_inline_blame = bounds.contains(&event.position);
let mouse_over_popover = editor
.inline_blame_popover
.as_ref()
.and_then(|state| state.popover_bounds)
- .map_or(false, |bounds| bounds.contains(&event.position));
+ .is_some_and(|bounds| bounds.contains(&event.position));
let keyboard_grace = editor
.inline_blame_popover
.as_ref()
- .map_or(false, |state| state.keyboard_grace);
+ .is_some_and(|state| state.keyboard_grace);
if mouse_over_inline_blame || mouse_over_popover {
- editor.show_blame_popover(&blame_entry, event.position, false, cx);
+ editor.show_blame_popover(*buffer_id, blame_entry, event.position, false, cx);
} else if !keyboard_grace {
editor.hide_blame_popover(cx);
}
@@ -1179,10 +1199,10 @@ impl EditorElement {
let is_visible = editor
.gutter_breakpoint_indicator
.0
- .map_or(false, |indicator| indicator.is_active);
+ .is_some_and(|indicator| indicator.is_active);
let has_existing_breakpoint =
- editor.breakpoint_store.as_ref().map_or(false, |store| {
+ editor.breakpoint_store.as_ref().is_some_and(|store| {
let Some(project) = &editor.project else {
return false;
};
@@ -1380,29 +1400,27 @@ impl EditorElement {
ref drop_cursor,
ref hide_drop_cursor,
} = editor.selection_drag_state
+ && !hide_drop_cursor
+ && (drop_cursor
+ .start
+ .cmp(&selection.start, &snapshot.buffer_snapshot)
+ .eq(&Ordering::Less)
+ || drop_cursor
+ .end
+ .cmp(&selection.end, &snapshot.buffer_snapshot)
+ .eq(&Ordering::Greater))
{
- if !hide_drop_cursor
- && (drop_cursor
- .start
- .cmp(&selection.start, &snapshot.buffer_snapshot)
- .eq(&Ordering::Less)
- || drop_cursor
- .end
- .cmp(&selection.end, &snapshot.buffer_snapshot)
- .eq(&Ordering::Greater))
- {
- let drag_cursor_layout = SelectionLayout::new(
- drop_cursor.clone(),
- false,
- CursorShape::Bar,
- &snapshot.display_snapshot,
- false,
- false,
- None,
- );
- let absent_color = cx.theme().players().absent();
- selections.push((absent_color, vec![drag_cursor_layout]));
- }
+ let drag_cursor_layout = SelectionLayout::new(
+ drop_cursor.clone(),
+ false,
+ CursorShape::Bar,
+ &snapshot.display_snapshot,
+ false,
+ false,
+ None,
+ );
+ let absent_color = cx.theme().players().absent();
+ selections.push((absent_color, vec![drag_cursor_layout]));
}
}
@@ -1413,19 +1431,15 @@ impl EditorElement {
CollaboratorId::PeerId(peer_id) => {
if let Some(collaborator) =
collaboration_hub.collaborators(cx).get(&peer_id)
- {
- if let Some(participant_index) = collaboration_hub
+ && let Some(participant_index) = collaboration_hub
.user_participant_indices(cx)
.get(&collaborator.user_id)
- {
- if let Some((local_selection_style, _)) = selections.first_mut()
- {
- *local_selection_style = cx
- .theme()
- .players()
- .color_for_participant(participant_index.0);
- }
- }
+ && let Some((local_selection_style, _)) = selections.first_mut()
+ {
+ *local_selection_style = cx
+ .theme()
+ .players()
+ .color_for_participant(participant_index.0);
}
}
CollaboratorId::Agent => {
@@ -2168,11 +2182,13 @@ impl EditorElement {
};
let padding = ProjectSettings::get_global(cx).diagnostics.inline.padding as f32 * em_width;
- let min_x = ProjectSettings::get_global(cx)
- .diagnostics
- .inline
- .min_column as f32
- * em_width;
+ let min_x = self.column_pixels(
+ ProjectSettings::get_global(cx)
+ .diagnostics
+ .inline
+ .min_column as usize,
+ window,
+ );
let mut elements = HashMap::default();
for (row, mut diagnostics) in diagnostics_by_rows {
@@ -2213,12 +2229,11 @@ impl EditorElement {
cmp::max(padded_line, min_start)
};
- let behind_edit_prediction_popover = edit_prediction_popover_origin.as_ref().map_or(
- false,
- |edit_prediction_popover_origin| {
+ let behind_edit_prediction_popover = edit_prediction_popover_origin
+ .as_ref()
+ .is_some_and(|edit_prediction_popover_origin| {
(pos_y..pos_y + line_height).contains(&edit_prediction_popover_origin.y)
- },
- );
+ });
let opacity = if behind_edit_prediction_popover {
0.5
} else {
@@ -2284,9 +2299,7 @@ impl EditorElement {
None
}
})
- .map_or(false, |source| {
- matches!(source, CodeActionSource::Indicator(..))
- });
+ .is_some_and(|source| matches!(source, CodeActionSource::Indicator(..)));
Some(editor.render_inline_code_actions(icon_size, display_point.row(), active, cx))
})?;
@@ -2434,20 +2447,19 @@ impl EditorElement {
.unwrap_or_default()
.padding as f32;
- if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() {
- match &edit_prediction.completion {
- EditPrediction::Edit {
- display_mode: EditDisplayMode::TabAccept,
- ..
- } => padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS,
- _ => {}
- }
+ if let Some(edit_prediction) = editor.active_edit_prediction.as_ref()
+ && let EditPrediction::Edit {
+ display_mode: EditDisplayMode::TabAccept,
+ ..
+ } = &edit_prediction.completion
+ {
+ padding += INLINE_ACCEPT_SUGGESTION_EM_WIDTHS
}
padding * em_width
};
- let entry = blame
+ let (buffer_id, entry) = blame
.update(cx, |blame, cx| {
blame.blame_for_rows(&[*row_info], cx).next()
})
@@ -2482,13 +2494,22 @@ impl EditorElement {
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let bounds = Bounds::new(absolute_offset, size);
- self.layout_blame_entry_popover(entry.clone(), blame, line_height, text_hitbox, window, cx);
+ self.layout_blame_entry_popover(
+ entry.clone(),
+ blame,
+ line_height,
+ text_hitbox,
+ row_info.buffer_id?,
+ window,
+ cx,
+ );
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
Some(InlineBlameLayout {
element,
bounds,
+ buffer_id,
entry,
})
}
@@ -2499,6 +2520,7 @@ impl EditorElement {
blame: Entity<GitBlame>,
line_height: Pixels,
text_hitbox: &Hitbox,
+ buffer: BufferId,
window: &mut Window,
cx: &mut App,
) {
@@ -2523,6 +2545,7 @@ impl EditorElement {
popover_state.markdown,
workspace,
&blame,
+ buffer,
window,
cx,
)
@@ -2597,14 +2620,16 @@ impl EditorElement {
.into_iter()
.enumerate()
.flat_map(|(ix, blame_entry)| {
+ let (buffer_id, blame_entry) = blame_entry?;
let mut element = render_blame_entry(
ix,
&blame,
- blame_entry?,
+ blame_entry,
&self.style,
&mut last_used_color,
self.editor.clone(),
workspace.clone(),
+ buffer_id,
blame_renderer.clone(),
cx,
)?;
@@ -2747,7 +2772,10 @@ impl EditorElement {
let mut block_offset = 0;
let mut found_excerpt_header = false;
for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
- if matches!(block, Block::ExcerptBoundary { .. }) {
+ if matches!(
+ block,
+ Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
+ ) {
found_excerpt_header = true;
break;
}
@@ -2764,7 +2792,10 @@ impl EditorElement {
let mut block_height = 0;
let mut found_excerpt_header = false;
for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
- if matches!(block, Block::ExcerptBoundary { .. }) {
+ if matches!(
+ block,
+ Block::ExcerptBoundary { .. } | Block::BufferHeader { .. }
+ ) {
found_excerpt_header = true;
}
block_height += block.height();
@@ -2811,7 +2842,7 @@ impl EditorElement {
}
let row =
- MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(&snapshot).row);
+ MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(snapshot).row);
if snapshot.is_line_folded(row) {
return None;
}
@@ -2902,7 +2933,7 @@ impl EditorElement {
if multibuffer_row
.0
.checked_sub(1)
- .map_or(false, |previous_row| {
+ .is_some_and(|previous_row| {
snapshot.is_line_folded(MultiBufferRow(previous_row))
})
{
@@ -2975,8 +3006,8 @@ impl EditorElement {
.ilog10()
+ 1;
- let elements = buffer_rows
- .into_iter()
+ buffer_rows
+ .iter()
.enumerate()
.map(|(ix, row_info)| {
let ExpandInfo {
@@ -3031,9 +3062,7 @@ impl EditorElement {
Some((toggle, origin))
})
- .collect();
-
- elements
+ .collect()
}
fn calculate_relative_line_numbers(
@@ -3133,7 +3162,7 @@ impl EditorElement {
let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to);
let mut line_number = String::new();
let line_numbers = buffer_rows
- .into_iter()
+ .iter()
.enumerate()
.flat_map(|(ix, row_info)| {
let display_row = DisplayRow(rows.start.0 + ix as u32);
@@ -3210,7 +3239,7 @@ impl EditorElement {
&& self.editor.read(cx).is_singleton(cx);
if include_fold_statuses {
row_infos
- .into_iter()
+ .iter()
.enumerate()
.map(|(ix, info)| {
if info.expand_info.is_some() {
@@ -3250,12 +3279,165 @@ impl EditorElement {
.collect()
}
+ fn bg_segments_per_row(
+ rows: Range<DisplayRow>,
+ selections: &[(PlayerColor, Vec<SelectionLayout>)],
+ highlight_ranges: &[(Range<DisplayPoint>, Hsla)],
+ base_background: Hsla,
+ ) -> Vec<Vec<(Range<DisplayPoint>, Hsla)>> {
+ if rows.start >= rows.end {
+ return Vec::new();
+ }
+ if !base_background.is_opaque() {
+ // We don't actually know what color is behind this editor.
+ return Vec::new();
+ }
+ let highlight_iter = highlight_ranges.iter().cloned();
+ let selection_iter = selections.iter().flat_map(|(player_color, layouts)| {
+ let color = player_color.selection;
+ layouts.iter().filter_map(move |selection_layout| {
+ if selection_layout.range.start != selection_layout.range.end {
+ Some((selection_layout.range.clone(), color))
+ } else {
+ None
+ }
+ })
+ });
+ let mut per_row_map = vec![Vec::new(); rows.len()];
+ for (range, color) in highlight_iter.chain(selection_iter) {
+ let covered_rows = if range.end.column() == 0 {
+ cmp::max(range.start.row(), rows.start)..cmp::min(range.end.row(), rows.end)
+ } else {
+ cmp::max(range.start.row(), rows.start)
+ ..cmp::min(range.end.row().next_row(), rows.end)
+ };
+ for row in covered_rows.iter_rows() {
+ let seg_start = if row == range.start.row() {
+ range.start
+ } else {
+ DisplayPoint::new(row, 0)
+ };
+ let seg_end = if row == range.end.row() && range.end.column() != 0 {
+ range.end
+ } else {
+ DisplayPoint::new(row, u32::MAX)
+ };
+ let ix = row.minus(rows.start) as usize;
+ debug_assert!(row >= rows.start && row < rows.end);
+ debug_assert!(ix < per_row_map.len());
+ per_row_map[ix].push((seg_start..seg_end, color));
+ }
+ }
+ for row_segments in per_row_map.iter_mut() {
+ if row_segments.is_empty() {
+ continue;
+ }
+ let segments = mem::take(row_segments);
+ let merged = Self::merge_overlapping_ranges(segments, base_background);
+ *row_segments = merged;
+ }
+ per_row_map
+ }
+
+ /// Merge overlapping ranges by splitting at all range boundaries and blending colors where
+ /// multiple ranges overlap. The result contains non-overlapping ranges ordered from left to right.
+ ///
+ /// Expects `start.row() == end.row()` for each range.
+ fn merge_overlapping_ranges(
+ ranges: Vec<(Range<DisplayPoint>, Hsla)>,
+ base_background: Hsla,
+ ) -> Vec<(Range<DisplayPoint>, Hsla)> {
+ struct Boundary {
+ pos: DisplayPoint,
+ is_start: bool,
+ index: usize,
+ color: Hsla,
+ }
+
+ let mut boundaries: SmallVec<[Boundary; 16]> = SmallVec::with_capacity(ranges.len() * 2);
+ for (index, (range, color)) in ranges.iter().enumerate() {
+ debug_assert!(
+ range.start.row() == range.end.row(),
+ "expects single-row ranges"
+ );
+ if range.start < range.end {
+ boundaries.push(Boundary {
+ pos: range.start,
+ is_start: true,
+ index,
+ color: *color,
+ });
+ boundaries.push(Boundary {
+ pos: range.end,
+ is_start: false,
+ index,
+ color: *color,
+ });
+ }
+ }
+
+ if boundaries.is_empty() {
+ return Vec::new();
+ }
+
+ boundaries
+ .sort_unstable_by(|a, b| a.pos.cmp(&b.pos).then_with(|| a.is_start.cmp(&b.is_start)));
+
+ let mut processed_ranges: Vec<(Range<DisplayPoint>, Hsla)> = Vec::new();
+ let mut active_ranges: SmallVec<[(usize, Hsla); 8]> = SmallVec::new();
+
+ let mut i = 0;
+ let mut start_pos = boundaries[0].pos;
+
+ let boundaries_len = boundaries.len();
+ while i < boundaries_len {
+ let current_boundary_pos = boundaries[i].pos;
+ if start_pos < current_boundary_pos {
+ if !active_ranges.is_empty() {
+ let mut color = base_background;
+ for &(_, c) in &active_ranges {
+ color = Hsla::blend(color, c);
+ }
+ if let Some((last_range, last_color)) = processed_ranges.last_mut() {
+ if *last_color == color && last_range.end == start_pos {
+ last_range.end = current_boundary_pos;
+ } else {
+ processed_ranges.push((start_pos..current_boundary_pos, color));
+ }
+ } else {
+ processed_ranges.push((start_pos..current_boundary_pos, color));
+ }
+ }
+ }
+ while i < boundaries_len && boundaries[i].pos == current_boundary_pos {
+ let active_range = &boundaries[i];
+ if active_range.is_start {
+ let idx = active_range.index;
+ let pos = active_ranges
+ .binary_search_by_key(&idx, |(i, _)| *i)
+ .unwrap_or_else(|p| p);
+ active_ranges.insert(pos, (idx, active_range.color));
+ } else {
+ let idx = active_range.index;
+ if let Ok(pos) = active_ranges.binary_search_by_key(&idx, |(i, _)| *i) {
+ active_ranges.remove(pos);
+ }
+ }
+ i += 1;
+ }
+ start_pos = current_boundary_pos;
+ }
+
+ processed_ranges
+ }
+
fn layout_lines(
rows: Range<DisplayRow>,
snapshot: &EditorSnapshot,
style: &EditorStyle,
editor_width: Pixels,
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
+ bg_segments_per_row: &[Vec<(Range<DisplayPoint>, Hsla)>],
window: &mut Window,
cx: &mut App,
) -> Vec<LineWithInvisibles> {
@@ -3271,12 +3453,15 @@ impl EditorElement {
let placeholder_lines = placeholder_text
.as_ref()
- .map_or("", AsRef::as_ref)
- .split('\n')
+ .map_or(Vec::new(), |text| text.split('\n').collect::<Vec<_>>());
+
+ let placeholder_line_count = placeholder_lines.len();
+
+ placeholder_lines
+ .into_iter()
.skip(rows.start.0 as usize)
.chain(iter::repeat(""))
- .take(rows.len());
- placeholder_lines
+ .take(cmp::max(rows.len(), placeholder_line_count))
.map(move |line| {
let run = TextRun {
len: line.len(),
@@ -3305,12 +3490,13 @@ impl EditorElement {
let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
LineWithInvisibles::from_chunks(
chunks,
- &style,
+ style,
MAX_LINE_LEN,
rows.len(),
&snapshot.mode,
editor_width,
is_row_soft_wrapped,
+ bg_segments_per_row,
window,
cx,
)
@@ -3386,7 +3572,7 @@ impl EditorElement {
let line_ix = align_to.row().0.checked_sub(rows.start.0);
x_position =
if let Some(layout) = line_ix.and_then(|ix| line_layouts.get(ix as usize)) {
- x_and_width(&layout)
+ x_and_width(layout)
} else {
x_and_width(&layout_line(
align_to.row(),
@@ -3452,42 +3638,41 @@ impl EditorElement {
.into_any_element()
}
- Block::ExcerptBoundary {
- excerpt,
- height,
- starts_new_buffer,
- ..
- } => {
+ Block::ExcerptBoundary { .. } => {
let color = cx.theme().colors().clone();
let mut result = v_flex().id(block_id).w_full();
+ result = result.child(
+ h_flex().relative().child(
+ div()
+ .top(line_height / 2.)
+ .absolute()
+ .w_full()
+ .h_px()
+ .bg(color.border_variant),
+ ),
+ );
+
+ result.into_any()
+ }
+
+ Block::BufferHeader { excerpt, height } => {
+ let mut result = v_flex().id(block_id).w_full();
+
let jump_data = header_jump_data(snapshot, block_row_start, *height, excerpt);
- if *starts_new_buffer {
- if sticky_header_excerpt_id != Some(excerpt.id) {
- let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
+ if sticky_header_excerpt_id != Some(excerpt.id) {
+ let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
- result = result.child(div().pr(editor_margins.right).child(
- self.render_buffer_header(
- excerpt, false, selected, false, jump_data, window, cx,
- ),
- ));
- } else {
- result =
- result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
- }
- } else {
- result = result.child(
- h_flex().relative().child(
- div()
- .top(line_height / 2.)
- .absolute()
- .w_full()
- .h_px()
- .bg(color.border_variant),
+ result = result.child(div().pr(editor_margins.right).child(
+ self.render_buffer_header(
+ excerpt, false, selected, false, jump_data, window, cx,
),
- );
- };
+ ));
+ } else {
+ result =
+ result.child(div().h(FILE_HEADER_HEIGHT as f32 * window.line_height()));
+ }
result.into_any()
}
@@ -3511,33 +3696,33 @@ impl EditorElement {
let mut x_offset = px(0.);
let mut is_block = true;
- if let BlockId::Custom(custom_block_id) = block_id {
- if block.has_height() {
- if block.place_near() {
- if let Some((x_target, line_width)) = x_position {
- let margin = em_width * 2;
- if line_width + final_size.width + margin
- < editor_width + editor_margins.gutter.full_width()
- && !row_block_types.contains_key(&(row - 1))
- && element_height_in_lines == 1
- {
- x_offset = line_width + margin;
- row = row - 1;
- is_block = false;
- element_height_in_lines = 0;
- row_block_types.insert(row, is_block);
- } else {
- let max_offset = editor_width + editor_margins.gutter.full_width()
- - final_size.width;
- let min_offset = (x_target + em_width - final_size.width)
- .max(editor_margins.gutter.full_width());
- x_offset = x_target.min(max_offset).max(min_offset);
- }
- }
- };
- if element_height_in_lines != block.height() {
- resized_blocks.insert(custom_block_id, element_height_in_lines);
+ if let BlockId::Custom(custom_block_id) = block_id
+ && block.has_height()
+ {
+ if block.place_near()
+ && let Some((x_target, line_width)) = x_position
+ {
+ let margin = em_width * 2;
+ if line_width + final_size.width + margin
+ < editor_width + editor_margins.gutter.full_width()
+ && !row_block_types.contains_key(&(row - 1))
+ && element_height_in_lines == 1
+ {
+ x_offset = line_width + margin;
+ row = row - 1;
+ is_block = false;
+ element_height_in_lines = 0;
+ row_block_types.insert(row, is_block);
+ } else {
+ let max_offset =
+ editor_width + editor_margins.gutter.full_width() - final_size.width;
+ let min_offset = (x_target + em_width - final_size.width)
+ .max(editor_margins.gutter.full_width());
+ x_offset = x_target.min(max_offset).max(min_offset);
}
+ };
+ if element_height_in_lines != block.height() {
+ resized_blocks.insert(custom_block_id, element_height_in_lines);
}
}
for i in 0..element_height_in_lines {
@@ -3556,11 +3741,10 @@ impl EditorElement {
jump_data: JumpData,
window: &mut Window,
cx: &mut App,
- ) -> Div {
+ ) -> impl IntoElement {
let editor = self.editor.read(cx);
- let file_status = editor
- .buffer
- .read(cx)
+ let multi_buffer = editor.buffer.read(cx);
+ let file_status = multi_buffer
.all_diff_hunks_expanded()
.then(|| {
editor
@@ -3570,6 +3754,17 @@ impl EditorElement {
.status_for_buffer_id(for_excerpt.buffer_id, cx)
})
.flatten();
+ let indicator = multi_buffer
+ .buffer(for_excerpt.buffer_id)
+ .and_then(|buffer| {
+ let buffer = buffer.read(cx);
+ let indicator_color = match (buffer.has_conflict(), buffer.is_dirty()) {
+ (true, _) => Some(Color::Warning),
+ (_, true) => Some(Color::Accent),
+ (false, false) => None,
+ };
+ indicator_color.map(|indicator_color| Indicator::dot().color(indicator_color))
+ });
let include_root = editor
.project
@@ -3577,17 +3772,17 @@ impl EditorElement {
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let can_open_excerpts = Editor::can_open_excerpts_in_file(for_excerpt.buffer.file());
- let path = for_excerpt.buffer.resolve_file_path(cx, include_root);
- let filename = path
+ let relative_path = for_excerpt.buffer.resolve_file_path(cx, include_root);
+ let filename = relative_path
.as_ref()
.and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
- let parent_path = path.as_ref().and_then(|path| {
+ let parent_path = relative_path.as_ref().and_then(|path| {
Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
});
let focus_handle = editor.focus_handle(cx);
let colors = cx.theme().colors();
- div()
+ let header = div()
.p_1()
.w_full()
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
@@ -3677,6 +3872,12 @@ impl EditorElement {
})
.take(1),
)
+ .child(
+ h_flex()
+ .size(Pixels(12.0))
+ .justify_center()
+ .children(indicator),
+ )
.child(
h_flex()
.cursor_pointer()
@@ -3687,29 +3888,38 @@ impl EditorElement {
.child(
h_flex()
.gap_2()
- .child(
- Label::new(
- filename
- .map(SharedString::from)
- .unwrap_or_else(|| "untitled".into()),
- )
- .single_line()
- .when_some(
- file_status,
- |el, status| {
- el.color(if status.is_conflicted() {
- Color::Conflict
- } else if status.is_modified() {
- Color::Modified
- } else if status.is_deleted() {
- Color::Disabled
- } else {
- Color::Created
- })
- .when(status.is_deleted(), |el| el.strikethrough())
- },
- ),
- )
+ .map(|path_header| {
+ let filename = filename
+ .map(SharedString::from)
+ .unwrap_or_else(|| "untitled".into());
+
+ path_header
+ .when(ItemSettings::get_global(cx).file_icons, |el| {
+ let path = path::Path::new(filename.as_str());
+ let icon = FileIcons::get_icon(path, cx)
+ .unwrap_or_default();
+ let icon =
+ Icon::from_path(icon).color(Color::Muted);
+ el.child(icon)
+ })
+ .child(Label::new(filename).single_line().when_some(
+ file_status,
+ |el, status| {
+ el.color(if status.is_conflicted() {
+ Color::Conflict
+ } else if status.is_modified() {
+ Color::Modified
+ } else if status.is_deleted() {
+ Color::Disabled
+ } else {
+ Color::Created
+ })
+ .when(status.is_deleted(), |el| {
+ el.strikethrough()
+ })
+ },
+ ))
+ })
.when_some(parent_path, |then, path| {
then.child(div().child(path).text_color(
if file_status.is_some_and(FileStatus::is_deleted) {
@@ -3720,23 +3930,26 @@ impl EditorElement {
))
}),
)
- .when(can_open_excerpts && is_selected && path.is_some(), |el| {
- el.child(
- h_flex()
- .id("jump-to-file-button")
- .gap_2p5()
- .child(Label::new("Jump To File"))
- .children(
- KeyBinding::for_action_in(
- &OpenExcerpts,
- &focus_handle,
- window,
- cx,
- )
- .map(|binding| binding.into_any_element()),
- ),
- )
- })
+ .when(
+ can_open_excerpts && is_selected && relative_path.is_some(),
+ |el| {
+ el.child(
+ h_flex()
+ .id("jump-to-file-button")
+ .gap_2p5()
+ .child(Label::new("Jump To File"))
+ .children(
+ KeyBinding::for_action_in(
+ &OpenExcerpts,
+ &focus_handle,
+ window,
+ cx,
+ )
+ .map(|binding| binding.into_any_element()),
+ ),
+ )
+ },
+ )
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.on_click(window.listener_for(&self.editor, {
move |editor, e: &ClickEvent, window, cx| {
@@ -10,16 +10,18 @@ use gpui::{
AnyElement, App, AppContext as _, Context, Entity, Hsla, ScrollHandle, Subscription, Task,
TextStyle, WeakEntity, Window,
};
-use language::{Bias, Buffer, BufferSnapshot, Edit};
+use itertools::Itertools;
+use language::{Bias, BufferSnapshot, Edit};
use markdown::Markdown;
-use multi_buffer::RowInfo;
+use multi_buffer::{MultiBuffer, RowInfo};
use project::{
- Project, ProjectItem,
+ Project, ProjectItem as _,
git_store::{GitStoreEvent, Repository, RepositoryEvent},
};
use smallvec::SmallVec;
use std::{sync::Arc, time::Duration};
use sum_tree::SumTree;
+use text::BufferId;
use workspace::Workspace;
#[derive(Clone, Debug, Default)]
@@ -63,16 +65,19 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
}
}
-pub struct GitBlame {
- project: Entity<Project>,
- buffer: Entity<Buffer>,
+struct GitBlameBuffer {
entries: SumTree<GitBlameEntry>,
- commit_details: HashMap<Oid, ParsedCommitMessage>,
buffer_snapshot: BufferSnapshot,
buffer_edits: text::Subscription,
+ commit_details: HashMap<Oid, ParsedCommitMessage>,
+}
+
+pub struct GitBlame {
+ project: Entity<Project>,
+ multi_buffer: WeakEntity<MultiBuffer>,
+ buffers: HashMap<BufferId, GitBlameBuffer>,
task: Task<Result<()>>,
focused: bool,
- generated: bool,
changed_while_blurred: bool,
user_triggered: bool,
regenerate_on_edit_task: Task<Result<()>>,
@@ -184,47 +189,46 @@ impl gpui::Global for GlobalBlameRenderer {}
impl GitBlame {
pub fn new(
- buffer: Entity<Buffer>,
+ multi_buffer: Entity<MultiBuffer>,
project: Entity<Project>,
user_triggered: bool,
focused: bool,
cx: &mut Context<Self>,
) -> Self {
- let entries = SumTree::from_item(
- GitBlameEntry {
- rows: buffer.read(cx).max_point().row + 1,
- blame: None,
+ let multi_buffer_subscription = cx.subscribe(
+ &multi_buffer,
+ |git_blame, multi_buffer, event, cx| match event {
+ multi_buffer::Event::DirtyChanged => {
+ if !multi_buffer.read(cx).is_dirty(cx) {
+ git_blame.generate(cx);
+ }
+ }
+ multi_buffer::Event::ExcerptsAdded { .. }
+ | multi_buffer::Event::ExcerptsEdited { .. } => git_blame.regenerate_on_edit(cx),
+ _ => {}
},
- &(),
);
- let buffer_subscriptions = cx.subscribe(&buffer, |this, buffer, event, cx| match event {
- language::BufferEvent::DirtyChanged => {
- if !buffer.read(cx).is_dirty() {
- this.generate(cx);
- }
- }
- language::BufferEvent::Edited => {
- this.regenerate_on_edit(cx);
- }
- _ => {}
- });
-
let project_subscription = cx.subscribe(&project, {
- let buffer = buffer.clone();
-
- move |this, _, event, cx| match event {
- project::Event::WorktreeUpdatedEntries(_, updated) => {
- let project_entry_id = buffer.read(cx).entry_id(cx);
+ let multi_buffer = multi_buffer.downgrade();
+
+ move |git_blame, _, event, cx| {
+ if let project::Event::WorktreeUpdatedEntries(_, updated) = event {
+ let Some(multi_buffer) = multi_buffer.upgrade() else {
+ return;
+ };
+ let project_entry_id = multi_buffer
+ .read(cx)
+ .as_singleton()
+ .and_then(|it| it.read(cx).entry_id(cx));
if updated
.iter()
.any(|(_, entry_id, _)| project_entry_id == Some(*entry_id))
{
log::debug!("Updated buffers. Regenerating blame data...",);
- this.generate(cx);
+ git_blame.generate(cx);
}
}
- _ => {}
}
});
@@ -240,24 +244,17 @@ impl GitBlame {
_ => {}
});
- let buffer_snapshot = buffer.read(cx).snapshot();
- let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
-
let mut this = Self {
project,
- buffer,
- buffer_snapshot,
- entries,
- buffer_edits,
+ multi_buffer: multi_buffer.downgrade(),
+ buffers: HashMap::default(),
user_triggered,
focused,
changed_while_blurred: false,
- commit_details: HashMap::default(),
task: Task::ready(Ok(())),
- generated: false,
regenerate_on_edit_task: Task::ready(Ok(())),
_regenerate_subscriptions: vec![
- buffer_subscriptions,
+ multi_buffer_subscription,
project_subscription,
git_store_subscription,
],
@@ -266,54 +263,61 @@ impl GitBlame {
this
}
- pub fn repository(&self, cx: &App) -> Option<Entity<Repository>> {
+ pub fn repository(&self, cx: &App, id: BufferId) -> Option<Entity<Repository>> {
self.project
.read(cx)
.git_store()
.read(cx)
- .repository_and_path_for_buffer_id(self.buffer.read(cx).remote_id(), cx)
+ .repository_and_path_for_buffer_id(id, cx)
.map(|(repo, _)| repo)
}
pub fn has_generated_entries(&self) -> bool {
- self.generated
+ !self.buffers.is_empty()
}
- pub fn details_for_entry(&self, entry: &BlameEntry) -> Option<ParsedCommitMessage> {
- self.commit_details.get(&entry.sha).cloned()
+ pub fn details_for_entry(
+ &self,
+ buffer: BufferId,
+ entry: &BlameEntry,
+ ) -> Option<ParsedCommitMessage> {
+ self.buffers
+ .get(&buffer)?
+ .commit_details
+ .get(&entry.sha)
+ .cloned()
}
pub fn blame_for_rows<'a>(
&'a mut self,
rows: &'a [RowInfo],
- cx: &App,
- ) -> impl 'a + Iterator<Item = Option<BlameEntry>> {
- self.sync(cx);
-
- let buffer_id = self.buffer_snapshot.remote_id();
- let mut cursor = self.entries.cursor::<u32>(&());
- rows.into_iter().map(move |info| {
- let row = info
- .buffer_row
- .filter(|_| info.buffer_id == Some(buffer_id))?;
- cursor.seek_forward(&row, Bias::Right);
- cursor.item()?.blame.clone()
+ cx: &'a mut App,
+ ) -> impl Iterator<Item = Option<(BufferId, BlameEntry)>> + use<'a> {
+ rows.iter().map(move |info| {
+ let buffer_id = info.buffer_id?;
+ self.sync(cx, buffer_id);
+
+ let buffer_row = info.buffer_row?;
+ let mut cursor = self.buffers.get(&buffer_id)?.entries.cursor::<u32>(&());
+ cursor.seek_forward(&buffer_row, Bias::Right);
+ Some((buffer_id, cursor.item()?.blame.clone()?))
})
}
- pub fn max_author_length(&mut self, cx: &App) -> usize {
- self.sync(cx);
-
+ pub fn max_author_length(&mut self, cx: &mut App) -> usize {
let mut max_author_length = 0;
-
- for entry in self.entries.iter() {
- let author_len = entry
- .blame
- .as_ref()
- .and_then(|entry| entry.author.as_ref())
- .map(|author| author.len());
- if let Some(author_len) = author_len {
- if author_len > max_author_length {
+ self.sync_all(cx);
+
+ for buffer in self.buffers.values() {
+ for entry in buffer.entries.iter() {
+ let author_len = entry
+ .blame
+ .as_ref()
+ .and_then(|entry| entry.author.as_ref())
+ .map(|author| author.len());
+ if let Some(author_len) = author_len
+ && author_len > max_author_length
+ {
max_author_length = author_len;
}
}
@@ -337,22 +341,48 @@ impl GitBlame {
}
}
- fn sync(&mut self, cx: &App) {
- let edits = self.buffer_edits.consume();
- let new_snapshot = self.buffer.read(cx).snapshot();
+ fn sync_all(&mut self, cx: &mut App) {
+ let Some(multi_buffer) = self.multi_buffer.upgrade() else {
+ return;
+ };
+ multi_buffer
+ .read(cx)
+ .excerpt_buffer_ids()
+ .into_iter()
+ .for_each(|id| self.sync(cx, id));
+ }
+
+ fn sync(&mut self, cx: &mut App, buffer_id: BufferId) {
+ let Some(blame_buffer) = self.buffers.get_mut(&buffer_id) else {
+ return;
+ };
+ let Some(buffer) = self
+ .multi_buffer
+ .upgrade()
+ .and_then(|multi_buffer| multi_buffer.read(cx).buffer(buffer_id))
+ else {
+ return;
+ };
+ let edits = blame_buffer.buffer_edits.consume();
+ let new_snapshot = buffer.read(cx).snapshot();
let mut row_edits = edits
.into_iter()
.map(|edit| {
- let old_point_range = self.buffer_snapshot.offset_to_point(edit.old.start)
- ..self.buffer_snapshot.offset_to_point(edit.old.end);
+ let old_point_range = blame_buffer.buffer_snapshot.offset_to_point(edit.old.start)
+ ..blame_buffer.buffer_snapshot.offset_to_point(edit.old.end);
let new_point_range = new_snapshot.offset_to_point(edit.new.start)
..new_snapshot.offset_to_point(edit.new.end);
if old_point_range.start.column
- == self.buffer_snapshot.line_len(old_point_range.start.row)
+ == blame_buffer
+ .buffer_snapshot
+ .line_len(old_point_range.start.row)
&& (new_snapshot.chars_at(edit.new.start).next() == Some('\n')
- || self.buffer_snapshot.line_len(old_point_range.end.row) == 0)
+ || blame_buffer
+ .buffer_snapshot
+ .line_len(old_point_range.end.row)
+ == 0)
{
Edit {
old: old_point_range.start.row + 1..old_point_range.end.row + 1,
@@ -376,7 +406,7 @@ impl GitBlame {
.peekable();
let mut new_entries = SumTree::default();
- let mut cursor = self.entries.cursor::<u32>(&());
+ let mut cursor = blame_buffer.entries.cursor::<u32>(&());
while let Some(mut edit) = row_edits.next() {
while let Some(next_edit) = row_edits.peek() {
@@ -415,37 +445,47 @@ impl GitBlame {
let old_end = cursor.end();
if row_edits
.peek()
- .map_or(true, |next_edit| next_edit.old.start >= old_end)
+ .is_none_or(|next_edit| next_edit.old.start >= old_end)
+ && let Some(entry) = cursor.item()
{
- if let Some(entry) = cursor.item() {
- if old_end > edit.old.end {
- new_entries.push(
- GitBlameEntry {
- rows: cursor.end() - edit.old.end,
- blame: entry.blame.clone(),
- },
- &(),
- );
- }
-
- cursor.next();
+ if old_end > edit.old.end {
+ new_entries.push(
+ GitBlameEntry {
+ rows: cursor.end() - edit.old.end,
+ blame: entry.blame.clone(),
+ },
+ &(),
+ );
}
+
+ cursor.next();
}
}
new_entries.append(cursor.suffix(), &());
drop(cursor);
- self.buffer_snapshot = new_snapshot;
- self.entries = new_entries;
+ blame_buffer.buffer_snapshot = new_snapshot;
+ blame_buffer.entries = new_entries;
}
#[cfg(test)]
fn check_invariants(&mut self, cx: &mut Context<Self>) {
- self.sync(cx);
- assert_eq!(
- self.entries.summary().rows,
- self.buffer.read(cx).max_point().row + 1
- );
+ self.sync_all(cx);
+ for (&id, buffer) in &self.buffers {
+ assert_eq!(
+ buffer.entries.summary().rows,
+ self.multi_buffer
+ .upgrade()
+ .unwrap()
+ .read(cx)
+ .buffer(id)
+ .unwrap()
+ .read(cx)
+ .max_point()
+ .row
+ + 1
+ );
+ }
}
fn generate(&mut self, cx: &mut Context<Self>) {
@@ -453,62 +493,105 @@ impl GitBlame {
self.changed_while_blurred = true;
return;
}
- let buffer_edits = self.buffer.update(cx, |buffer, _| buffer.subscribe());
- let snapshot = self.buffer.read(cx).snapshot();
let blame = self.project.update(cx, |project, cx| {
- project.blame_buffer(&self.buffer, None, cx)
+ let Some(multi_buffer) = self.multi_buffer.upgrade() else {
+ return Vec::new();
+ };
+ multi_buffer
+ .read(cx)
+ .all_buffer_ids()
+ .into_iter()
+ .filter_map(|id| {
+ let buffer = multi_buffer.read(cx).buffer(id)?;
+ let snapshot = buffer.read(cx).snapshot();
+ let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
+
+ let blame_buffer = project.blame_buffer(&buffer, None, cx);
+ Some((id, snapshot, buffer_edits, blame_buffer))
+ })
+ .collect::<Vec<_>>()
});
let provider_registry = GitHostingProviderRegistry::default_global(cx);
self.task = cx.spawn(async move |this, cx| {
- let result = cx
+ let (result, errors) = cx
.background_spawn({
- let snapshot = snapshot.clone();
async move {
- let Some(Blame {
- entries,
- messages,
- remote_url,
- }) = blame.await?
- else {
- return Ok(None);
- };
-
- let entries = build_blame_entry_sum_tree(entries, snapshot.max_point().row);
- let commit_details =
- parse_commit_messages(messages, remote_url, provider_registry).await;
-
- anyhow::Ok(Some((entries, commit_details)))
+ let mut res = vec![];
+ let mut errors = vec![];
+ for (id, snapshot, buffer_edits, blame) in blame {
+ match blame.await {
+ Ok(Some(Blame {
+ entries,
+ messages,
+ remote_url,
+ })) => {
+ let entries = build_blame_entry_sum_tree(
+ entries,
+ snapshot.max_point().row,
+ );
+ let commit_details = parse_commit_messages(
+ messages,
+ remote_url,
+ provider_registry.clone(),
+ )
+ .await;
+
+ res.push((
+ id,
+ snapshot,
+ buffer_edits,
+ Some(entries),
+ commit_details,
+ ));
+ }
+ Ok(None) => {
+ res.push((id, snapshot, buffer_edits, None, Default::default()))
+ }
+ Err(e) => errors.push(e),
+ }
+ }
+ (res, errors)
}
})
.await;
- this.update(cx, |this, cx| match result {
- Ok(None) => {
- // Nothing to do, e.g. no repository found
+ this.update(cx, |this, cx| {
+ this.buffers.clear();
+ for (id, snapshot, buffer_edits, entries, commit_details) in result {
+ let Some(entries) = entries else {
+ continue;
+ };
+ this.buffers.insert(
+ id,
+ GitBlameBuffer {
+ buffer_edits,
+ buffer_snapshot: snapshot,
+ entries,
+ commit_details,
+ },
+ );
}
- Ok(Some((entries, commit_details))) => {
- this.buffer_edits = buffer_edits;
- this.buffer_snapshot = snapshot;
- this.entries = entries;
- this.commit_details = commit_details;
- this.generated = true;
- cx.notify();
+ cx.notify();
+ if !errors.is_empty() {
+ this.project.update(cx, |_, cx| {
+ if this.user_triggered {
+ log::error!("failed to get git blame data: {errors:?}");
+ let notification = errors
+ .into_iter()
+ .format_with(",", |e, f| f(&format_args!("{:#}", e)))
+ .to_string();
+ cx.emit(project::Event::Toast {
+ notification_id: "git-blame".into(),
+ message: notification,
+ });
+ } else {
+ // If we weren't triggered by a user, we just log errors in the background, instead of sending
+ // notifications.
+ log::debug!("failed to get git blame data: {errors:?}");
+ }
+ })
}
- Err(error) => this.project.update(cx, |_, cx| {
- if this.user_triggered {
- log::error!("failed to get git blame data: {error:?}");
- let notification = format!("{:#}", error).trim().to_string();
- cx.emit(project::Event::Toast {
- notification_id: "git-blame".into(),
- message: notification,
- });
- } else {
- // If we weren't triggered by a user, we just log errors in the background, instead of sending
- // notifications.
- log::debug!("failed to get git blame data: {error:?}");
- }
- }),
})
});
}
@@ -522,7 +605,7 @@ impl GitBlame {
this.update(cx, |this, cx| {
this.generate(cx);
})
- })
+ });
}
}
@@ -661,6 +744,9 @@ mod tests {
)
.collect::<Vec<_>>(),
expected
+ .into_iter()
+ .map(|it| Some((buffer_id, it?)))
+ .collect::<Vec<_>>()
);
}
@@ -707,6 +793,7 @@ mod tests {
})
.await
.unwrap();
+ let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let blame = cx.new(|cx| GitBlame::new(buffer.clone(), project.clone(), true, true, cx));
@@ -787,6 +874,7 @@ mod tests {
.await
.unwrap();
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
+ let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx));
@@ -808,14 +896,14 @@ mod tests {
)
.collect::<Vec<_>>(),
vec![
- Some(blame_entry("1b1b1b", 0..1)),
- Some(blame_entry("0d0d0d", 1..2)),
- Some(blame_entry("3a3a3a", 2..3)),
+ Some((buffer_id, blame_entry("1b1b1b", 0..1))),
+ Some((buffer_id, blame_entry("0d0d0d", 1..2))),
+ Some((buffer_id, blame_entry("3a3a3a", 2..3))),
None,
None,
- Some(blame_entry("3a3a3a", 5..6)),
- Some(blame_entry("0d0d0d", 6..7)),
- Some(blame_entry("3a3a3a", 7..8)),
+ Some((buffer_id, blame_entry("3a3a3a", 5..6))),
+ Some((buffer_id, blame_entry("0d0d0d", 6..7))),
+ Some((buffer_id, blame_entry("3a3a3a", 7..8))),
]
);
// Subset of lines
@@ -833,8 +921,8 @@ mod tests {
)
.collect::<Vec<_>>(),
vec![
- Some(blame_entry("0d0d0d", 1..2)),
- Some(blame_entry("3a3a3a", 2..3)),
+ Some((buffer_id, blame_entry("0d0d0d", 1..2))),
+ Some((buffer_id, blame_entry("3a3a3a", 2..3))),
None
]
);
@@ -854,7 +942,7 @@ mod tests {
cx
)
.collect::<Vec<_>>(),
- vec![Some(blame_entry("0d0d0d", 1..2)), None, None]
+ vec![Some((buffer_id, blame_entry("0d0d0d", 1..2))), None, None]
);
});
}
@@ -897,6 +985,7 @@ mod tests {
.await
.unwrap();
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
+ let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx));
@@ -1018,7 +1107,7 @@ mod tests {
init_test(cx);
let fs = FakeFs::new(cx.executor());
- let buffer_initial_text_len = rng.gen_range(5..15);
+ let buffer_initial_text_len = rng.random_range(5..15);
let mut buffer_initial_text = Rope::from(
RandomCharIter::new(&mut rng)
.take(buffer_initial_text_len)
@@ -1063,13 +1152,14 @@ mod tests {
})
.await
.unwrap();
+ let mbuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
- let git_blame = cx.new(|cx| GitBlame::new(buffer.clone(), project, false, true, cx));
+ let git_blame = cx.new(|cx| GitBlame::new(mbuffer.clone(), project, false, true, cx));
cx.executor().run_until_parked();
git_blame.update(cx, |blame, cx| blame.check_invariants(cx));
for _ in 0..operations {
- match rng.gen_range(0..100) {
+ match rng.random_range(0..100) {
0..=19 => {
log::info!("quiescing");
cx.executor().run_until_parked();
@@ -1112,8 +1202,8 @@ mod tests {
let mut blame_entries = Vec::new();
for ix in 0..5 {
if last_row < max_row {
- let row_start = rng.gen_range(last_row..max_row);
- let row_end = rng.gen_range(row_start + 1..cmp::min(row_start + 3, max_row) + 1);
+ let row_start = rng.random_range(last_row..max_row);
+ let row_end = rng.random_range(row_start + 1..cmp::min(row_start + 3, max_row) + 1);
blame_entries.push(blame_entry(&ix.to_string(), row_start..row_end));
last_row = row_end;
} else {
@@ -1,6 +1,7 @@
use crate::{Editor, RangeToAnchorExt};
-use gpui::{Context, Window};
+use gpui::{Context, HighlightStyle, Window};
use language::CursorShape;
+use theme::ActiveTheme;
enum MatchingBracketHighlight {}
@@ -9,7 +10,7 @@ pub fn refresh_matching_bracket_highlights(
window: &mut Window,
cx: &mut Context<Editor>,
) {
- editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
+ editor.clear_highlights::<MatchingBracketHighlight>(cx);
let newest_selection = editor.selections.newest::<usize>(cx);
// Don't highlight brackets if the selection isn't empty
@@ -35,12 +36,19 @@ pub fn refresh_matching_bracket_highlights(
.buffer_snapshot
.innermost_enclosing_bracket_ranges(head..tail, None)
{
- editor.highlight_background::<MatchingBracketHighlight>(
- &[
+ editor.highlight_text::<MatchingBracketHighlight>(
+ vec![
opening_range.to_anchors(&snapshot.buffer_snapshot),
closing_range.to_anchors(&snapshot.buffer_snapshot),
],
- |theme| theme.colors().editor_document_highlight_bracket_background,
+ HighlightStyle {
+ background_color: Some(
+ cx.theme()
+ .colors()
+ .editor_document_highlight_bracket_background,
+ ),
+ ..Default::default()
+ },
cx,
)
}
@@ -104,7 +112,7 @@ mod tests {
another_test(1, 2, 3);
}
"#});
- cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test«(»"Test argument"«)» {
another_test(1, 2, 3);
}
@@ -115,7 +123,7 @@ mod tests {
another_test(1, ˇ2, 3);
}
"#});
- cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") {
another_test«(»1, 2, 3«)»;
}
@@ -126,7 +134,7 @@ mod tests {
anotherˇ_test(1, 2, 3);
}
"#});
- cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") «{»
another_test(1, 2, 3);
«}»
@@ -138,7 +146,7 @@ mod tests {
another_test(1, 2, 3);
}
"#});
- cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") {
another_test(1, 2, 3);
}
@@ -150,8 +158,8 @@ mod tests {
another_test(1, 2, 3);
}
"#});
- cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
- pub fn test("Test argument") {
+ cx.assert_editor_text_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ pub fn test«("Test argument") {
another_test(1, 2, 3);
}
"#});
@@ -188,22 +188,26 @@ impl Editor {
pub fn scroll_hover(
&mut self,
- amount: &ScrollAmount,
+ amount: ScrollAmount,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
let selection = self.selections.newest_anchor().head();
let snapshot = self.snapshot(window, cx);
- let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
+ if let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
popover
.symbol_range
.point_within_range(&TriggerPoint::Text(selection), &snapshot)
- }) else {
- return false;
- };
- popover.scroll(amount, window, cx);
- true
+ }) {
+ popover.scroll(amount, window, cx);
+ true
+ } else if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
+ context_menu.scroll_aside(amount, window, cx);
+ true
+ } else {
+ false
+ }
}
fn cmd_click_reveal_task(
@@ -271,7 +275,7 @@ impl Editor {
Task::ready(Ok(Navigated::No))
};
self.select(SelectPhase::End, window, cx);
- return navigate_task;
+ navigate_task
}
}
@@ -321,7 +325,10 @@ pub fn update_inlay_link_and_hover_points(
if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
match cached_hint.resolve_state {
ResolveState::CanResolve(_, _) => {
- if let Some(buffer_id) = previous_valid_anchor.buffer_id {
+ if let Some(buffer_id) = snapshot
+ .buffer_snapshot
+ .buffer_id_for_anchor(previous_valid_anchor)
+ {
inlay_hint_cache.spawn_hint_resolve(
buffer_id,
excerpt_id,
@@ -418,24 +425,22 @@ pub fn update_inlay_link_and_hover_points(
}
if let Some((language_server_id, location)) =
hovered_hint_part.location
+ && secondary_held
+ && !editor.has_pending_nonempty_selection()
{
- if secondary_held
- && !editor.has_pending_nonempty_selection()
- {
- go_to_definition_updated = true;
- show_link_definition(
- shift_held,
- editor,
- TriggerPoint::InlayHint(
- highlight,
- location,
- language_server_id,
- ),
- snapshot,
- window,
- cx,
- );
- }
+ go_to_definition_updated = true;
+ show_link_definition(
+ shift_held,
+ editor,
+ TriggerPoint::InlayHint(
+ highlight,
+ location,
+ language_server_id,
+ ),
+ snapshot,
+ window,
+ cx,
+ );
}
}
}
@@ -561,7 +566,7 @@ pub fn show_link_definition(
provider.definitions(&buffer, buffer_position, preferred_kind, cx)
})?;
if let Some(task) = task {
- task.await.ok().map(|definition_result| {
+ task.await.ok().flatten().map(|definition_result| {
(
definition_result.iter().find_map(|link| {
link.origin.as_ref().and_then(|origin| {
@@ -657,11 +662,11 @@ pub fn show_link_definition(
pub(crate) fn find_url(
buffer: &Entity<language::Buffer>,
position: text::Anchor,
- mut cx: AsyncWindowContext,
+ cx: AsyncWindowContext,
) -> Option<(Range<text::Anchor>, String)> {
const LIMIT: usize = 2048;
- let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else {
+ let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else {
return None;
};
@@ -719,11 +724,11 @@ pub(crate) fn find_url(
pub(crate) fn find_url_from_range(
buffer: &Entity<language::Buffer>,
range: Range<text::Anchor>,
- mut cx: AsyncWindowContext,
+ cx: AsyncWindowContext,
) -> Option<String> {
const LIMIT: usize = 2048;
- let Ok(snapshot) = buffer.read_with(&mut cx, |buffer, _| buffer.snapshot()) else {
+ let Ok(snapshot) = buffer.read_with(&cx, |buffer, _| buffer.snapshot()) else {
return None;
};
@@ -766,10 +771,11 @@ pub(crate) fn find_url_from_range(
let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]);
- if let Some(link) = finder.links(&text).next() {
- if link.start() == 0 && link.end() == text.len() {
- return Some(link.as_str().to_string());
- }
+ if let Some(link) = finder.links(&text).next()
+ && link.start() == 0
+ && link.end() == text.len()
+ {
+ return Some(link.as_str().to_string());
}
None
@@ -794,7 +800,7 @@ pub(crate) async fn find_file(
) -> Option<ResolvedPath> {
project
.update(cx, |project, cx| {
- project.resolve_path_in_buffer(&candidate_file_path, buffer, cx)
+ project.resolve_path_in_buffer(candidate_file_path, buffer, cx)
})
.ok()?
.await
@@ -872,7 +878,7 @@ fn surrounding_filename(
.peekable();
while let Some(ch) = forwards.next() {
// Skip escaped whitespace
- if ch == '\\' && forwards.peek().map_or(false, |ch| ch.is_whitespace()) {
+ if ch == '\\' && forwards.peek().is_some_and(|ch| ch.is_whitespace()) {
token_end += ch.len_utf8();
let whitespace = forwards.next().unwrap();
token_end += whitespace.len_utf8();
@@ -142,11 +142,11 @@ pub fn hover_at_inlay(
.info_popovers
.iter()
.any(|InfoPopover { symbol_range, .. }| {
- if let RangeInEditor::Inlay(range) = symbol_range {
- if range == &inlay_hover.range {
- // Hover triggered from same location as last time. Don't show again.
- return true;
- }
+ if let RangeInEditor::Inlay(range) = symbol_range
+ && range == &inlay_hover.range
+ {
+ // Hover triggered from same location as last time. Don't show again.
+ return true;
}
false
})
@@ -167,17 +167,16 @@ pub fn hover_at_inlay(
let language_registry = project.read_with(cx, |p, _| p.languages().clone())?;
let blocks = vec![inlay_hover.tooltip];
- let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await;
+ let parsed_content =
+ parse_blocks(&blocks, Some(&language_registry), None, cx).await;
let scroll_handle = ScrollHandle::new();
let subscription = this
.update(cx, |_, cx| {
- if let Some(parsed_content) = &parsed_content {
- Some(cx.observe(parsed_content, |_, _, cx| cx.notify()))
- } else {
- None
- }
+ parsed_content.as_ref().map(|parsed_content| {
+ cx.observe(parsed_content, |_, _, cx| cx.notify())
+ })
})
.ok()
.flatten();
@@ -251,7 +250,9 @@ fn show_hover(
let (excerpt_id, _, _) = editor.buffer().read(cx).excerpt_containing(anchor, cx)?;
- let language_registry = editor.project.as_ref()?.read(cx).languages().clone();
+ let language_registry = editor
+ .project()
+ .map(|project| project.read(cx).languages().clone());
let provider = editor.semantics_provider.clone()?;
if !ignore_timeout {
@@ -267,13 +268,12 @@ fn show_hover(
}
// Don't request again if the location is the same as the previous request
- if let Some(triggered_from) = &editor.hover_state.triggered_from {
- if triggered_from
+ if let Some(triggered_from) = &editor.hover_state.triggered_from
+ && triggered_from
.cmp(&anchor, &snapshot.buffer_snapshot)
.is_eq()
- {
- return None;
- }
+ {
+ return None;
}
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
@@ -428,7 +428,7 @@ fn show_hover(
};
let hovers_response = if let Some(hover_request) = hover_request {
- hover_request.await
+ hover_request.await.unwrap_or_default()
} else {
Vec::new()
};
@@ -443,15 +443,14 @@ fn show_hover(
text: format!("Unicode character U+{:02X}", invisible as u32),
kind: HoverBlockKind::PlainText,
}];
- let parsed_content = parse_blocks(&blocks, &language_registry, None, cx).await;
+ let parsed_content =
+ parse_blocks(&blocks, language_registry.as_ref(), None, cx).await;
let scroll_handle = ScrollHandle::new();
let subscription = this
.update(cx, |_, cx| {
- if let Some(parsed_content) = &parsed_content {
- Some(cx.observe(parsed_content, |_, _, cx| cx.notify()))
- } else {
- None
- }
+ parsed_content.as_ref().map(|parsed_content| {
+ cx.observe(parsed_content, |_, _, cx| cx.notify())
+ })
})
.ok()
.flatten();
@@ -493,16 +492,15 @@ fn show_hover(
let blocks = hover_result.contents;
let language = hover_result.language;
- let parsed_content = parse_blocks(&blocks, &language_registry, language, cx).await;
+ let parsed_content =
+ parse_blocks(&blocks, language_registry.as_ref(), language, cx).await;
let scroll_handle = ScrollHandle::new();
hover_highlights.push(range.clone());
let subscription = this
.update(cx, |_, cx| {
- if let Some(parsed_content) = &parsed_content {
- Some(cx.observe(parsed_content, |_, _, cx| cx.notify()))
- } else {
- None
- }
+ parsed_content.as_ref().map(|parsed_content| {
+ cx.observe(parsed_content, |_, _, cx| cx.notify())
+ })
})
.ok()
.flatten();
@@ -583,7 +581,7 @@ fn same_diagnostic_hover(editor: &Editor, snapshot: &EditorSnapshot, anchor: Anc
async fn parse_blocks(
blocks: &[HoverBlock],
- language_registry: &Arc<LanguageRegistry>,
+ language_registry: Option<&Arc<LanguageRegistry>>,
language: Option<Arc<Language>>,
cx: &mut AsyncWindowContext,
) -> Option<Entity<Markdown>> {
@@ -599,18 +597,15 @@ async fn parse_blocks(
})
.join("\n\n");
- let rendered_block = cx
- .new_window_entity(|_window, cx| {
- Markdown::new(
- combined_text.into(),
- Some(language_registry.clone()),
- language.map(|language| language.name()),
- cx,
- )
- })
- .ok();
-
- rendered_block
+ cx.new_window_entity(|_window, cx| {
+ Markdown::new(
+ combined_text.into(),
+ language_registry.cloned(),
+ language.map(|language| language.name()),
+ cx,
+ )
+ })
+ .ok()
}
pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
@@ -622,7 +617,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let mut base_text_style = window.text_style();
base_text_style.refine(&TextStyleRefinement {
- font_family: Some(ui_font_family.clone()),
+ font_family: Some(ui_font_family),
font_fallbacks: ui_font_fallbacks,
color: Some(cx.theme().colors().editor_foreground),
..Default::default()
@@ -671,7 +666,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let mut base_text_style = window.text_style();
base_text_style.refine(&TextStyleRefinement {
- font_family: Some(ui_font_family.clone()),
+ font_family: Some(ui_font_family),
font_fallbacks: ui_font_fallbacks,
color: Some(cx.theme().colors().editor_foreground),
..Default::default()
@@ -712,59 +707,54 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
}
pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App) {
- if let Ok(uri) = Url::parse(&link) {
- if uri.scheme() == "file" {
- if let Some(workspace) = window.root::<Workspace>().flatten() {
- workspace.update(cx, |workspace, cx| {
- let task = workspace.open_abs_path(
- PathBuf::from(uri.path()),
- OpenOptions {
- visible: Some(OpenVisible::None),
- ..Default::default()
- },
- window,
- cx,
- );
+ if let Ok(uri) = Url::parse(&link)
+ && uri.scheme() == "file"
+ && let Some(workspace) = window.root::<Workspace>().flatten()
+ {
+ workspace.update(cx, |workspace, cx| {
+ let task = workspace.open_abs_path(
+ PathBuf::from(uri.path()),
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
+ window,
+ cx,
+ );
- cx.spawn_in(window, async move |_, cx| {
- let item = task.await?;
- // Ruby LSP uses URLs with #L1,1-4,4
- // we'll just take the first number and assume it's a line number
- let Some(fragment) = uri.fragment() else {
- return anyhow::Ok(());
- };
- let mut accum = 0u32;
- for c in fragment.chars() {
- if c >= '0' && c <= '9' && accum < u32::MAX / 2 {
- accum *= 10;
- accum += c as u32 - '0' as u32;
- } else if accum > 0 {
- break;
- }
- }
- if accum == 0 {
- return Ok(());
- }
- let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else {
- return Ok(());
- };
- editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(
- Default::default(),
- window,
- cx,
- |selections| {
- selections.select_ranges([text::Point::new(accum - 1, 0)
- ..text::Point::new(accum - 1, 0)]);
- },
- );
- })
- })
- .detach_and_log_err(cx);
- });
- return;
- }
- }
+ cx.spawn_in(window, async move |_, cx| {
+ let item = task.await?;
+ // Ruby LSP uses URLs with #L1,1-4,4
+ // we'll just take the first number and assume it's a line number
+ let Some(fragment) = uri.fragment() else {
+ return anyhow::Ok(());
+ };
+ let mut accum = 0u32;
+ for c in fragment.chars() {
+ if c >= '0' && c <= '9' && accum < u32::MAX / 2 {
+ accum *= 10;
+ accum += c as u32 - '0' as u32;
+ } else if accum > 0 {
+ break;
+ }
+ }
+ if accum == 0 {
+ return Ok(());
+ }
+ let Some(editor) = cx.update(|_, cx| item.act_as::<Editor>(cx))? else {
+ return Ok(());
+ };
+ editor.update_in(cx, |editor, window, cx| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
+ selections.select_ranges([
+ text::Point::new(accum - 1, 0)..text::Point::new(accum - 1, 0)
+ ]);
+ });
+ })
+ })
+ .detach_and_log_err(cx);
+ });
+ return;
}
cx.open_url(&link);
}
@@ -834,20 +824,19 @@ impl HoverState {
pub fn focused(&self, window: &mut Window, cx: &mut Context<Editor>) -> bool {
let mut hover_popover_is_focused = false;
for info_popover in &self.info_popovers {
- if let Some(markdown_view) = &info_popover.parsed_content {
- if markdown_view.focus_handle(cx).is_focused(window) {
- hover_popover_is_focused = true;
- }
+ if let Some(markdown_view) = &info_popover.parsed_content
+ && markdown_view.focus_handle(cx).is_focused(window)
+ {
+ hover_popover_is_focused = true;
}
}
- if let Some(diagnostic_popover) = &self.diagnostic_popover {
- if diagnostic_popover
+ if let Some(diagnostic_popover) = &self.diagnostic_popover
+ && diagnostic_popover
.markdown
.focus_handle(cx)
.is_focused(window)
- {
- hover_popover_is_focused = true;
- }
+ {
+ hover_popover_is_focused = true;
}
hover_popover_is_focused
}
@@ -907,7 +896,7 @@ impl InfoPopover {
.into_any_element()
}
- pub fn scroll(&self, amount: &ScrollAmount, window: &mut Window, cx: &mut Context<Editor>) {
+ pub fn scroll(&self, amount: ScrollAmount, window: &mut Window, cx: &mut Context<Editor>) {
let mut current = self.scroll_handle.offset();
current.y -= amount.pixels(
window.line_height(),
@@ -164,15 +164,15 @@ pub fn indent_guides_in_range(
let end_anchor = snapshot.buffer_snapshot.anchor_after(end_offset);
let mut fold_ranges = Vec::<Range<Point>>::new();
- let mut folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
- while let Some(fold) = folds.next() {
+ let folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
+ for fold in folds {
let start = fold.range.start.to_point(&snapshot.buffer_snapshot);
let end = fold.range.end.to_point(&snapshot.buffer_snapshot);
- if let Some(last_range) = fold_ranges.last_mut() {
- if last_range.end >= start {
- last_range.end = last_range.end.max(end);
- continue;
- }
+ if let Some(last_range) = fold_ranges.last_mut()
+ && last_range.end >= start
+ {
+ last_range.end = last_range.end.max(end);
+ continue;
}
fold_ranges.push(start..end);
}
@@ -475,10 +475,7 @@ impl InlayHintCache {
let excerpt_cached_hints = excerpt_cached_hints.read();
let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
- let Some(buffer) = shown_anchor
- .buffer_id
- .and_then(|buffer_id| multi_buffer.buffer(buffer_id))
- else {
+ let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else {
return false;
};
let buffer_snapshot = buffer.read(cx).snapshot();
@@ -498,16 +495,14 @@ impl InlayHintCache {
cmp::Ordering::Less | cmp::Ordering::Equal => {
if !old_kinds.contains(&cached_hint.kind)
&& new_kinds.contains(&cached_hint.kind)
- {
- if let Some(anchor) = multi_buffer_snapshot
+ && let Some(anchor) = multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, cached_hint.position)
- {
- to_insert.push(Inlay::hint(
- cached_hint_id.id(),
- anchor,
- cached_hint,
- ));
- }
+ {
+ to_insert.push(Inlay::hint(
+ cached_hint_id.id(),
+ anchor,
+ cached_hint,
+ ));
}
excerpt_cache.next();
}
@@ -522,16 +517,16 @@ impl InlayHintCache {
for cached_hint_id in excerpt_cache {
let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
let cached_hint_kind = maybe_missed_cached_hint.kind;
- if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
- if let Some(anchor) = multi_buffer_snapshot
+ if !old_kinds.contains(&cached_hint_kind)
+ && new_kinds.contains(&cached_hint_kind)
+ && let Some(anchor) = multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position)
- {
- to_insert.push(Inlay::hint(
- cached_hint_id.id(),
- anchor,
- maybe_missed_cached_hint,
- ));
- }
+ {
+ to_insert.push(Inlay::hint(
+ cached_hint_id.id(),
+ anchor,
+ maybe_missed_cached_hint,
+ ));
}
}
}
@@ -620,44 +615,44 @@ impl InlayHintCache {
) {
if let Some(excerpt_hints) = self.hints.get(&excerpt_id) {
let mut guard = excerpt_hints.write();
- if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
- if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state {
- let hint_to_resolve = cached_hint.clone();
- let server_id = *server_id;
- cached_hint.resolve_state = ResolveState::Resolving;
- drop(guard);
- cx.spawn_in(window, async move |editor, cx| {
- let resolved_hint_task = editor.update(cx, |editor, cx| {
- let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
- editor.semantics_provider.as_ref()?.resolve_inlay_hint(
- hint_to_resolve,
- buffer,
- server_id,
- cx,
- )
- })?;
- if let Some(resolved_hint_task) = resolved_hint_task {
- let mut resolved_hint =
- resolved_hint_task.await.context("hint resolve task")?;
- editor.read_with(cx, |editor, _| {
- if let Some(excerpt_hints) =
- editor.inlay_hint_cache.hints.get(&excerpt_id)
+ if let Some(cached_hint) = guard.hints_by_id.get_mut(&id)
+ && let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state
+ {
+ let hint_to_resolve = cached_hint.clone();
+ let server_id = *server_id;
+ cached_hint.resolve_state = ResolveState::Resolving;
+ drop(guard);
+ cx.spawn_in(window, async move |editor, cx| {
+ let resolved_hint_task = editor.update(cx, |editor, cx| {
+ let buffer = editor.buffer().read(cx).buffer(buffer_id)?;
+ editor.semantics_provider.as_ref()?.resolve_inlay_hint(
+ hint_to_resolve,
+ buffer,
+ server_id,
+ cx,
+ )
+ })?;
+ if let Some(resolved_hint_task) = resolved_hint_task {
+ let mut resolved_hint =
+ resolved_hint_task.await.context("hint resolve task")?;
+ editor.read_with(cx, |editor, _| {
+ if let Some(excerpt_hints) =
+ editor.inlay_hint_cache.hints.get(&excerpt_id)
+ {
+ let mut guard = excerpt_hints.write();
+ if let Some(cached_hint) = guard.hints_by_id.get_mut(&id)
+ && cached_hint.resolve_state == ResolveState::Resolving
{
- let mut guard = excerpt_hints.write();
- if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
- if cached_hint.resolve_state == ResolveState::Resolving {
- resolved_hint.resolve_state = ResolveState::Resolved;
- *cached_hint = resolved_hint;
- }
- }
+ resolved_hint.resolve_state = ResolveState::Resolved;
+ *cached_hint = resolved_hint;
}
- })?;
- }
+ }
+ })?;
+ }
- anyhow::Ok(())
- })
- .detach_and_log_err(cx);
- }
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
}
}
}
@@ -990,8 +985,8 @@ fn fetch_and_update_hints(
let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?;
- if !editor.registered_buffers.contains_key(&query.buffer_id) {
- if let Some(project) = editor.project.as_ref() {
+ if !editor.registered_buffers.contains_key(&query.buffer_id)
+ && let Some(project) = editor.project.as_ref() {
project.update(cx, |project, cx| {
editor.registered_buffers.insert(
query.buffer_id,
@@ -999,7 +994,6 @@ fn fetch_and_update_hints(
);
})
}
- }
editor
.semantics_provider
@@ -1240,14 +1234,12 @@ fn apply_hint_update(
.inlay_hint_cache
.allowed_hint_kinds
.contains(&new_hint.kind)
- {
- if let Some(new_hint_position) =
+ && let Some(new_hint_position) =
multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position)
- {
- splice
- .to_insert
- .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
- }
+ {
+ splice
+ .to_insert
+ .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
}
let new_id = InlayId::Hint(new_inlay_id);
cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
@@ -1347,7 +1339,7 @@ pub mod tests {
let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1;
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(file_with_hints).unwrap(),
+ lsp::Uri::from_file_path(file_with_hints).unwrap(),
);
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, i),
@@ -1457,7 +1449,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(file_with_hints).unwrap(),
+ lsp::Uri::from_file_path(file_with_hints).unwrap(),
);
let current_call_id =
Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
@@ -1602,7 +1594,7 @@ pub mod tests {
"Rust" => {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs"))
+ lsp::Uri::from_file_path(path!("/a/main.rs"))
.unwrap(),
);
rs_lsp_request_count.fetch_add(1, Ordering::Release)
@@ -1611,7 +1603,7 @@ pub mod tests {
"Markdown" => {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/other.md"))
+ lsp::Uri::from_file_path(path!("/a/other.md"))
.unwrap(),
);
md_lsp_request_count.fetch_add(1, Ordering::Release)
@@ -1797,7 +1789,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(file_with_hints).unwrap(),
+ lsp::Uri::from_file_path(file_with_hints).unwrap(),
);
Ok(Some(vec![
lsp::InlayHint {
@@ -2135,7 +2127,7 @@ pub mod tests {
let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(file_with_hints).unwrap(),
+ lsp::Uri::from_file_path(file_with_hints).unwrap(),
);
Ok(Some(vec![lsp::InlayHint {
position: lsp::Position::new(0, i),
@@ -2298,7 +2290,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
task_lsp_request_ranges.lock().push(params.range);
@@ -2641,11 +2633,11 @@ pub mod tests {
let task_editor_edited = Arc::clone(&closure_editor_edited);
async move {
let hint_text = if params.text_document.uri
- == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
+ == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
{
"main hint"
} else if params.text_document.uri
- == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
+ == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
{
"other hint"
} else {
@@ -2952,11 +2944,11 @@ pub mod tests {
let task_editor_edited = Arc::clone(&closure_editor_edited);
async move {
let hint_text = if params.text_document.uri
- == lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
+ == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
{
"main hint"
} else if params.text_document.uri
- == lsp::Url::from_file_path(path!("/a/other.rs")).unwrap()
+ == lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap()
{
"other hint"
} else {
@@ -3124,7 +3116,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
let query_start = params.range.start;
Ok(Some(vec![lsp::InlayHint {
@@ -3196,7 +3188,7 @@ pub mod tests {
async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(file_with_hints).unwrap(),
+ lsp::Uri::from_file_path(file_with_hints).unwrap(),
);
let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1;
@@ -3359,7 +3351,7 @@ pub mod tests {
move |params, _| async move {
assert_eq!(
params.text_document.uri,
- lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
+ lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
Ok(Some(
serde_json::from_value(json!([
@@ -42,6 +42,7 @@ use ui::{IconDecorationKind, prelude::*};
use util::{ResultExt, TryFutureExt, paths::PathExt};
use workspace::{
CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
+ invalid_buffer_view::InvalidBufferView,
item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
};
@@ -103,9 +104,9 @@ impl FollowableItem for Editor {
multibuffer = MultiBuffer::new(project.read(cx).capability());
let mut sorted_excerpts = state.excerpts.clone();
sorted_excerpts.sort_by_key(|e| e.id);
- let mut sorted_excerpts = sorted_excerpts.into_iter().peekable();
+ let sorted_excerpts = sorted_excerpts.into_iter().peekable();
- while let Some(excerpt) = sorted_excerpts.next() {
+ for excerpt in sorted_excerpts {
let Ok(buffer_id) = BufferId::new(excerpt.buffer_id) else {
continue;
};
@@ -201,7 +202,7 @@ impl FollowableItem for Editor {
if buffer
.as_singleton()
.and_then(|buffer| buffer.read(cx).file())
- .map_or(false, |file| file.is_private())
+ .is_some_and(|file| file.is_private())
{
return None;
}
@@ -293,7 +294,7 @@ impl FollowableItem for Editor {
EditorEvent::ExcerptsRemoved { ids, .. } => {
update
.deleted_excerpts
- .extend(ids.iter().map(ExcerptId::to_proto));
+ .extend(ids.iter().copied().map(ExcerptId::to_proto));
true
}
EditorEvent::ScrollPositionChanged { autoscroll, .. } if !autoscroll => {
@@ -524,8 +525,8 @@ fn serialize_selection(
) -> proto::Selection {
proto::Selection {
id: selection.id as u64,
- start: Some(serialize_anchor(&selection.start, &buffer)),
- end: Some(serialize_anchor(&selection.end, &buffer)),
+ start: Some(serialize_anchor(&selection.start, buffer)),
+ end: Some(serialize_anchor(&selection.end, buffer)),
reversed: selection.reversed,
}
}
@@ -650,10 +651,15 @@ impl Item for Editor {
if let Some(path) = path_for_buffer(&self.buffer, detail, true, cx) {
path.to_string_lossy().to_string().into()
} else {
- "untitled".into()
+ // Use the same logic as the displayed title for consistency
+ self.buffer.read(cx).title(cx).to_string().into()
}
}
+ fn suggested_filename(&self, cx: &App) -> SharedString {
+ self.buffer.read(cx).title(cx).to_string().into()
+ }
+
fn tab_icon(&self, _: &Window, cx: &App) -> Option<Icon> {
ItemSettings::get_global(cx)
.file_icons
@@ -674,7 +680,7 @@ impl Item for Editor {
let buffer = buffer.read(cx);
let path = buffer.project_path(cx)?;
let buffer_id = buffer.remote_id();
- let project = self.project.as_ref()?.read(cx);
+ let project = self.project()?.read(cx);
let entry = project.entry_for_path(&path, cx)?;
let (repo, repo_path) = project
.git_store()
@@ -711,7 +717,7 @@ impl Item for Editor {
.read(cx)
.as_singleton()
.and_then(|buffer| buffer.read(cx).file())
- .map_or(false, |file| file.disk_state() == DiskState::Deleted);
+ .is_some_and(|file| file.disk_state() == DiskState::Deleted);
h_flex()
.gap_2()
@@ -770,12 +776,6 @@ impl Item for Editor {
self.nav_history = Some(history);
}
- fn discarded(&self, _project: Entity<Project>, _: &mut Window, cx: &mut Context<Self>) {
- for buffer in self.buffer().clone().read(cx).all_buffers() {
- buffer.update(cx, |buffer, cx| buffer.discarded(cx))
- }
- }
-
fn on_removed(&self, cx: &App) {
self.report_editor_event(ReportEditorEvent::Closed, None, cx);
}
@@ -926,10 +926,10 @@ impl Item for Editor {
})?;
buffer
.update(cx, |buffer, cx| {
- if let Some(transaction) = transaction {
- if !buffer.is_singleton() {
- buffer.push_transaction(&transaction.0, cx);
- }
+ if let Some(transaction) = transaction
+ && !buffer.is_singleton()
+ {
+ buffer.push_transaction(&transaction.0, cx);
}
})
.ok();
@@ -1005,24 +1005,18 @@ impl Item for Editor {
) {
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
if let Some(workspace) = &workspace.weak_handle().upgrade() {
- cx.subscribe(
- &workspace,
- |editor, _, event: &workspace::Event, _cx| match event {
- workspace::Event::ModalOpened => {
- editor.mouse_context_menu.take();
- editor.inline_blame_popover.take();
- }
- _ => {}
- },
- )
+ cx.subscribe(workspace, |editor, _, event: &workspace::Event, _cx| {
+ if let workspace::Event::ModalOpened = event {
+ editor.mouse_context_menu.take();
+ editor.inline_blame_popover.take();
+ }
+ })
.detach();
}
}
fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
match event {
- EditorEvent::Closed => f(ItemEvent::CloseItem),
-
EditorEvent::Saved | EditorEvent::TitleChanged => {
f(ItemEvent::UpdateTab);
f(ItemEvent::UpdateBreadcrumbs);
@@ -1036,6 +1030,10 @@ impl Item for Editor {
f(ItemEvent::UpdateBreadcrumbs);
}
+ EditorEvent::BreadcrumbsChanged => {
+ f(ItemEvent::UpdateBreadcrumbs);
+ }
+
EditorEvent::DirtyChanged => {
f(ItemEvent::UpdateTab);
}
@@ -1132,7 +1130,7 @@ impl SerializableItem for Editor {
// First create the empty buffer
let buffer = project
- .update(cx, |project, cx| project.create_buffer(cx))?
+ .update(cx, |project, cx| project.create_buffer(true, cx))?
.await?;
// Then set the text so that the dirty bit is set correctly
@@ -1240,7 +1238,7 @@ impl SerializableItem for Editor {
..
} => window.spawn(cx, async move |cx| {
let buffer = project
- .update(cx, |project, cx| project.create_buffer(cx))?
+ .update(cx, |project, cx| project.create_buffer(true, cx))?
.await?;
cx.update(|window, cx| {
@@ -1288,7 +1286,7 @@ impl SerializableItem for Editor {
project
.read(cx)
.worktree_for_id(worktree_id, cx)
- .and_then(|worktree| worktree.read(cx).absolutize(&file.path()).ok())
+ .and_then(|worktree| worktree.read(cx).absolutize(file.path()).ok())
.or_else(|| {
let full_path = file.full_path(cx);
let project_path = project.read(cx).find_project_path(&full_path, cx)?;
@@ -1366,40 +1364,47 @@ impl ProjectItem for Editor {
let mut editor = Self::for_buffer(buffer.clone(), Some(project), window, cx);
if let Some((excerpt_id, buffer_id, snapshot)) =
editor.buffer().read(cx).snapshot(cx).as_singleton()
+ && WorkspaceSettings::get(None, cx).restore_on_file_reopen
+ && let Some(restoration_data) = Self::project_item_kind()
+ .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind))
+ .and_then(|data| data.downcast_ref::<EditorRestorationData>())
+ .and_then(|data| {
+ let file = project::File::from_dyn(buffer.read(cx).file())?;
+ data.entries.get(&file.abs_path(cx))
+ })
{
- if WorkspaceSettings::get(None, cx).restore_on_file_reopen {
- if let Some(restoration_data) = Self::project_item_kind()
- .and_then(|kind| pane.as_ref()?.project_item_restoration_data.get(&kind))
- .and_then(|data| data.downcast_ref::<EditorRestorationData>())
- .and_then(|data| {
- let file = project::File::from_dyn(buffer.read(cx).file())?;
- data.entries.get(&file.abs_path(cx))
- })
- {
- editor.fold_ranges(
- clip_ranges(&restoration_data.folds, &snapshot),
- false,
- window,
- cx,
- );
- if !restoration_data.selections.is_empty() {
- editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
- s.select_ranges(clip_ranges(&restoration_data.selections, &snapshot));
- });
- }
- let (top_row, offset) = restoration_data.scroll_position;
- let anchor = Anchor::in_buffer(
- *excerpt_id,
- buffer_id,
- snapshot.anchor_before(Point::new(top_row, 0)),
- );
- editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx);
- }
+ editor.fold_ranges(
+ clip_ranges(&restoration_data.folds, snapshot),
+ false,
+ window,
+ cx,
+ );
+ if !restoration_data.selections.is_empty() {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges(clip_ranges(&restoration_data.selections, snapshot));
+ });
}
+ let (top_row, offset) = restoration_data.scroll_position;
+ let anchor = Anchor::in_buffer(
+ *excerpt_id,
+ buffer_id,
+ snapshot.anchor_before(Point::new(top_row, 0)),
+ );
+ editor.set_scroll_anchor(ScrollAnchor { anchor, offset }, window, cx);
}
editor
}
+
+ fn for_broken_project_item(
+ abs_path: &Path,
+ is_local: bool,
+ e: &anyhow::Error,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<InvalidBufferView> {
+ Some(InvalidBufferView::new(abs_path, is_local, e, window, cx))
+ }
}
fn clip_ranges<'a>(
@@ -37,7 +37,7 @@ pub(crate) fn should_auto_close(
let text = buffer
.text_for_range(edited_range.clone())
.collect::<String>();
- let edited_range = edited_range.to_offset(&buffer);
+ let edited_range = edited_range.to_offset(buffer);
if !text.ends_with(">") {
continue;
}
@@ -51,12 +51,11 @@ pub(crate) fn should_auto_close(
continue;
};
let mut jsx_open_tag_node = node;
- if node.grammar_name() != config.open_tag_node_name {
- if let Some(parent) = node.parent() {
- if parent.grammar_name() == config.open_tag_node_name {
- jsx_open_tag_node = parent;
- }
- }
+ if node.grammar_name() != config.open_tag_node_name
+ && let Some(parent) = node.parent()
+ && parent.grammar_name() == config.open_tag_node_name
+ {
+ jsx_open_tag_node = parent;
}
if jsx_open_tag_node.grammar_name() != config.open_tag_node_name {
continue;
@@ -87,9 +86,9 @@ pub(crate) fn should_auto_close(
});
}
if to_auto_edit.is_empty() {
- return None;
+ None
} else {
- return Some(to_auto_edit);
+ Some(to_auto_edit)
}
}
@@ -182,12 +181,12 @@ pub(crate) fn generate_auto_close_edits(
*/
{
let tag_node_name_equals = |node: &Node, name: &str| {
- let is_empty = name.len() == 0;
+ let is_empty = name.is_empty();
if let Some(node_name) = node.named_child(TS_NODE_TAG_NAME_CHILD_INDEX) {
let range = node_name.byte_range();
return buffer.text_for_range(range).equals_str(name);
}
- return is_empty;
+ is_empty
};
let tree_root_node = {
@@ -208,7 +207,7 @@ pub(crate) fn generate_auto_close_edits(
cur = descendant;
}
- assert!(ancestors.len() > 0);
+ assert!(!ancestors.is_empty());
let mut tree_root_node = open_tag;
@@ -228,7 +227,7 @@ pub(crate) fn generate_auto_close_edits(
let has_open_tag_with_same_tag_name = ancestor
.named_child(0)
.filter(|n| n.kind() == config.open_tag_node_name)
- .map_or(false, |element_open_tag_node| {
+ .is_some_and(|element_open_tag_node| {
tag_node_name_equals(&element_open_tag_node, &tag_name)
});
if has_open_tag_with_same_tag_name {
@@ -264,8 +263,7 @@ pub(crate) fn generate_auto_close_edits(
}
let is_after_open_tag = |node: &Node| {
- return node.start_byte() < open_tag.start_byte()
- && node.end_byte() < open_tag.start_byte();
+ node.start_byte() < open_tag.start_byte() && node.end_byte() < open_tag.start_byte()
};
// perf: use cursor for more efficient traversal
@@ -284,10 +282,8 @@ pub(crate) fn generate_auto_close_edits(
unclosed_open_tag_count -= 1;
}
} else if has_erroneous_close_tag && kind == erroneous_close_tag_node_name {
- if tag_node_name_equals(&node, &tag_name) {
- if !is_after_open_tag(&node) {
- unclosed_open_tag_count -= 1;
- }
+ if tag_node_name_equals(&node, &tag_name) && !is_after_open_tag(&node) {
+ unclosed_open_tag_count -= 1;
}
} else if kind == config.jsx_element_node_name {
// perf: filter only open,close,element,erroneous nodes
@@ -304,7 +300,7 @@ pub(crate) fn generate_auto_close_edits(
let edit_range = edit_anchor..edit_anchor;
edits.push((edit_range, format!("</{}>", tag_name)));
}
- return Ok(edits);
+ Ok(edits)
}
pub(crate) fn refresh_enabled_in_any_buffer(
@@ -370,7 +366,7 @@ pub(crate) fn construct_initial_buffer_versions_map<
initial_buffer_versions.insert(buffer_id, buffer_version);
}
}
- return initial_buffer_versions;
+ initial_buffer_versions
}
pub(crate) fn handle_from(
@@ -458,12 +454,9 @@ pub(crate) fn handle_from(
let ensure_no_edits_since_start = || -> Option<()> {
let has_edits_since_start = this
.read_with(cx, |this, cx| {
- this.buffer
- .read(cx)
- .buffer(buffer_id)
- .map_or(true, |buffer| {
- buffer.read(cx).has_edits_since(&buffer_version_initial)
- })
+ this.buffer.read(cx).buffer(buffer_id).is_none_or(|buffer| {
+ buffer.read(cx).has_edits_since(&buffer_version_initial)
+ })
})
.ok()?;
@@ -514,7 +507,7 @@ pub(crate) fn handle_from(
{
let selections = this
- .read_with(cx, |this, _| this.selections.disjoint_anchors().clone())
+ .read_with(cx, |this, _| this.selections.disjoint_anchors())
.ok()?;
for selection in selections.iter() {
let Some(selection_buffer_offset_head) =
@@ -815,10 +808,7 @@ mod jsx_tag_autoclose_tests {
);
buf
});
- let buffer_c = cx.new(|cx| {
- let buf = language::Buffer::local("<span", cx);
- buf
- });
+ let buffer_c = cx.new(|cx| language::Buffer::local("<span", cx));
let buffer = cx.new(|cx| {
let mut buf = MultiBuffer::new(language::Capability::ReadWrite);
buf.push_excerpts(
@@ -51,7 +51,7 @@ pub(super) fn refresh_linked_ranges(
if editor.pending_rename.is_some() {
return None;
}
- let project = editor.project.as_ref()?.downgrade();
+ let project = editor.project()?.downgrade();
editor.linked_editing_range_task = Some(cx.spawn_in(window, async move |editor, cx| {
cx.background_executor().timer(UPDATE_DEBOUNCE).await;
@@ -72,7 +72,7 @@ pub(super) fn refresh_linked_ranges(
// Throw away selections spanning multiple buffers.
continue;
}
- if let Some(buffer) = end_position.buffer_id.and_then(|id| buffer.buffer(id)) {
+ if let Some(buffer) = buffer.buffer_for_anchor(end_position, cx) {
applicable_selections.push((
buffer,
start_position.text_anchor,
@@ -207,7 +207,7 @@ impl Editor {
.entry(buffer_snapshot.remote_id())
.or_insert_with(Vec::new);
let excerpt_point_range =
- excerpt_range.context.to_point_utf16(&buffer_snapshot);
+ excerpt_range.context.to_point_utf16(buffer_snapshot);
excerpt_data.push((
excerpt_id,
buffer_snapshot.clone(),
@@ -76,7 +76,7 @@ async fn lsp_task_context(
let project_env = project
.update(cx, |project, cx| {
- project.buffer_environment(&buffer, &worktree_store, cx)
+ project.buffer_environment(buffer, &worktree_store, cx)
})
.ok()?
.await;
@@ -147,16 +147,15 @@ pub fn lsp_tasks(
},
cx,
)
- }) {
- if let Some(new_runnables) = runnables_task.await.log_err() {
- new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map(
- |(location, runnable)| {
- let resolved_task =
- runnable.resolve_task(&id_base, &lsp_buffer_context)?;
- Some((location, resolved_task))
- },
- ));
- }
+ }) && let Some(new_runnables) = runnables_task.await.log_err()
+ {
+ new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map(
+ |(location, runnable)| {
+ let resolved_task =
+ runnable.resolve_task(&id_base, &lsp_buffer_context)?;
+ Some((location, resolved_task))
+ },
+ ));
}
lsp_tasks
.entry(source_kind)
@@ -61,13 +61,13 @@ impl MouseContextMenu {
source,
offset: position - (source_position + content_origin),
};
- return Some(MouseContextMenu::new(
+ Some(MouseContextMenu::new(
editor,
menu_position,
context_menu,
window,
cx,
- ));
+ ))
}
pub(crate) fn new(
@@ -102,11 +102,11 @@ impl MouseContextMenu {
let display_snapshot = &editor
.display_map
.update(cx, |display_map, cx| display_map.snapshot(cx));
- let selection_init_range = selection_init.display_range(&display_snapshot);
+ let selection_init_range = selection_init.display_range(display_snapshot);
let selection_now_range = editor
.selections
.newest_anchor()
- .display_range(&display_snapshot);
+ .display_range(display_snapshot);
if selection_now_range == selection_init_range {
return;
}
@@ -190,14 +190,16 @@ pub fn deploy_context_menu(
.all::<PointUtf16>(cx)
.into_iter()
.any(|s| !s.is_empty());
- let has_git_repo = anchor.buffer_id.is_some_and(|buffer_id| {
- project
- .read(cx)
- .git_store()
- .read(cx)
- .repository_and_path_for_buffer_id(buffer_id, cx)
- .is_some()
- });
+ let has_git_repo = buffer
+ .buffer_id_for_anchor(anchor)
+ .is_some_and(|buffer_id| {
+ project
+ .read(cx)
+ .git_store()
+ .read(cx)
+ .repository_and_path_for_buffer_id(buffer_id, cx)
+ .is_some()
+ });
let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
let run_to_cursor = window.is_action_available(&RunToCursor, cx);
@@ -4,7 +4,7 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{DisplayRow, EditorStyle, ToOffset, ToPoint, scroll::ScrollAnchor};
use gpui::{Pixels, WindowTextSystem};
-use language::Point;
+use language::{CharClassifier, Point};
use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
use serde::Deserialize;
use workspace::searchable::Direction;
@@ -289,12 +289,114 @@ pub fn previous_word_start_or_newline(map: &DisplaySnapshot, point: DisplayPoint
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
- (classifier.kind(left) != classifier.kind(right) && !right.is_whitespace())
+ (classifier.kind(left) != classifier.kind(right) && !classifier.is_whitespace(right))
|| left == '\n'
|| right == '\n'
})
}
+/// Text movements are too greedy, making deletions too greedy too.
+/// Makes deletions more ergonomic by potentially reducing the deletion range based on its text contents:
+/// * whitespace sequences with length >= 2 stop the deletion after removal (despite movement jumping over the word behind the whitespaces)
+/// * brackets stop the deletion after removal (despite movement currently not accounting for these and jumping over)
+pub fn adjust_greedy_deletion(
+ map: &DisplaySnapshot,
+ delete_from: DisplayPoint,
+ delete_until: DisplayPoint,
+ ignore_brackets: bool,
+) -> DisplayPoint {
+ if delete_from == delete_until {
+ return delete_until;
+ }
+ let is_backward = delete_from > delete_until;
+ let delete_range = if is_backward {
+ map.display_point_to_point(delete_until, Bias::Left)
+ .to_offset(&map.buffer_snapshot)
+ ..map
+ .display_point_to_point(delete_from, Bias::Right)
+ .to_offset(&map.buffer_snapshot)
+ } else {
+ map.display_point_to_point(delete_from, Bias::Left)
+ .to_offset(&map.buffer_snapshot)
+ ..map
+ .display_point_to_point(delete_until, Bias::Right)
+ .to_offset(&map.buffer_snapshot)
+ };
+
+ let trimmed_delete_range = if ignore_brackets {
+ delete_range
+ } else {
+ let brackets_in_delete_range = map
+ .buffer_snapshot
+ .bracket_ranges(delete_range.clone())
+ .into_iter()
+ .flatten()
+ .flat_map(|(left_bracket, right_bracket)| {
+ [
+ left_bracket.start,
+ left_bracket.end,
+ right_bracket.start,
+ right_bracket.end,
+ ]
+ })
+ .filter(|&bracket| delete_range.start < bracket && bracket < delete_range.end);
+ let closest_bracket = if is_backward {
+ brackets_in_delete_range.max()
+ } else {
+ brackets_in_delete_range.min()
+ };
+
+ if is_backward {
+ closest_bracket.unwrap_or(delete_range.start)..delete_range.end
+ } else {
+ delete_range.start..closest_bracket.unwrap_or(delete_range.end)
+ }
+ };
+
+ let mut whitespace_sequences = Vec::new();
+ let mut current_offset = trimmed_delete_range.start;
+ let mut whitespace_sequence_length = 0;
+ let mut whitespace_sequence_start = 0;
+ for ch in map
+ .buffer_snapshot
+ .text_for_range(trimmed_delete_range.clone())
+ .flat_map(str::chars)
+ {
+ if ch.is_whitespace() {
+ if whitespace_sequence_length == 0 {
+ whitespace_sequence_start = current_offset;
+ }
+ whitespace_sequence_length += 1;
+ } else {
+ if whitespace_sequence_length >= 2 {
+ whitespace_sequences.push((whitespace_sequence_start, current_offset));
+ }
+ whitespace_sequence_start = 0;
+ whitespace_sequence_length = 0;
+ }
+ current_offset += ch.len_utf8();
+ }
+ if whitespace_sequence_length >= 2 {
+ whitespace_sequences.push((whitespace_sequence_start, current_offset));
+ }
+
+ let closest_whitespace_end = if is_backward {
+ whitespace_sequences.last().map(|&(start, _)| start)
+ } else {
+ whitespace_sequences.first().map(|&(_, end)| end)
+ };
+
+ closest_whitespace_end
+ .unwrap_or_else(|| {
+ if is_backward {
+ trimmed_delete_range.start
+ } else {
+ trimmed_delete_range.end
+ }
+ })
+ .to_display_point(map)
+}
+
/// Returns a position of the previous subword boundary, where a subword is defined as a run of
/// word characters of the same "subkind" - where subcharacter kinds are '_' character,
/// lowerspace characters and uppercase characters.
@@ -303,15 +405,18 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
- let is_word_start =
- classifier.kind(left) != classifier.kind(right) && !right.is_whitespace();
- let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
- || left == '_' && right != '_'
- || left.is_lowercase() && right.is_uppercase();
- is_word_start || is_subword_start || left == '\n'
+ is_subword_start(left, right, &classifier) || left == '\n'
})
}
+pub fn is_subword_start(left: char, right: char, classifier: &CharClassifier) -> bool {
+ let is_word_start = classifier.kind(left) != classifier.kind(right) && !right.is_whitespace();
+ let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
+ || left == '_' && right != '_'
+ || left.is_lowercase() && right.is_uppercase();
+ is_word_start || is_subword_start
+}
+
/// Returns a position of the next word boundary, where a word character is defined as either
/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
@@ -361,15 +466,19 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
find_boundary(map, point, FindRange::MultiLine, |left, right| {
- let is_word_end =
- (classifier.kind(left) != classifier.kind(right)) && !classifier.is_whitespace(left);
- let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
- || left != '_' && right == '_'
- || left.is_lowercase() && right.is_uppercase();
- is_word_end || is_subword_end || right == '\n'
+ is_subword_end(left, right, &classifier) || right == '\n'
})
}
+pub fn is_subword_end(left: char, right: char, classifier: &CharClassifier) -> bool {
+ let is_word_end =
+ (classifier.kind(left) != classifier.kind(right)) && !classifier.is_whitespace(left);
+ let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
+ || left != '_' && right == '_'
+ || left.is_lowercase() && right.is_uppercase();
+ is_word_end || is_subword_end
+}
+
/// Returns a position of the start of the current paragraph, where a paragraph
/// is defined as a run of non-blank lines.
pub fn start_of_paragraph(
@@ -439,17 +548,17 @@ pub fn start_of_excerpt(
};
match direction {
Direction::Prev => {
- let mut start = excerpt.start_anchor().to_display_point(&map);
+ let mut start = excerpt.start_anchor().to_display_point(map);
if start >= display_point && start.row() > DisplayRow(0) {
let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
return display_point;
};
- start = excerpt.start_anchor().to_display_point(&map);
+ start = excerpt.start_anchor().to_display_point(map);
}
start
}
Direction::Next => {
- let mut end = excerpt.end_anchor().to_display_point(&map);
+ let mut end = excerpt.end_anchor().to_display_point(map);
*end.row_mut() += 1;
map.clip_point(end, Bias::Right)
}
@@ -467,7 +576,7 @@ pub fn end_of_excerpt(
};
match direction {
Direction::Prev => {
- let mut start = excerpt.start_anchor().to_display_point(&map);
+ let mut start = excerpt.start_anchor().to_display_point(map);
if start.row() > DisplayRow(0) {
*start.row_mut() -= 1;
}
@@ -476,7 +585,7 @@ pub fn end_of_excerpt(
start
}
Direction::Next => {
- let mut end = excerpt.end_anchor().to_display_point(&map);
+ let mut end = excerpt.end_anchor().to_display_point(map);
*end.column_mut() = 0;
if end <= display_point {
*end.row_mut() += 1;
@@ -485,7 +594,7 @@ pub fn end_of_excerpt(
else {
return display_point;
};
- end = excerpt.end_anchor().to_display_point(&map);
+ end = excerpt.end_anchor().to_display_point(map);
*end.column_mut() = 0;
}
end
@@ -510,10 +619,10 @@ pub fn find_preceding_boundary_point(
if find_range == FindRange::SingleLine && ch == '\n' {
break;
}
- if let Some(prev_ch) = prev_ch {
- if is_boundary(ch, prev_ch) {
- break;
- }
+ if let Some(prev_ch) = prev_ch
+ && is_boundary(ch, prev_ch)
+ {
+ break;
}
offset -= ch.len_utf8();
@@ -562,13 +671,13 @@ pub fn find_boundary_point(
if find_range == FindRange::SingleLine && ch == '\n' {
break;
}
- if let Some(prev_ch) = prev_ch {
- if is_boundary(prev_ch, ch) {
- if return_point_before_boundary {
- return map.clip_point(prev_offset.to_display_point(map), Bias::Right);
- } else {
- break;
- }
+ if let Some(prev_ch) = prev_ch
+ && is_boundary(prev_ch, ch)
+ {
+ if return_point_before_boundary {
+ return map.clip_point(prev_offset.to_display_point(map), Bias::Right);
+ } else {
+ break;
}
}
prev_offset = offset;
@@ -603,13 +712,13 @@ pub fn find_preceding_boundary_trail(
// Find the boundary
let start_offset = offset;
for ch in forward {
- if let Some(prev_ch) = prev_ch {
- if is_boundary(prev_ch, ch) {
- if start_offset == offset {
- trail_offset = Some(offset);
- } else {
- break;
- }
+ if let Some(prev_ch) = prev_ch
+ && is_boundary(prev_ch, ch)
+ {
+ if start_offset == offset {
+ trail_offset = Some(offset);
+ } else {
+ break;
}
}
offset -= ch.len_utf8();
@@ -651,13 +760,13 @@ pub fn find_boundary_trail(
// Find the boundary
let start_offset = offset;
for ch in forward {
- if let Some(prev_ch) = prev_ch {
- if is_boundary(prev_ch, ch) {
- if start_offset == offset {
- trail_offset = Some(offset);
- } else {
- break;
- }
+ if let Some(prev_ch) = prev_ch
+ && is_boundary(prev_ch, ch)
+ {
+ if start_offset == offset {
+ trail_offset = Some(offset);
+ } else {
+ break;
}
}
offset += ch.len_utf8();
@@ -1,13 +1,17 @@
use anyhow::Result;
-use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
-use db::sqlez::statement::Statement;
+use db::{
+ query,
+ sqlez::{
+ bindable::{Bind, Column, StaticColumnCount},
+ domain::Domain,
+ statement::Statement,
+ },
+ sqlez_macros::sql,
+};
use fs::MTime;
use itertools::Itertools as _;
use std::path::PathBuf;
-use db::sqlez_macros::sql;
-use db::{define_connection, query};
-
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
#[derive(Clone, Debug, PartialEq, Default)]
@@ -83,7 +87,11 @@ impl Column for SerializedEditor {
}
}
-define_connection!(
+pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection);
+
+impl Domain for EditorDb {
+ const NAME: &str = stringify!(EditorDb);
+
// Current schema shape using pseudo-rust syntax:
// editors(
// item_id: usize,
@@ -113,7 +121,8 @@ define_connection!(
// start: usize,
// end: usize,
// )
- pub static ref DB: EditorDb<WorkspaceDb> = &[
+
+ const MIGRATIONS: &[&str] = &[
sql! (
CREATE TABLE editors(
item_id INTEGER NOT NULL,
@@ -189,7 +198,9 @@ define_connection!(
) STRICT;
),
];
-);
+}
+
+db::static_connection!(DB, EditorDb, [WorkspaceDb]);
// https://www.sqlite.org/limits.html
// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
@@ -241,24 +241,13 @@ impl ProposedChangesEditor {
event: &BufferEvent,
_cx: &mut Context<Self>,
) {
- match event {
- BufferEvent::Operation { .. } => {
- self.recalculate_diffs_tx
- .unbounded_send(RecalculateDiff {
- buffer,
- debounce: true,
- })
- .ok();
- }
- // BufferEvent::DiffBaseChanged => {
- // self.recalculate_diffs_tx
- // .unbounded_send(RecalculateDiff {
- // buffer,
- // debounce: false,
- // })
- // .ok();
- // }
- _ => (),
+ if let BufferEvent::Operation { .. } = event {
+ self.recalculate_diffs_tx
+ .unbounded_send(RecalculateDiff {
+ buffer,
+ debounce: true,
+ })
+ .ok();
}
}
}
@@ -442,7 +431,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
buffer: &Entity<Buffer>,
position: text::Anchor,
cx: &mut App,
- ) -> Option<Task<Vec<project::Hover>>> {
+ ) -> Option<Task<Option<Vec<project::Hover>>>> {
let buffer = self.to_base(buffer, &[position], cx)?;
self.0.hover(&buffer, position, cx)
}
@@ -478,7 +467,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
}
fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool {
- if let Some(buffer) = self.to_base(&buffer, &[], cx) {
+ if let Some(buffer) = self.to_base(buffer, &[], cx) {
self.0.supports_inlay_hints(&buffer, cx)
} else {
false
@@ -491,7 +480,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
position: text::Anchor,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<project::DocumentHighlight>>>> {
- let buffer = self.to_base(&buffer, &[position], cx)?;
+ let buffer = self.to_base(buffer, &[position], cx)?;
self.0.document_highlights(&buffer, position, cx)
}
@@ -501,8 +490,8 @@ impl SemanticsProvider for BranchBufferSemanticsProvider {
position: text::Anchor,
kind: crate::GotoDefinitionKind,
cx: &mut App,
- ) -> Option<Task<anyhow::Result<Vec<project::LocationLink>>>> {
- let buffer = self.to_base(&buffer, &[position], cx)?;
+ ) -> Option<Task<anyhow::Result<Option<Vec<project::LocationLink>>>>> {
+ let buffer = self.to_base(buffer, &[position], cx)?;
self.0.definitions(&buffer, position, kind, cx)
}
@@ -26,6 +26,17 @@ fn is_rust_language(language: &Language) -> bool {
}
pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &mut App) {
+ if editor.read(cx).project().is_some_and(|project| {
+ project
+ .read(cx)
+ .language_server_statuses(cx)
+ .any(|(_, status)| status.name == RUST_ANALYZER_NAME)
+ }) {
+ register_action(editor, window, cancel_flycheck_action);
+ register_action(editor, window, run_flycheck_action);
+ register_action(editor, window, clear_flycheck_action);
+ }
+
if editor
.read(cx)
.buffer()
@@ -35,12 +46,9 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
.filter_map(|buffer| buffer.read(cx).language())
.any(|language| is_rust_language(language))
{
- register_action(&editor, window, go_to_parent_module);
- register_action(&editor, window, expand_macro_recursively);
- register_action(&editor, window, open_docs);
- register_action(&editor, window, cancel_flycheck_action);
- register_action(&editor, window, run_flycheck_action);
- register_action(&editor, window, clear_flycheck_action);
+ register_action(editor, window, go_to_parent_module);
+ register_action(editor, window, expand_macro_recursively);
+ register_action(editor, window, open_docs);
}
}
@@ -192,7 +200,7 @@ pub fn expand_macro_recursively(
}
let buffer = project
- .update(cx, |project, cx| project.create_buffer(cx))?
+ .update(cx, |project, cx| project.create_buffer(false, cx))?
.await?;
workspace.update_in(cx, |workspace, window, cx| {
buffer.update(cx, |buffer, cx| {
@@ -285,11 +293,11 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu
workspace.update(cx, |_workspace, cx| {
// Check if the local document exists, otherwise fallback to the online document.
// Open with the default browser.
- if let Some(local_url) = docs_urls.local {
- if fs::metadata(Path::new(&local_url[8..])).is_ok() {
- cx.open_url(&local_url);
- return;
- }
+ if let Some(local_url) = docs_urls.local
+ && fs::metadata(Path::new(&local_url[8..])).is_ok()
+ {
+ cx.open_url(&local_url);
+ return;
}
if let Some(web_url) = docs_urls.web {
@@ -309,7 +317,7 @@ fn cancel_flycheck_action(
let Some(project) = &editor.project else {
return;
};
- let Some(buffer_id) = editor
+ let buffer_id = editor
.selections
.disjoint_anchors()
.iter()
@@ -321,10 +329,7 @@ fn cancel_flycheck_action(
.read(cx)
.entry_id(cx)?;
project.path_for_entry(entry_id, cx)
- })
- else {
- return;
- };
+ });
cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
}
@@ -337,7 +342,7 @@ fn run_flycheck_action(
let Some(project) = &editor.project else {
return;
};
- let Some(buffer_id) = editor
+ let buffer_id = editor
.selections
.disjoint_anchors()
.iter()
@@ -349,10 +354,7 @@ fn run_flycheck_action(
.read(cx)
.entry_id(cx)?;
project.path_for_entry(entry_id, cx)
- })
- else {
- return;
- };
+ });
run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
}
@@ -365,7 +367,7 @@ fn clear_flycheck_action(
let Some(project) = &editor.project else {
return;
};
- let Some(buffer_id) = editor
+ let buffer_id = editor
.selections
.disjoint_anchors()
.iter()
@@ -377,9 +379,6 @@ fn clear_flycheck_action(
.read(cx)
.entry_id(cx)?;
project.path_for_entry(entry_id, cx)
- })
- else {
- return;
- };
+ });
clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
}
@@ -675,7 +675,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
- if matches!(self.mode, EditorMode::SingleLine { .. }) {
+ if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate();
return;
}
@@ -703,20 +703,20 @@ impl Editor {
if matches!(
settings.defaults.soft_wrap,
SoftWrap::PreferredLineLength | SoftWrap::Bounded
- ) {
- if (settings.defaults.preferred_line_length as f32) < visible_column_count {
- visible_column_count = settings.defaults.preferred_line_length as f32;
- }
+ ) && (settings.defaults.preferred_line_length as f32) < visible_column_count
+ {
+ visible_column_count = settings.defaults.preferred_line_length as f32;
}
// If the scroll position is currently at the left edge of the document
// (x == 0.0) and the intent is to scroll right, the gutter's margin
// should first be added to the current position, otherwise the cursor
// will end at the column position minus the margin, which looks off.
- if current_position.x == 0.0 && amount.columns(visible_column_count) > 0. {
- if let Some(last_position_map) = &self.last_position_map {
- current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance;
- }
+ if current_position.x == 0.0
+ && amount.columns(visible_column_count) > 0.
+ && let Some(last_position_map) = &self.last_position_map
+ {
+ current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance;
}
let new_position = current_position
+ point(
@@ -749,12 +749,10 @@ impl Editor {
if let (Some(visible_lines), Some(visible_columns)) =
(self.visible_line_count(), self.visible_column_count())
+ && newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32)
+ && newest_head.column() <= screen_top.column() + visible_columns as u32
{
- if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32)
- && newest_head.column() <= screen_top.column() + visible_columns as u32
- {
- return Ordering::Equal;
- }
+ return Ordering::Equal;
}
Ordering::Greater
@@ -16,7 +16,7 @@ impl Editor {
return;
}
- if matches!(self.mode, EditorMode::SingleLine { .. }) {
+ if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate();
return;
}
@@ -116,12 +116,12 @@ impl Editor {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let original_y = scroll_position.y;
- if let Some(last_bounds) = self.expect_bounds_change.take() {
- if scroll_position.y != 0. {
- scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
- if scroll_position.y < 0. {
- scroll_position.y = 0.;
- }
+ if let Some(last_bounds) = self.expect_bounds_change.take()
+ && scroll_position.y != 0.
+ {
+ scroll_position.y += (bounds.top() - last_bounds.top()) / line_height;
+ if scroll_position.y < 0. {
+ scroll_position.y = 0.;
}
}
if scroll_position.y > max_scroll_top {
@@ -15,7 +15,7 @@ impl ScrollDirection {
}
}
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
pub enum ScrollAmount {
// Scroll N lines (positive is towards the end of the document)
Line(f32),
@@ -67,10 +67,7 @@ impl ScrollAmount {
}
pub fn is_full_page(&self) -> bool {
- match self {
- ScrollAmount::Page(count) if count.abs() == 1.0 => true,
- _ => false,
- }
+ matches!(self, ScrollAmount::Page(count) if count.abs() == 1.0)
}
pub fn direction(&self) -> ScrollDirection {
@@ -119,8 +119,8 @@ impl SelectionsCollection {
cx: &mut App,
) -> Option<Selection<D>> {
let map = self.display_map(cx);
- let selection = resolve_selections(self.pending_anchor().as_ref(), &map).next();
- selection
+
+ resolve_selections(self.pending_anchor().as_ref(), &map).next()
}
pub(crate) fn pending_mode(&self) -> Option<SelectMode> {
@@ -276,18 +276,18 @@ impl SelectionsCollection {
cx: &mut App,
) -> Selection<D> {
let map = self.display_map(cx);
- let selection = resolve_selections([self.newest_anchor()], &map)
+
+ resolve_selections([self.newest_anchor()], &map)
.next()
- .unwrap();
- selection
+ .unwrap()
}
pub fn newest_display(&self, cx: &mut App) -> Selection<DisplayPoint> {
let map = self.display_map(cx);
- let selection = resolve_selections_display([self.newest_anchor()], &map)
+
+ resolve_selections_display([self.newest_anchor()], &map)
.next()
- .unwrap();
- selection
+ .unwrap()
}
pub fn oldest_anchor(&self) -> &Selection<Anchor> {
@@ -303,10 +303,10 @@ impl SelectionsCollection {
cx: &mut App,
) -> Selection<D> {
let map = self.display_map(cx);
- let selection = resolve_selections([self.oldest_anchor()], &map)
+
+ resolve_selections([self.oldest_anchor()], &map)
.next()
- .unwrap();
- selection
+ .unwrap()
}
pub fn first_anchor(&self) -> Selection<Anchor> {
@@ -169,7 +169,7 @@ impl Editor {
else {
return;
};
- let Some(lsp_store) = self.project.as_ref().map(|p| p.read(cx).lsp_store()) else {
+ let Some(lsp_store) = self.project().map(|p| p.read(cx).lsp_store()) else {
return;
};
let task = lsp_store.update(cx, |lsp_store, cx| {
@@ -182,7 +182,9 @@ impl Editor {
let signature_help = task.await;
editor
.update(cx, |editor, cx| {
- let Some(mut signature_help) = signature_help.into_iter().next() else {
+ let Some(mut signature_help) =
+ signature_help.unwrap_or_default().into_iter().next()
+ else {
editor
.signature_help_state
.hide(SignatureHelpHiddenBy::AutoClose);
@@ -196,7 +198,7 @@ impl Editor {
.highlight_text(&text, 0..signature.label.len())
.into_iter()
.flat_map(|(range, highlight_id)| {
- Some((range, highlight_id.style(&cx.theme().syntax())?))
+ Some((range, highlight_id.style(cx.theme().syntax())?))
});
signature.highlights =
combine_highlights(signature.highlights.clone(), highlights)
@@ -89,7 +89,7 @@ impl Editor {
.lsp_task_source()?;
if lsp_settings
.get(&lsp_tasks_source)
- .map_or(true, |s| s.enable_lsp_tasks)
+ .is_none_or(|s| s.enable_lsp_tasks)
{
let buffer_id = buffer.read(cx).remote_id();
Some((lsp_tasks_source, buffer_id))
@@ -20,7 +20,7 @@ use multi_buffer::ToPoint;
use pretty_assertions::assert_eq;
use project::{Project, project_settings::DiagnosticSeverity};
use ui::{App, BorrowAppContext, px};
-use util::test::{marked_text_offsets, marked_text_ranges};
+use util::test::{generate_marked_text, marked_text_offsets, marked_text_ranges};
#[cfg(test)]
#[ctor::ctor]
@@ -104,13 +104,14 @@ pub fn assert_text_with_selections(
marked_text: &str,
cx: &mut Context<Editor>,
) {
- let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
+ let (unmarked_text, _text_ranges) = marked_text_ranges(marked_text, true);
assert_eq!(editor.text(cx), unmarked_text, "text doesn't match");
- assert_eq!(
- editor.selections.ranges(cx),
- text_ranges,
- "selections don't match",
+ let actual = generate_marked_text(
+ &editor.text(cx),
+ &editor.selections.ranges(cx),
+ marked_text.contains("«"),
);
+ assert_eq!(actual, marked_text, "Selections don't match");
}
// RA thinks this is dead code even though it is used in a whole lot of tests
@@ -184,12 +185,12 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
for (row, block) in blocks {
match block {
Block::Custom(custom_block) => {
- if let BlockPlacement::Near(x) = &custom_block.placement {
- if snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) {
- continue;
- }
+ if let BlockPlacement::Near(x) = &custom_block.placement
+ && snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot))
+ {
+ continue;
};
- let content = block_content_for_tests(&editor, custom_block.id, cx)
+ let content = block_content_for_tests(editor, custom_block.id, cx)
.expect("block content not found");
// 2: "related info 1 for diagnostic 0"
if let Some(height) = custom_block.height {
@@ -230,26 +231,23 @@ pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestCo
lines[row as usize].push_str("§ -----");
}
}
- Block::ExcerptBoundary {
- excerpt,
- height,
- starts_new_buffer,
- } => {
- if starts_new_buffer {
- lines[row.0 as usize].push_str(&cx.update(|_, cx| {
- format!(
- "§ {}",
- excerpt
- .buffer
- .file()
- .unwrap()
- .file_name(cx)
- .to_string_lossy()
- )
- }));
- } else {
- lines[row.0 as usize].push_str("§ -----")
+ Block::ExcerptBoundary { height, .. } => {
+ for row in row.0..row.0 + height {
+ lines[row as usize].push_str("§ -----");
}
+ }
+ Block::BufferHeader { excerpt, height } => {
+ lines[row.0 as usize].push_str(&cx.update(|_, cx| {
+ format!(
+ "§ {}",
+ excerpt
+ .buffer
+ .file()
+ .unwrap()
+ .file_name(cx)
+ .to_string_lossy()
+ )
+ }));
for row in row.0 + 1..row.0 + height {
lines[row as usize].push_str("§ -----");
}
@@ -29,7 +29,7 @@ pub struct EditorLspTestContext {
pub cx: EditorTestContext,
pub lsp: lsp::FakeLanguageServer,
pub workspace: Entity<Workspace>,
- pub buffer_lsp_url: lsp::Url,
+ pub buffer_lsp_url: lsp::Uri,
}
pub(crate) fn rust_lang() -> Arc<Language> {
@@ -189,7 +189,7 @@ impl EditorLspTestContext {
},
lsp,
workspace,
- buffer_lsp_url: lsp::Url::from_file_path(root.join("dir").join(file_name)).unwrap(),
+ buffer_lsp_url: lsp::Uri::from_file_path(root.join("dir").join(file_name)).unwrap(),
}
}
@@ -300,6 +300,7 @@ impl EditorLspTestContext {
self.to_lsp_range(ranges[0].clone())
}
+ #[expect(clippy::wrong_self_convention, reason = "This is test code")]
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
@@ -326,6 +327,7 @@ impl EditorLspTestContext {
})
}
+ #[expect(clippy::wrong_self_convention, reason = "This is test code")]
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
let snapshot = self.update_editor(|editor, window, cx| editor.snapshot(window, cx));
let point = offset.to_point(&snapshot.buffer_snapshot);
@@ -356,7 +358,7 @@ impl EditorLspTestContext {
where
T: 'static + request::Request,
T::Params: 'static + Send,
- F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncApp) -> Fut,
+ F: 'static + Send + FnMut(lsp::Uri, T::Params, gpui::AsyncApp) -> Fut,
Fut: 'static + Future<Output = Result<T::Result>>,
{
let url = self.buffer_lsp_url.clone();
@@ -119,13 +119,7 @@ impl EditorTestContext {
for excerpt in excerpts.into_iter() {
let (text, ranges) = marked_text_ranges(excerpt, false);
let buffer = cx.new(|cx| Buffer::local(text, cx));
- multibuffer.push_excerpts(
- buffer,
- ranges
- .into_iter()
- .map(|range| ExcerptRange::new(range.clone())),
- cx,
- );
+ multibuffer.push_excerpts(buffer, ranges.into_iter().map(ExcerptRange::new), cx);
}
multibuffer
});
@@ -297,9 +291,8 @@ impl EditorTestContext {
pub fn set_head_text(&mut self, diff_base: &str) {
self.cx.run_until_parked();
- let fs = self.update_editor(|editor, _, cx| {
- editor.project.as_ref().unwrap().read(cx).fs().as_fake()
- });
+ let fs =
+ self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
fs.set_head_for_repo(
&Self::root_path().join(".git"),
@@ -311,18 +304,16 @@ impl EditorTestContext {
pub fn clear_index_text(&mut self) {
self.cx.run_until_parked();
- let fs = self.update_editor(|editor, _, cx| {
- editor.project.as_ref().unwrap().read(cx).fs().as_fake()
- });
+ let fs =
+ self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
fs.set_index_for_repo(&Self::root_path().join(".git"), &[]);
self.cx.run_until_parked();
}
pub fn set_index_text(&mut self, diff_base: &str) {
self.cx.run_until_parked();
- let fs = self.update_editor(|editor, _, cx| {
- editor.project.as_ref().unwrap().read(cx).fs().as_fake()
- });
+ let fs =
+ self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
fs.set_index_for_repo(
&Self::root_path().join(".git"),
@@ -333,9 +324,8 @@ impl EditorTestContext {
#[track_caller]
pub fn assert_index_text(&mut self, expected: Option<&str>) {
- let fs = self.update_editor(|editor, _, cx| {
- editor.project.as_ref().unwrap().read(cx).fs().as_fake()
- });
+ let fs =
+ self.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).fs().as_fake());
let path = self.update_buffer(|buffer, _| buffer.file().unwrap().path().clone());
let mut found = None;
fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| {
@@ -430,7 +420,7 @@ impl EditorTestContext {
if expected_text == "[FOLDED]\n" {
assert!(is_folded, "excerpt {} should be folded", ix);
let is_selected = selections.iter().any(|s| s.head().excerpt_id == excerpt_id);
- if expected_selections.len() > 0 {
+ if !expected_selections.is_empty() {
assert!(
is_selected,
"excerpt {ix} should be selected. got {:?}",
@@ -54,7 +54,7 @@ impl AssertionsReport {
pub fn passed_count(&self) -> usize {
self.ran
.iter()
- .filter(|a| a.result.as_ref().map_or(false, |result| result.passed))
+ .filter(|a| a.result.as_ref().is_ok_and(|result| result.passed))
.count()
}
@@ -103,7 +103,7 @@ fn main() {
let languages: HashSet<String> = args.languages.into_iter().collect();
let http_client = Arc::new(ReqwestClient::new());
- let app = Application::headless().with_http_client(http_client.clone());
+ let app = Application::headless().with_http_client(http_client);
let all_threads = examples::all(&examples_dir);
app.run(move |cx| {
@@ -112,7 +112,7 @@ fn main() {
let telemetry = app_state.client.telemetry();
telemetry.start(system_id, installation_id, session_id, cx);
- let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").map_or(false, |value| value == "1")
+ let enable_telemetry = env::var("ZED_EVAL_TELEMETRY").is_ok_and(|value| value == "1")
&& telemetry.has_checksum_seed();
if enable_telemetry {
println!("Telemetry enabled");
@@ -167,15 +167,14 @@ fn main() {
continue;
}
- if let Some(language) = meta.language_server {
- if !languages.contains(&language.file_extension) {
+ if let Some(language) = meta.language_server
+ && !languages.contains(&language.file_extension) {
panic!(
"Eval for {:?} could not be run because no language server was found for extension {:?}",
meta.name,
language.file_extension
);
}
- }
// TODO: This creates a worktree per repetition. Ideally these examples should
// either be run sequentially on the same worktree, or reuse worktrees when there
@@ -417,11 +416,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
language::init(cx);
debug_adapter_extension::init(extension_host_proxy.clone(), cx);
- language_extension::init(
- LspAccess::Noop,
- extension_host_proxy.clone(),
- languages.clone(),
- );
+ language_extension::init(LspAccess::Noop, extension_host_proxy, languages.clone());
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), cx);
languages::init(languages.clone(), node_runtime.clone(), cx);
@@ -520,7 +515,7 @@ async fn judge_example(
enable_telemetry: bool,
cx: &AsyncApp,
) -> JudgeOutput {
- let judge_output = example.judge(model.clone(), &run_output, cx).await;
+ let judge_output = example.judge(model.clone(), run_output, cx).await;
if enable_telemetry {
telemetry::event!(
@@ -531,7 +526,7 @@ async fn judge_example(
example_name = example.name.clone(),
example_repetition = example.repetition,
diff_evaluation = judge_output.diff.clone(),
- thread_evaluation = judge_output.thread.clone(),
+ thread_evaluation = judge_output.thread,
tool_metrics = run_output.tool_metrics,
response_count = run_output.response_count,
token_usage = run_output.token_usage,
@@ -711,7 +706,7 @@ fn print_report(
println!("Average thread score: {average_thread_score}%");
}
- println!("");
+ println!();
print_h2("CUMULATIVE TOOL METRICS");
println!("{}", cumulative_tool_metrics);
@@ -64,7 +64,7 @@ impl ExampleMetadata {
self.url
.split('/')
.next_back()
- .unwrap_or(&"")
+ .unwrap_or("")
.trim_end_matches(".git")
.into()
}
@@ -255,7 +255,7 @@ impl ExampleContext {
thread.update(cx, |thread, _cx| {
if let Some(tool_use) = pending_tool_use {
let mut tool_metrics = tool_metrics.lock().unwrap();
- if let Some(tool_result) = thread.tool_result(&tool_use_id) {
+ if let Some(tool_result) = thread.tool_result(tool_use_id) {
let message = if tool_result.is_error {
format!("✖︎ {}", tool_use.name)
} else {
@@ -335,7 +335,7 @@ impl ExampleContext {
for message in thread.messages().skip(message_count_before) {
messages.push(Message {
_role: message.role,
- text: message.to_string(),
+ text: message.to_message_content(),
tool_use: thread
.tool_uses_for_message(message.id, cx)
.into_iter()
@@ -70,10 +70,10 @@ impl Example for AddArgToTraitMethod {
let path_str = format!("crates/assistant_tools/src/{}.rs", tool_name);
let edits = edits.get(Path::new(&path_str));
- let ignored = edits.map_or(false, |edits| {
+ let ignored = edits.is_some_and(|edits| {
edits.has_added_line(" _window: Option<gpui::AnyWindowHandle>,\n")
});
- let uningored = edits.map_or(false, |edits| {
+ let uningored = edits.is_some_and(|edits| {
edits.has_added_line(" window: Option<gpui::AnyWindowHandle>,\n")
});
@@ -89,7 +89,7 @@ impl Example for AddArgToTraitMethod {
let batch_tool_edits = edits.get(Path::new("crates/assistant_tools/src/batch_tool.rs"));
cx.assert(
- batch_tool_edits.map_or(false, |edits| {
+ batch_tool_edits.is_some_and(|edits| {
edits.has_added_line(" window: Option<gpui::AnyWindowHandle>,\n")
}),
"Argument: batch_tool",
@@ -46,27 +46,25 @@ fn find_target_files_recursive(
max_depth,
found_files,
)?;
- } else if path.is_file() {
- if let Some(filename_osstr) = path.file_name() {
- if let Some(filename_str) = filename_osstr.to_str() {
- if filename_str == target_filename {
- found_files.push(path);
- }
- }
- }
+ } else if path.is_file()
+ && let Some(filename_osstr) = path.file_name()
+ && let Some(filename_str) = filename_osstr.to_str()
+ && filename_str == target_filename
+ {
+ found_files.push(path);
}
}
Ok(())
}
pub fn generate_explorer_html(input_paths: &[PathBuf], output_path: &PathBuf) -> Result<String> {
- if let Some(parent) = output_path.parent() {
- if !parent.exists() {
- fs::create_dir_all(parent).context(format!(
- "Failed to create output directory: {}",
- parent.display()
- ))?;
- }
+ if let Some(parent) = output_path.parent()
+ && !parent.exists()
+ {
+ fs::create_dir_all(parent).context(format!(
+ "Failed to create output directory: {}",
+ parent.display()
+ ))?;
}
let template_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/explorer.html");
@@ -90,11 +90,8 @@ impl ExampleInstance {
worktrees_dir: &Path,
repetition: usize,
) -> Self {
- let name = thread.meta().name.to_string();
- let run_directory = run_dir
- .join(&name)
- .join(repetition.to_string())
- .to_path_buf();
+ let name = thread.meta().name;
+ let run_directory = run_dir.join(&name).join(repetition.to_string());
let repo_path = repo_path_for_url(repos_dir, &thread.meta().url);
@@ -376,11 +373,10 @@ impl ExampleInstance {
);
let result = this.thread.conversation(&mut example_cx).await;
- if let Err(err) = result {
- if !err.is::<FailedAssertion>() {
+ if let Err(err) = result
+ && !err.is::<FailedAssertion>() {
return Err(err);
}
- }
println!("{}Stopped", this.log_prefix);
@@ -459,8 +455,8 @@ impl ExampleInstance {
let mut output_file =
File::create(self.run_directory.join("judge.md")).expect("failed to create judge.md");
- let diff_task = self.judge_diff(model.clone(), &run_output, cx);
- let thread_task = self.judge_thread(model.clone(), &run_output, cx);
+ let diff_task = self.judge_diff(model.clone(), run_output, cx);
+ let thread_task = self.judge_thread(model.clone(), run_output, cx);
let (diff_result, thread_result) = futures::join!(diff_task, thread_task);
@@ -661,7 +657,7 @@ pub fn wait_for_lang_server(
.update(cx, |buffer, cx| {
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
- .language_servers_for_local_buffer(&buffer, cx)
+ .language_servers_for_local_buffer(buffer, cx)
.next()
.is_some()
})
@@ -679,8 +675,8 @@ pub fn wait_for_lang_server(
[
cx.subscribe(&lsp_store, {
let log_prefix = log_prefix.clone();
- move |_, event, _| match event {
- project::LspStoreEvent::LanguageServerUpdate {
+ move |_, event, _| {
+ if let project::LspStoreEvent::LanguageServerUpdate {
message:
client::proto::update_language_server::Variant::WorkProgress(
LspWorkProgress {
@@ -689,11 +685,13 @@ pub fn wait_for_lang_server(
},
),
..
- } => println!("{}⟲ {message}", log_prefix),
- _ => {}
+ } = event
+ {
+ println!("{}⟲ {message}", log_prefix)
+ }
}
}),
- cx.subscribe(&project, {
+ cx.subscribe(project, {
let buffer = buffer.clone();
move |project, event, cx| match event {
project::Event::LanguageServerAdded(_, _, _) => {
@@ -771,7 +769,7 @@ pub async fn query_lsp_diagnostics(
}
fn parse_assertion_result(response: &str) -> Result<RanAssertionResult> {
- let analysis = get_tag("analysis", response)?.to_string();
+ let analysis = get_tag("analysis", response)?;
let passed = match get_tag("passed", response)?.to_lowercase().as_str() {
"true" => true,
"false" => false,
@@ -838,7 +836,7 @@ fn messages_to_markdown<'a>(message_iter: impl IntoIterator<Item = &'a Message>)
for segment in &message.segments {
match segment {
MessageSegment::Text(text) => {
- messages.push_str(&text);
+ messages.push_str(text);
messages.push_str("\n\n");
}
MessageSegment::Thinking { text, signature } => {
@@ -846,7 +844,7 @@ fn messages_to_markdown<'a>(message_iter: impl IntoIterator<Item = &'a Message>)
if let Some(sig) = signature {
messages.push_str(&format!("Signature: {}\n\n", sig));
}
- messages.push_str(&text);
+ messages.push_str(text);
messages.push_str("\n");
}
MessageSegment::RedactedThinking(items) => {
@@ -878,7 +876,7 @@ pub async fn send_language_model_request(
request: LanguageModelRequest,
cx: &AsyncApp,
) -> anyhow::Result<String> {
- match model.stream_completion_text(request, &cx).await {
+ match model.stream_completion_text(request, cx).await {
Ok(mut stream) => {
let mut full_response = String::new();
while let Some(chunk_result) = stream.stream.next().await {
@@ -915,9 +913,9 @@ impl RequestMarkdown {
for tool in &request.tools {
write!(&mut tools, "# {}\n\n", tool.name).unwrap();
write!(&mut tools, "{}\n\n", tool.description).unwrap();
- write!(
+ writeln!(
&mut tools,
- "{}\n",
+ "{}",
MarkdownCodeBlock {
tag: "json",
text: &format!("{:#}", tool.input_schema)
@@ -1191,7 +1189,7 @@ mod test {
output.analysis,
Some("The model did a good job but there were still compilations errors.".into())
);
- assert_eq!(output.passed, true);
+ assert!(output.passed);
let response = r#"
Text around ignored
@@ -1211,6 +1209,6 @@ mod test {
output.analysis,
Some("Failed to compile:\n- Error 1\n- Error 2".into())
);
- assert_eq!(output.passed, false);
+ assert!(!output.passed);
}
}
@@ -178,16 +178,15 @@ pub fn parse_wasm_extension_version(
for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
if let wasmparser::Payload::CustomSection(s) =
part.context("error parsing wasm extension")?
+ && s.name() == "zed:api-version"
{
- if s.name() == "zed:api-version" {
- version = parse_wasm_extension_version_custom_section(s.data());
- if version.is_none() {
- bail!(
- "extension {} has invalid zed:api-version section: {:?}",
- extension_id,
- s.data()
- );
- }
+ version = parse_wasm_extension_version_custom_section(s.data());
+ if version.is_none() {
+ bail!(
+ "extension {} has invalid zed:api-version section: {:?}",
+ extension_id,
+ s.data()
+ );
}
}
}
@@ -401,7 +401,7 @@ impl ExtensionBuilder {
let mut clang_path = wasi_sdk_dir.clone();
clang_path.extend(["bin", &format!("clang{}", env::consts::EXE_SUFFIX)]);
- if fs::metadata(&clang_path).map_or(false, |metadata| metadata.is_file()) {
+ if fs::metadata(&clang_path).is_ok_and(|metadata| metadata.is_file()) {
return Ok(clang_path);
}
@@ -452,7 +452,7 @@ impl ExtensionBuilder {
let mut output = Vec::new();
let mut stack = Vec::new();
- for payload in Parser::new(0).parse_all(&input) {
+ for payload in Parser::new(0).parse_all(input) {
let payload = payload?;
// Track nesting depth, so that we don't mess with inner producer sections:
@@ -484,14 +484,10 @@ impl ExtensionBuilder {
_ => {}
}
- match &payload {
- CustomSection(c) => {
- if strip_custom_section(c.name()) {
- continue;
- }
- }
-
- _ => {}
+ if let CustomSection(c) = &payload
+ && strip_custom_section(c.name())
+ {
+ continue;
}
if let Some((id, range)) = payload.as_section() {
RawSection {
@@ -19,9 +19,8 @@ pub struct ExtensionEvents;
impl ExtensionEvents {
/// Returns the global [`ExtensionEvents`].
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
- return cx
- .try_global::<GlobalExtensionEvents>()
- .map(|g| g.0.clone());
+ cx.try_global::<GlobalExtensionEvents>()
+ .map(|g| g.0.clone())
}
fn new(_cx: &mut Context<Self>) -> Self {
@@ -28,7 +28,6 @@ pub struct ExtensionHostProxy {
snippet_proxy: RwLock<Option<Arc<dyn ExtensionSnippetProxy>>>,
slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
- indexed_docs_provider_proxy: RwLock<Option<Arc<dyn ExtensionIndexedDocsProviderProxy>>>,
debug_adapter_provider_proxy: RwLock<Option<Arc<dyn ExtensionDebugAdapterProviderProxy>>>,
}
@@ -54,7 +53,6 @@ impl ExtensionHostProxy {
snippet_proxy: RwLock::default(),
slash_command_proxy: RwLock::default(),
context_server_proxy: RwLock::default(),
- indexed_docs_provider_proxy: RwLock::default(),
debug_adapter_provider_proxy: RwLock::default(),
}
}
@@ -87,14 +85,6 @@ impl ExtensionHostProxy {
self.context_server_proxy.write().replace(Arc::new(proxy));
}
- pub fn register_indexed_docs_provider_proxy(
- &self,
- proxy: impl ExtensionIndexedDocsProviderProxy,
- ) {
- self.indexed_docs_provider_proxy
- .write()
- .replace(Arc::new(proxy));
- }
pub fn register_debug_adapter_proxy(&self, proxy: impl ExtensionDebugAdapterProviderProxy) {
self.debug_adapter_provider_proxy
.write()
@@ -408,30 +398,6 @@ impl ExtensionContextServerProxy for ExtensionHostProxy {
}
}
-pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static {
- fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>);
-
- fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>);
-}
-
-impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
- fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>) {
- let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else {
- return;
- };
-
- proxy.register_indexed_docs_provider(extension, provider_id)
- }
-
- fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>) {
- let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else {
- return;
- };
-
- proxy.unregister_indexed_docs_provider(provider_id)
- }
-}
-
pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static {
fn register_debug_adapter(
&self,
@@ -84,8 +84,6 @@ pub struct ExtensionManifest {
#[serde(default)]
pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
#[serde(default)]
- pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
- #[serde(default)]
pub snippets: Option<PathBuf>,
#[serde(default)]
pub capabilities: Vec<ExtensionCapability>,
@@ -195,9 +193,6 @@ pub struct SlashCommandManifestEntry {
pub requires_argument: bool,
}
-#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
-pub struct IndexedDocsProviderEntry {}
-
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct DebugAdapterManifestEntry {
pub schema_path: Option<PathBuf>,
@@ -271,7 +266,6 @@ fn manifest_from_old_manifest(
language_servers: Default::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
- indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
debug_adapters: Default::default(),
@@ -304,7 +298,6 @@ mod tests {
language_servers: BTreeMap::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
- indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: vec![],
debug_adapters: Default::default(),
@@ -232,10 +232,10 @@ pub trait Extension: Send + Sync {
///
/// To work through a real-world example, take a `cargo run` task and a hypothetical `cargo` locator:
/// 1. We may need to modify the task; in this case, it is problematic that `cargo run` spawns a binary. We should turn `cargo run` into a debug scenario with
- /// `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope.
+ /// `cargo build` task. This is the decision we make at `dap_locator_create_scenario` scope.
/// 2. Then, after the build task finishes, we will run `run_dap_locator` of the locator that produced the build task to find the program to be debugged. This function
- /// should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user
- /// found the artifact path by themselves.
+ /// should give us a debugger-agnostic configuration for launching a debug target (that we end up resolving with [`Extension::dap_config_to_scenario`]). It's almost as if the user
+ /// found the artifact path by themselves.
///
/// Note that you're not obliged to use build tasks with locators. Specifically, it is sufficient to provide a debug configuration directly in the return value of
/// `dap_locator_create_scenario` if you're able to do that. Make sure to not fill out `build` field in that case, as that will prevent Zed from running second phase of resolution in such case.
@@ -144,10 +144,6 @@ fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet<ExtensionProvide
provides.insert(ExtensionProvides::ContextServers);
}
- if !manifest.indexed_docs_providers.is_empty() {
- provides.insert(ExtensionProvides::IndexedDocsProviders);
- }
-
if manifest.snippets.is_some() {
provides.insert(ExtensionProvides::Snippets);
}
@@ -132,7 +132,6 @@ fn manifest() -> ExtensionManifest {
.collect(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
- indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: vec![ExtensionCapability::ProcessExec(
extension::ProcessExecCapability {
@@ -108,7 +108,6 @@ mod tests {
language_servers: BTreeMap::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
- indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: vec![],
debug_adapters: Default::default(),
@@ -146,7 +145,7 @@ mod tests {
command: "*".to_string(),
args: vec!["**".to_string()],
})],
- manifest.clone(),
+ manifest,
);
assert!(granter.grant_exec("ls", &["-la"]).is_ok());
}
@@ -16,9 +16,9 @@ pub use extension::ExtensionManifest;
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
use extension::{
ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents,
- ExtensionGrammarProxy, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy,
- ExtensionLanguageProxy, ExtensionLanguageServerProxy, ExtensionSlashCommandProxy,
- ExtensionSnippetProxy, ExtensionThemeProxy,
+ ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy,
+ ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy,
+ ExtensionThemeProxy,
};
use fs::{Fs, RemoveOptions};
use futures::future::join_all;
@@ -43,7 +43,7 @@ use language::{
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use release_channel::ReleaseChannel;
-use remote::SshRemoteClient;
+use remote::{RemoteClient, RemoteConnectionOptions};
use semantic_version::SemanticVersion;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -93,10 +93,9 @@ pub fn is_version_compatible(
.wasm_api_version
.as_ref()
.and_then(|wasm_api_version| SemanticVersion::from_str(wasm_api_version).ok())
+ && !is_supported_wasm_api_version(release_channel, wasm_api_version)
{
- if !is_supported_wasm_api_version(release_channel, wasm_api_version) {
- return false;
- }
+ return false;
}
true
@@ -118,7 +117,7 @@ pub struct ExtensionStore {
pub wasm_host: Arc<WasmHost>,
pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
pub tasks: Vec<Task<()>>,
- pub ssh_clients: HashMap<String, WeakEntity<SshRemoteClient>>,
+ pub remote_clients: HashMap<RemoteConnectionOptions, WeakEntity<RemoteClient>>,
pub ssh_registered_tx: UnboundedSender<()>,
}
@@ -271,7 +270,7 @@ impl ExtensionStore {
reload_tx,
tasks: Vec::new(),
- ssh_clients: HashMap::default(),
+ remote_clients: HashMap::default(),
ssh_registered_tx: connection_registered_tx,
};
@@ -292,19 +291,17 @@ impl ExtensionStore {
// it must be asynchronously rebuilt.
let mut extension_index = ExtensionIndex::default();
let mut extension_index_needs_rebuild = true;
- if let Ok(index_content) = index_content {
- if let Some(index) = serde_json::from_str(&index_content).log_err() {
- extension_index = index;
- if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) =
- (index_metadata, extensions_metadata)
- {
- if index_metadata
- .mtime
- .bad_is_greater_than(extensions_metadata.mtime)
- {
- extension_index_needs_rebuild = false;
- }
- }
+ if let Ok(index_content) = index_content
+ && let Some(index) = serde_json::from_str(&index_content).log_err()
+ {
+ extension_index = index;
+ if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) =
+ (index_metadata, extensions_metadata)
+ && index_metadata
+ .mtime
+ .bad_is_greater_than(extensions_metadata.mtime)
+ {
+ extension_index_needs_rebuild = false;
}
}
@@ -392,10 +389,9 @@ impl ExtensionStore {
if let Some(path::Component::Normal(extension_dir_name)) =
event_path.components().next()
+ && let Some(extension_id) = extension_dir_name.to_str()
{
- if let Some(extension_id) = extension_dir_name.to_str() {
- reload_tx.unbounded_send(Some(extension_id.into())).ok();
- }
+ reload_tx.unbounded_send(Some(extension_id.into())).ok();
}
}
}
@@ -566,12 +562,12 @@ impl ExtensionStore {
extensions
.into_iter()
.filter(|extension| {
- this.extension_index.extensions.get(&extension.id).map_or(
- true,
- |installed_extension| {
+ this.extension_index
+ .extensions
+ .get(&extension.id)
+ .is_none_or(|installed_extension| {
installed_extension.manifest.version != extension.manifest.version
- },
- )
+ })
})
.collect()
})
@@ -763,8 +759,8 @@ impl ExtensionStore {
if let ExtensionOperation::Install = operation {
this.update( cx, |this, cx| {
cx.emit(Event::ExtensionInstalled(extension_id.clone()));
- if let Some(events) = ExtensionEvents::try_global(cx) {
- if let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
+ if let Some(events) = ExtensionEvents::try_global(cx)
+ && let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
events.update(cx, |this, cx| {
this.emit(
extension::Event::ExtensionInstalled(manifest.clone()),
@@ -772,7 +768,6 @@ impl ExtensionStore {
)
});
}
- }
})
.ok();
}
@@ -912,12 +907,12 @@ impl ExtensionStore {
extension_store.update(cx, |_, cx| {
cx.emit(Event::ExtensionUninstalled(extension_id.clone()));
- if let Some(events) = ExtensionEvents::try_global(cx) {
- if let Some(manifest) = extension_manifest {
- events.update(cx, |this, cx| {
- this.emit(extension::Event::ExtensionUninstalled(manifest.clone()), cx)
- });
- }
+ if let Some(events) = ExtensionEvents::try_global(cx)
+ && let Some(manifest) = extension_manifest
+ {
+ events.update(cx, |this, cx| {
+ this.emit(extension::Event::ExtensionUninstalled(manifest.clone()), cx)
+ });
}
})?;
@@ -997,12 +992,12 @@ impl ExtensionStore {
this.update(cx, |this, cx| this.reload(None, cx))?.await;
this.update(cx, |this, cx| {
cx.emit(Event::ExtensionInstalled(extension_id.clone()));
- if let Some(events) = ExtensionEvents::try_global(cx) {
- if let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
- events.update(cx, |this, cx| {
- this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx)
- });
- }
+ if let Some(events) = ExtensionEvents::try_global(cx)
+ && let Some(manifest) = this.extension_manifest_for_id(&extension_id)
+ {
+ events.update(cx, |this, cx| {
+ this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx)
+ });
}
})?;
@@ -1180,22 +1175,18 @@ impl ExtensionStore {
}
}
- for (server_id, _) in &extension.manifest.context_servers {
+ for server_id in extension.manifest.context_servers.keys() {
self.proxy.unregister_context_server(server_id.clone(), cx);
}
- for (adapter, _) in &extension.manifest.debug_adapters {
+ for adapter in extension.manifest.debug_adapters.keys() {
self.proxy.unregister_debug_adapter(adapter.clone());
}
- for (locator, _) in &extension.manifest.debug_locators {
+ for locator in extension.manifest.debug_locators.keys() {
self.proxy.unregister_debug_locator(locator.clone());
}
- for (command_name, _) in &extension.manifest.slash_commands {
+ for command_name in extension.manifest.slash_commands.keys() {
self.proxy.unregister_slash_command(command_name.clone());
}
- for (provider_id, _) in &extension.manifest.indexed_docs_providers {
- self.proxy
- .unregister_indexed_docs_provider(provider_id.clone());
- }
}
self.wasm_extensions
@@ -1279,6 +1270,7 @@ impl ExtensionStore {
queries,
context_provider,
toolchain_provider: None,
+ manifest_name: None,
})
}),
);
@@ -1344,7 +1336,7 @@ impl ExtensionStore {
&extension_path,
&extension.manifest,
wasm_host.clone(),
- &cx,
+ cx,
)
.await
.with_context(|| format!("Loading extension from {extension_path:?}"));
@@ -1394,16 +1386,11 @@ impl ExtensionStore {
);
}
- for (id, _context_server_entry) in &manifest.context_servers {
+ for id in manifest.context_servers.keys() {
this.proxy
.register_context_server(extension.clone(), id.clone(), cx);
}
- for (provider_id, _provider) in &manifest.indexed_docs_providers {
- this.proxy
- .register_indexed_docs_provider(extension.clone(), provider_id.clone());
- }
-
for (debug_adapter, meta) in &manifest.debug_adapters {
let mut path = root_dir.clone();
path.push(Path::new(manifest.id.as_ref()));
@@ -1464,7 +1451,7 @@ impl ExtensionStore {
if extension_dir
.file_name()
- .map_or(false, |file_name| file_name == ".DS_Store")
+ .is_some_and(|file_name| file_name == ".DS_Store")
{
continue;
}
@@ -1688,9 +1675,8 @@ impl ExtensionStore {
let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta);
if fs.is_file(&src_dir.join(schema_path)).await {
- match schema_path.parent() {
- Some(parent) => fs.create_dir(&tmp_dir.join(parent)).await?,
- None => {}
+ if let Some(parent) = schema_path.parent() {
+ fs.create_dir(&tmp_dir.join(parent)).await?
}
fs.copy_file(
&src_dir.join(schema_path),
@@ -1707,7 +1693,7 @@ impl ExtensionStore {
async fn sync_extensions_over_ssh(
this: &WeakEntity<Self>,
- client: WeakEntity<SshRemoteClient>,
+ client: WeakEntity<RemoteClient>,
cx: &mut AsyncApp,
) -> Result<()> {
let extensions = this.update(cx, |this, _cx| {
@@ -1779,12 +1765,12 @@ impl ExtensionStore {
pub async fn update_ssh_clients(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
let clients = this.update(cx, |this, _cx| {
- this.ssh_clients.retain(|_k, v| v.upgrade().is_some());
- this.ssh_clients.values().cloned().collect::<Vec<_>>()
+ this.remote_clients.retain(|_k, v| v.upgrade().is_some());
+ this.remote_clients.values().cloned().collect::<Vec<_>>()
})?;
for client in clients {
- Self::sync_extensions_over_ssh(&this, client, cx)
+ Self::sync_extensions_over_ssh(this, client, cx)
.await
.log_err();
}
@@ -1792,17 +1778,16 @@ impl ExtensionStore {
anyhow::Ok(())
}
- pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) {
- let connection_options = client.read(cx).connection_options();
- let ssh_url = connection_options.ssh_url();
+ pub fn register_remote_client(&mut self, client: Entity<RemoteClient>, cx: &mut Context<Self>) {
+ let options = client.read(cx).connection_options();
- if let Some(existing_client) = self.ssh_clients.get(&ssh_url) {
- if existing_client.upgrade().is_some() {
- return;
- }
+ if let Some(existing_client) = self.remote_clients.get(&options)
+ && existing_client.upgrade().is_some()
+ {
+ return;
}
- self.ssh_clients.insert(ssh_url, client.downgrade());
+ self.remote_clients.insert(options, client.downgrade());
self.ssh_registered_tx.unbounded_send(()).ok();
}
}
@@ -3,10 +3,11 @@ use collections::HashMap;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use std::sync::Arc;
-#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
pub struct ExtensionSettings {
/// The extensions that should be automatically installed by Zed.
///
@@ -38,8 +39,6 @@ impl ExtensionSettings {
}
impl Settings for ExtensionSettings {
- const KEY: Option<&'static str> = None;
-
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
@@ -160,7 +160,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
language_servers: BTreeMap::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
- indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
debug_adapters: Default::default(),
@@ -191,7 +190,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
language_servers: BTreeMap::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
- indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
debug_adapters: Default::default(),
@@ -371,7 +369,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
language_servers: BTreeMap::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
- indexed_docs_providers: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
debug_adapters: Default::default(),
@@ -163,6 +163,7 @@ impl HeadlessExtensionStore {
queries: LanguageQueries::default(),
context_provider: None,
toolchain_provider: None,
+ manifest_name: None,
})
}),
);
@@ -174,7 +175,7 @@ impl HeadlessExtensionStore {
}
let wasm_extension: Arc<dyn Extension> =
- Arc::new(WasmExtension::load(&extension_dir, &manifest, wasm_host.clone(), &cx).await?);
+ Arc::new(WasmExtension::load(&extension_dir, &manifest, wasm_host.clone(), cx).await?);
for (language_server_id, language_server_config) in &manifest.language_servers {
for language in language_server_config.languages() {
@@ -532,7 +532,7 @@ fn wasm_engine(executor: &BackgroundExecutor) -> wasmtime::Engine {
// `Future::poll`.
const EPOCH_INTERVAL: Duration = Duration::from_millis(100);
let mut timer = Timer::interval(EPOCH_INTERVAL);
- while let Some(_) = timer.next().await {
+ while (timer.next().await).is_some() {
// Exit the loop and thread once the engine is dropped.
let Some(engine) = engine_ref.upgrade() else {
break;
@@ -701,16 +701,15 @@ pub fn parse_wasm_extension_version(
for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
if let wasmparser::Payload::CustomSection(s) =
part.context("error parsing wasm extension")?
+ && s.name() == "zed:api-version"
{
- if s.name() == "zed:api-version" {
- version = parse_wasm_extension_version_custom_section(s.data());
- if version.is_none() {
- bail!(
- "extension {} has invalid zed:api-version section: {:?}",
- extension_id,
- s.data()
- );
- }
+ version = parse_wasm_extension_version_custom_section(s.data());
+ if version.is_none() {
+ bail!(
+ "extension {} has invalid zed:api-version section: {:?}",
+ extension_id,
+ s.data()
+ );
}
}
}
@@ -938,7 +938,7 @@ impl ExtensionImports for WasmState {
binary: settings.binary.map(|binary| settings::CommandSettings {
path: binary.path,
arguments: binary.arguments,
- env: binary.env,
+ env: binary.env.map(|env| env.into_iter().collect()),
}),
settings: settings.settings,
initialization_options: settings.initialization_options,
@@ -61,7 +61,6 @@ impl RenderOnce for FeatureUpsell {
.icon_size(IconSize::Small)
.icon_position(IconPosition::End)
.on_click({
- let docs_url = docs_url.clone();
move |_event, _window, cx| {
telemetry::event!(
"Documentation Viewed",
@@ -207,8 +207,8 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let version_match = &self.matches[ix];
- let extension_version = &self.extension_versions[version_match.candidate_id];
+ let version_match = &self.matches.get(ix)?;
+ let extension_version = &self.extension_versions.get(version_match.candidate_id)?;
let is_version_compatible =
extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version);
@@ -116,6 +116,7 @@ pub fn init(cx: &mut App) {
files: false,
directories: true,
multiple: false,
+ prompt: None,
},
DirectoryLister::Local(
workspace.project().clone(),
@@ -326,7 +327,7 @@ impl ExtensionsPage {
let query_editor = cx.new(|cx| {
let mut input = Editor::single_line(window, cx);
- input.set_placeholder_text("Search extensions...", cx);
+ input.set_placeholder_text("Search extensions...", window, cx);
if let Some(id) = focus_extension_id {
input.set_text(format!("id:{id}"), window, cx);
}
@@ -693,7 +694,7 @@ impl ExtensionsPage {
cx.open_url(&repository_url);
}
}))
- .tooltip(Tooltip::text(repository_url.clone()))
+ .tooltip(Tooltip::text(repository_url))
})),
)
}
@@ -703,7 +704,7 @@ impl ExtensionsPage {
extension: &ExtensionMetadata,
cx: &mut Context<Self>,
) -> ExtensionCard {
- let this = cx.entity().clone();
+ let this = cx.entity();
let status = Self::extension_status(&extension.id, cx);
let has_dev_extension = Self::dev_extension_exists(&extension.id, cx);
@@ -826,7 +827,7 @@ impl ExtensionsPage {
cx.open_url(&repository_url);
}
}))
- .tooltip(Tooltip::text(repository_url.clone())),
+ .tooltip(Tooltip::text(repository_url)),
)
.child(
PopoverMenu::new(SharedString::from(format!(
@@ -862,7 +863,7 @@ impl ExtensionsPage {
window: &mut Window,
cx: &mut App,
) -> Entity<ContextMenu> {
- let context_menu = ContextMenu::build(window, cx, |context_menu, window, _| {
+ ContextMenu::build(window, cx, |context_menu, window, _| {
context_menu
.entry(
"Install Another Version...",
@@ -886,9 +887,7 @@ impl ExtensionsPage {
cx.write_to_clipboard(ClipboardItem::new_string(authors.join(", ")));
}
})
- });
-
- context_menu
+ })
}
fn show_extension_version_list(
@@ -1030,15 +1029,14 @@ impl ExtensionsPage {
.read(cx)
.extension_manifest_for_id(&extension_id)
.cloned()
+ && let Some(events) = extension::ExtensionEvents::try_global(cx)
{
- if let Some(events) = extension::ExtensionEvents::try_global(cx) {
- events.update(cx, |this, cx| {
- this.emit(
- extension::Event::ConfigureExtensionRequested(manifest),
- cx,
- )
- });
- }
+ events.update(cx, |this, cx| {
+ this.emit(
+ extension::Event::ConfigureExtensionRequested(manifest),
+ cx,
+ )
+ });
}
}
})
@@ -1347,7 +1345,7 @@ impl ExtensionsPage {
this.update_settings::<VimModeSetting>(
selection,
cx,
- |setting, value| *setting = Some(value),
+ |setting, value| setting.vim_mode = Some(value),
);
}),
)),
@@ -14,7 +14,7 @@ struct FeatureFlags {
}
pub static ZED_DISABLE_STAFF: LazyLock<bool> = LazyLock::new(|| {
- std::env::var("ZED_DISABLE_STAFF").map_or(false, |value| !value.is_empty() && value != "0")
+ std::env::var("ZED_DISABLE_STAFF").is_ok_and(|value| !value.is_empty() && value != "0")
});
impl FeatureFlags {
@@ -23,7 +23,7 @@ impl FeatureFlags {
return true;
}
- if self.staff && T::enabled_for_staff() {
+ if (cfg!(debug_assertions) || self.staff) && !*ZED_DISABLE_STAFF && T::enabled_for_staff() {
return true;
}
@@ -66,9 +66,10 @@ impl FeatureFlag for LlmClosedBetaFeatureFlag {
const NAME: &'static str = "llm-closed-beta";
}
-pub struct ZedProFeatureFlag {}
-impl FeatureFlag for ZedProFeatureFlag {
- const NAME: &'static str = "zed-pro";
+pub struct BillingV2FeatureFlag {}
+
+impl FeatureFlag for BillingV2FeatureFlag {
+ const NAME: &'static str = "billing-v2";
}
pub struct NotebookFeatureFlag;
@@ -77,14 +78,6 @@ impl FeatureFlag for NotebookFeatureFlag {
const NAME: &'static str = "notebooks";
}
-pub struct ThreadAutoCaptureFeatureFlag {}
-impl FeatureFlag for ThreadAutoCaptureFeatureFlag {
- const NAME: &'static str = "thread-auto-capture";
-
- fn enabled_for_staff() -> bool {
- false
- }
-}
pub struct PanicFeatureFlag;
impl FeatureFlag for PanicFeatureFlag {
@@ -97,10 +90,29 @@ impl FeatureFlag for JjUiFeatureFlag {
const NAME: &'static str = "jj-ui";
}
-pub struct AcpFeatureFlag;
+pub struct GeminiAndNativeFeatureFlag;
+
+impl FeatureFlag for GeminiAndNativeFeatureFlag {
+ // This was previously called "acp".
+ //
+ // We renamed it because existing builds used it to enable the Claude Code
+ // integration too, and we'd like to turn Gemini/Native on in new builds
+ // without enabling Claude Code in old builds.
+ const NAME: &'static str = "gemini-and-native";
+
+ fn enabled_for_all() -> bool {
+ true
+ }
+}
+
+pub struct ClaudeCodeFeatureFlag;
+
+impl FeatureFlag for ClaudeCodeFeatureFlag {
+ const NAME: &'static str = "claude-code";
-impl FeatureFlag for AcpFeatureFlag {
- const NAME: &'static str = "acp";
+ fn enabled_for_all() -> bool {
+ true
+ }
}
pub trait FeatureFlagViewExt<V: 'static> {
@@ -198,7 +210,10 @@ impl FeatureFlagAppExt for App {
fn has_flag<T: FeatureFlag>(&self) -> bool {
self.try_global::<FeatureFlags>()
.map(|flags| flags.has_flag::<T>())
- .unwrap_or(false)
+ .unwrap_or_else(|| {
+ (cfg!(debug_assertions) && T::enabled_for_staff() && !*ZED_DISABLE_STAFF)
+ || T::enabled_for_all()
+ })
}
fn is_staff(&self) -> bool {
@@ -15,13 +15,9 @@ path = "src/feedback.rs"
test-support = []
[dependencies]
-client.workspace = true
gpui.workspace = true
-human_bytes = "0.4.1"
menu.workspace = true
-release_channel.workspace = true
-serde.workspace = true
-sysinfo.workspace = true
+system_specs.workspace = true
ui.workspace = true
urlencoding.workspace = true
util.workspace = true
@@ -1,18 +1,14 @@
use gpui::{App, ClipboardItem, PromptLevel, actions};
-use system_specs::SystemSpecs;
+use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs};
use util::ResultExt;
use workspace::Workspace;
use zed_actions::feedback::FileBugReport;
pub mod feedback_modal;
-pub mod system_specs;
-
actions!(
zed,
[
- /// Copies system specifications to the clipboard for bug reports.
- CopySystemSpecsIntoClipboard,
/// Opens email client to send feedback to Zed support.
EmailZed,
/// Opens the Zed repository on GitHub.
@@ -209,11 +209,11 @@ impl FileFinder {
let Some(init_modifiers) = self.init_modifiers.take() else {
return;
};
- if self.picker.read(cx).delegate.has_changed_selected_index {
- if !event.modified() || !init_modifiers.is_subset_of(&event) {
- self.init_modifiers = None;
- window.dispatch_action(menu::Confirm.boxed_clone(), cx);
- }
+ if self.picker.read(cx).delegate.has_changed_selected_index
+ && (!event.modified() || !init_modifiers.is_subset_of(event))
+ {
+ self.init_modifiers = None;
+ window.dispatch_action(menu::Confirm.boxed_clone(), cx);
}
}
@@ -267,10 +267,9 @@ impl FileFinder {
) {
self.picker.update(cx, |picker, cx| {
picker.delegate.include_ignored = match picker.delegate.include_ignored {
- Some(true) => match FileFinderSettings::get_global(cx).include_ignored {
- Some(_) => Some(false),
- None => None,
- },
+ Some(true) => FileFinderSettings::get_global(cx)
+ .include_ignored
+ .map(|_| false),
Some(false) => Some(true),
None => Some(true),
};
@@ -323,27 +322,27 @@ impl FileFinder {
) {
self.picker.update(cx, |picker, cx| {
let delegate = &mut picker.delegate;
- if let Some(workspace) = delegate.workspace.upgrade() {
- if let Some(m) = delegate.matches.get(delegate.selected_index()) {
- let path = match &m {
- Match::History { path, .. } => {
- let worktree_id = path.project.worktree_id;
- ProjectPath {
- worktree_id,
- path: Arc::clone(&path.project.path),
- }
+ if let Some(workspace) = delegate.workspace.upgrade()
+ && let Some(m) = delegate.matches.get(delegate.selected_index())
+ {
+ let path = match &m {
+ Match::History { path, .. } => {
+ let worktree_id = path.project.worktree_id;
+ ProjectPath {
+ worktree_id,
+ path: Arc::clone(&path.project.path),
}
- Match::Search(m) => ProjectPath {
- worktree_id: WorktreeId::from_usize(m.0.worktree_id),
- path: m.0.path.clone(),
- },
- Match::CreateNew(p) => p.clone(),
- };
- let open_task = workspace.update(cx, move |workspace, cx| {
- workspace.split_path_preview(path, false, Some(split_direction), window, cx)
- });
- open_task.detach_and_log_err(cx);
- }
+ }
+ Match::Search(m) => ProjectPath {
+ worktree_id: WorktreeId::from_usize(m.0.worktree_id),
+ path: m.0.path.clone(),
+ },
+ Match::CreateNew(p) => p.clone(),
+ };
+ let open_task = workspace.update(cx, move |workspace, cx| {
+ workspace.split_path_preview(path, false, Some(split_direction), window, cx)
+ });
+ open_task.detach_and_log_err(cx);
}
})
}
@@ -497,7 +496,7 @@ impl Match {
fn panel_match(&self) -> Option<&ProjectPanelOrdMatch> {
match self {
Match::History { panel_match, .. } => panel_match.as_ref(),
- Match::Search(panel_match) => Some(&panel_match),
+ Match::Search(panel_match) => Some(panel_match),
Match::CreateNew(_) => None,
}
}
@@ -537,7 +536,7 @@ impl Matches {
self.matches.binary_search_by(|m| {
// `reverse()` since if cmp_matches(a, b) == Ordering::Greater, then a is better than b.
// And we want the better entries go first.
- Self::cmp_matches(self.separate_history, currently_opened, &m, &entry).reverse()
+ Self::cmp_matches(self.separate_history, currently_opened, m, entry).reverse()
})
}
}
@@ -675,17 +674,17 @@ impl Matches {
let path_str = panel_match.0.path.to_string_lossy();
let filename_str = filename.to_string_lossy();
- if let Some(filename_pos) = path_str.rfind(&*filename_str) {
- if panel_match.0.positions[0] >= filename_pos {
- let mut prev_position = panel_match.0.positions[0];
- for p in &panel_match.0.positions[1..] {
- if *p != prev_position + 1 {
- return false;
- }
- prev_position = *p;
+ if let Some(filename_pos) = path_str.rfind(&*filename_str)
+ && panel_match.0.positions[0] >= filename_pos
+ {
+ let mut prev_position = panel_match.0.positions[0];
+ for p in &panel_match.0.positions[1..] {
+ if *p != prev_position + 1 {
+ return false;
}
- return true;
+ prev_position = *p;
}
+ return true;
}
}
@@ -878,9 +877,7 @@ impl FileFinderDelegate {
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: self.include_ignored.unwrap_or_else(|| {
- worktree
- .root_entry()
- .map_or(false, |entry| entry.is_ignored)
+ worktree.root_entry().is_some_and(|entry| entry.is_ignored)
}),
include_root_name,
candidates: project::Candidates::Files,
@@ -1045,10 +1042,10 @@ impl FileFinderDelegate {
)
} else {
let mut path = Arc::clone(project_relative_path);
- if project_relative_path.as_ref() == Path::new("") {
- if let Some(absolute_path) = &entry_path.absolute {
- path = Arc::from(absolute_path.as_path());
- }
+ if project_relative_path.as_ref() == Path::new("")
+ && let Some(absolute_path) = &entry_path.absolute
+ {
+ path = Arc::from(absolute_path.as_path());
}
let mut path_match = PathMatch {
@@ -1078,23 +1075,21 @@ impl FileFinderDelegate {
),
};
- if file_name_positions.is_empty() {
- if let Some(user_home_path) = std::env::var("HOME").ok() {
- let user_home_path = user_home_path.trim();
- if !user_home_path.is_empty() {
- if (&full_path).starts_with(user_home_path) {
- full_path.replace_range(0..user_home_path.len(), "~");
- full_path_positions.retain_mut(|pos| {
- if *pos >= user_home_path.len() {
- *pos -= user_home_path.len();
- *pos += 1;
- true
- } else {
- false
- }
- })
+ if file_name_positions.is_empty()
+ && let Some(user_home_path) = std::env::var("HOME").ok()
+ {
+ let user_home_path = user_home_path.trim();
+ if !user_home_path.is_empty() && full_path.starts_with(user_home_path) {
+ full_path.replace_range(0..user_home_path.len(), "~");
+ full_path_positions.retain_mut(|pos| {
+ if *pos >= user_home_path.len() {
+ *pos -= user_home_path.len();
+ *pos += 1;
+ true
+ } else {
+ false
}
- }
+ })
}
}
@@ -1242,14 +1237,13 @@ impl FileFinderDelegate {
/// Skips first history match (that is displayed topmost) if it's currently opened.
fn calculate_selected_index(&self, cx: &mut Context<Picker<Self>>) -> usize {
- if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search {
- if let Some(Match::History { path, .. }) = self.matches.get(0) {
- if Some(path) == self.currently_opened_path.as_ref() {
- let elements_after_first = self.matches.len() - 1;
- if elements_after_first > 0 {
- return 1;
- }
- }
+ if FileFinderSettings::get_global(cx).skip_focus_for_active_in_search
+ && let Some(Match::History { path, .. }) = self.matches.get(0)
+ && Some(path) == self.currently_opened_path.as_ref()
+ {
+ let elements_after_first = self.matches.len() - 1;
+ if elements_after_first > 0 {
+ return 1;
}
}
@@ -1310,10 +1304,10 @@ impl PickerDelegate for FileFinderDelegate {
.enumerate()
.find(|(_, m)| !matches!(m, Match::History { .. }))
.map(|(i, _)| i);
- if let Some(first_non_history_index) = first_non_history_index {
- if first_non_history_index > 0 {
- return vec![first_non_history_index - 1];
- }
+ if let Some(first_non_history_index) = first_non_history_index
+ && first_non_history_index > 0
+ {
+ return vec![first_non_history_index - 1];
}
}
Vec::new()
@@ -1387,7 +1381,7 @@ impl PickerDelegate for FileFinderDelegate {
project
.worktree_for_id(history_item.project.worktree_id, cx)
.is_some()
- || ((project.is_local() || project.is_via_ssh())
+ || ((project.is_local() || project.is_via_remote_server())
&& history_item.absolute.is_some())
}),
self.currently_opened_path.as_ref(),
@@ -1402,18 +1396,21 @@ impl PickerDelegate for FileFinderDelegate {
cx.notify();
Task::ready(())
} else {
- let path_position = PathWithPosition::parse_str(&raw_query);
+ let path_position = PathWithPosition::parse_str(raw_query);
#[cfg(windows)]
let raw_query = raw_query.trim().to_owned().replace("/", "\\");
#[cfg(not(windows))]
- let raw_query = raw_query.trim().to_owned();
+ let raw_query = raw_query.trim();
- let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query {
+ let raw_query = raw_query.trim_end_matches(':').to_owned();
+ let path = path_position.path.to_str();
+ let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':');
+ let file_query_end = if path_trimmed == raw_query {
None
} else {
// Safe to unwrap as we won't get here when the unwrap in if fails
- Some(path_position.path.to_str().unwrap().len())
+ Some(path.unwrap().len())
};
let query = FileSearchQuery {
@@ -1436,69 +1433,101 @@ impl PickerDelegate for FileFinderDelegate {
window: &mut Window,
cx: &mut Context<Picker<FileFinderDelegate>>,
) {
- if let Some(m) = self.matches.get(self.selected_index()) {
- if let Some(workspace) = self.workspace.upgrade() {
- let open_task = workspace.update(cx, |workspace, cx| {
- let split_or_open =
- |workspace: &mut Workspace,
- project_path,
- window: &mut Window,
- cx: &mut Context<Workspace>| {
- let allow_preview =
- PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
- if secondary {
- workspace.split_path_preview(
- project_path,
- allow_preview,
- None,
- window,
- cx,
- )
- } else {
- workspace.open_path_preview(
- project_path,
- None,
- true,
- allow_preview,
- true,
- window,
- cx,
- )
- }
- };
- match &m {
- Match::CreateNew(project_path) => {
- // Create a new file with the given filename
- if secondary {
- workspace.split_path_preview(
- project_path.clone(),
- false,
- None,
- window,
- cx,
- )
- } else {
- workspace.open_path_preview(
- project_path.clone(),
- None,
- true,
- false,
- true,
- window,
- cx,
- )
- }
+ if let Some(m) = self.matches.get(self.selected_index())
+ && let Some(workspace) = self.workspace.upgrade()
+ {
+ let open_task = workspace.update(cx, |workspace, cx| {
+ let split_or_open =
+ |workspace: &mut Workspace,
+ project_path,
+ window: &mut Window,
+ cx: &mut Context<Workspace>| {
+ let allow_preview =
+ PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder;
+ if secondary {
+ workspace.split_path_preview(
+ project_path,
+ allow_preview,
+ None,
+ window,
+ cx,
+ )
+ } else {
+ workspace.open_path_preview(
+ project_path,
+ None,
+ true,
+ allow_preview,
+ true,
+ window,
+ cx,
+ )
}
+ };
+ match &m {
+ Match::CreateNew(project_path) => {
+ // Create a new file with the given filename
+ if secondary {
+ workspace.split_path_preview(
+ project_path.clone(),
+ false,
+ None,
+ window,
+ cx,
+ )
+ } else {
+ workspace.open_path_preview(
+ project_path.clone(),
+ None,
+ true,
+ false,
+ true,
+ window,
+ cx,
+ )
+ }
+ }
- Match::History { path, .. } => {
- let worktree_id = path.project.worktree_id;
- if workspace
- .project()
- .read(cx)
- .worktree_for_id(worktree_id, cx)
- .is_some()
- {
- split_or_open(
+ Match::History { path, .. } => {
+ let worktree_id = path.project.worktree_id;
+ if workspace
+ .project()
+ .read(cx)
+ .worktree_for_id(worktree_id, cx)
+ .is_some()
+ {
+ split_or_open(
+ workspace,
+ ProjectPath {
+ worktree_id,
+ path: Arc::clone(&path.project.path),
+ },
+ window,
+ cx,
+ )
+ } else {
+ match path.absolute.as_ref() {
+ Some(abs_path) => {
+ if secondary {
+ workspace.split_abs_path(
+ abs_path.to_path_buf(),
+ false,
+ window,
+ cx,
+ )
+ } else {
+ workspace.open_abs_path(
+ abs_path.to_path_buf(),
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
+ }
+ }
+ None => split_or_open(
workspace,
ProjectPath {
worktree_id,
@@ -1506,88 +1535,52 @@ impl PickerDelegate for FileFinderDelegate {
},
window,
cx,
- )
- } else {
- match path.absolute.as_ref() {
- Some(abs_path) => {
- if secondary {
- workspace.split_abs_path(
- abs_path.to_path_buf(),
- false,
- window,
- cx,
- )
- } else {
- workspace.open_abs_path(
- abs_path.to_path_buf(),
- OpenOptions {
- visible: Some(OpenVisible::None),
- ..Default::default()
- },
- window,
- cx,
- )
- }
- }
- None => split_or_open(
- workspace,
- ProjectPath {
- worktree_id,
- path: Arc::clone(&path.project.path),
- },
- window,
- cx,
- ),
- }
+ ),
}
}
- Match::Search(m) => split_or_open(
- workspace,
- ProjectPath {
- worktree_id: WorktreeId::from_usize(m.0.worktree_id),
- path: m.0.path.clone(),
- },
- window,
- cx,
- ),
}
- });
+ Match::Search(m) => split_or_open(
+ workspace,
+ ProjectPath {
+ worktree_id: WorktreeId::from_usize(m.0.worktree_id),
+ path: m.0.path.clone(),
+ },
+ window,
+ cx,
+ ),
+ }
+ });
- let row = self
- .latest_search_query
- .as_ref()
- .and_then(|query| query.path_position.row)
- .map(|row| row.saturating_sub(1));
- let col = self
- .latest_search_query
- .as_ref()
- .and_then(|query| query.path_position.column)
- .unwrap_or(0)
- .saturating_sub(1);
- let finder = self.file_finder.clone();
-
- cx.spawn_in(window, async move |_, cx| {
- let item = open_task.await.notify_async_err(cx)?;
- if let Some(row) = row {
- if let Some(active_editor) = item.downcast::<Editor>() {
- active_editor
- .downgrade()
- .update_in(cx, |editor, window, cx| {
- editor.go_to_singleton_buffer_point(
- Point::new(row, col),
- window,
- cx,
- );
- })
- .log_err();
- }
- }
- finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?;
+ let row = self
+ .latest_search_query
+ .as_ref()
+ .and_then(|query| query.path_position.row)
+ .map(|row| row.saturating_sub(1));
+ let col = self
+ .latest_search_query
+ .as_ref()
+ .and_then(|query| query.path_position.column)
+ .unwrap_or(0)
+ .saturating_sub(1);
+ let finder = self.file_finder.clone();
+
+ cx.spawn_in(window, async move |_, cx| {
+ let item = open_task.await.notify_async_err(cx)?;
+ if let Some(row) = row
+ && let Some(active_editor) = item.downcast::<Editor>()
+ {
+ active_editor
+ .downgrade()
+ .update_in(cx, |editor, window, cx| {
+ editor.go_to_singleton_buffer_point(Point::new(row, col), window, cx);
+ })
+ .log_err();
+ }
+ finder.update(cx, |_, cx| cx.emit(DismissEvent)).ok()?;
- Some(())
- })
- .detach();
- }
+ Some(())
+ })
+ .detach();
}
}
@@ -1606,10 +1599,7 @@ impl PickerDelegate for FileFinderDelegate {
) -> Option<Self::ListItem> {
let settings = FileFinderSettings::get_global(cx);
- let path_match = self
- .matches
- .get(ix)
- .expect("Invalid matches state: no element for index {ix}");
+ let path_match = self.matches.get(ix)?;
let history_icon = match &path_match {
Match::History { .. } => Icon::new(IconName::HistoryRerun)
@@ -1759,7 +1749,7 @@ impl PickerDelegate for FileFinderDelegate {
Some(ContextMenu::build(window, cx, {
let focus_handle = focus_handle.clone();
move |menu, _, _| {
- menu.context(focus_handle.clone())
+ menu.context(focus_handle)
.action(
"Split Left",
pane::SplitLeft.boxed_clone(),
@@ -1,7 +1,7 @@
use anyhow::Result;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
pub struct FileFinderSettings {
@@ -11,7 +11,8 @@ pub struct FileFinderSettings {
pub include_ignored: Option<bool>,
}
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "file_finder")]
pub struct FileFinderSettingsContent {
/// Whether to show file icons in the file finder.
///
@@ -42,8 +43,6 @@ pub struct FileFinderSettingsContent {
}
impl Settings for FileFinderSettings {
- const KEY: Option<&'static str> = Some("file_finder");
-
type FileContent = FileFinderSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut gpui::App) -> Result<Self> {
@@ -218,6 +218,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
" ndan ",
" band ",
"a bandana",
+ "bandana:",
] {
picker
.update_in(cx, |picker, window, cx| {
@@ -252,6 +253,53 @@ async fn test_matching_paths(cx: &mut TestAppContext) {
}
}
+#[gpui::test]
+async fn test_matching_paths_with_colon(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ path!("/root"),
+ json!({
+ "a": {
+ "foo:bar.rs": "",
+ "foo.rs": "",
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
+
+ let (picker, _, cx) = build_find_picker(project, cx);
+
+ // 'foo:' matches both files
+ cx.simulate_input("foo:");
+ picker.update(cx, |picker, _| {
+ assert_eq!(picker.delegate.matches.len(), 3);
+ assert_match_at_position(picker, 0, "foo.rs");
+ assert_match_at_position(picker, 1, "foo:bar.rs");
+ });
+
+ // 'foo:b' matches one of the files
+ cx.simulate_input("b");
+ picker.update(cx, |picker, _| {
+ assert_eq!(picker.delegate.matches.len(), 2);
+ assert_match_at_position(picker, 0, "foo:bar.rs");
+ });
+
+ cx.dispatch_action(editor::actions::Backspace);
+
+ // 'foo:1' matches both files, specifying which row to jump to
+ cx.simulate_input("1");
+ picker.update(cx, |picker, _| {
+ assert_eq!(picker.delegate.matches.len(), 3);
+ assert_match_at_position(picker, 0, "foo.rs");
+ assert_match_at_position(picker, 1, "foo:bar.rs");
+ });
+}
+
#[gpui::test]
async fn test_unicode_paths(cx: &mut TestAppContext) {
let app_state = init_test(cx);
@@ -1614,7 +1662,7 @@ async fn test_select_current_open_file_when_no_history(cx: &mut gpui::TestAppCon
let picker = open_file_picker(&workspace, cx);
picker.update(cx, |finder, _| {
- assert_match_selection(&finder, 0, "1_qw");
+ assert_match_selection(finder, 0, "1_qw");
});
}
@@ -2623,7 +2671,7 @@ async fn open_queried_buffer(
workspace: &Entity<Workspace>,
cx: &mut gpui::VisualTestContext,
) -> Vec<FoundPath> {
- let picker = open_file_picker(&workspace, cx);
+ let picker = open_file_picker(workspace, cx);
cx.simulate_input(input);
let history_items = picker.update(cx, |finder, _| {
@@ -1,7 +1,7 @@
use crate::file_finder_settings::FileFinderSettings;
use file_icons::FileIcons;
use futures::channel::oneshot;
-use fuzzy::{StringMatch, StringMatchCandidate};
+use fuzzy::{CharBag, StringMatch, StringMatchCandidate};
use gpui::{HighlightStyle, StyledText, Task};
use picker::{Picker, PickerDelegate};
use project::{DirectoryItem, DirectoryLister};
@@ -23,7 +23,6 @@ use workspace::Workspace;
pub(crate) struct OpenPathPrompt;
-#[derive(Debug)]
pub struct OpenPathDelegate {
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
lister: DirectoryLister,
@@ -35,6 +34,9 @@ pub struct OpenPathDelegate {
prompt_root: String,
path_style: PathStyle,
replace_prompt: Task<()>,
+ render_footer:
+ Arc<dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static>,
+ hidden_entries: bool,
}
impl OpenPathDelegate {
@@ -60,9 +62,25 @@ impl OpenPathDelegate {
},
path_style,
replace_prompt: Task::ready(()),
+ render_footer: Arc::new(|_, _| None),
+ hidden_entries: false,
}
}
+ pub fn with_footer(
+ mut self,
+ footer: Arc<
+ dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static,
+ >,
+ ) -> Self {
+ self.render_footer = footer;
+ self
+ }
+
+ pub fn show_hidden(mut self) -> Self {
+ self.hidden_entries = true;
+ self
+ }
fn get_entry(&self, selected_match_index: usize) -> Option<CandidateInfo> {
match &self.directory_state {
DirectoryState::List { entries, .. } => {
@@ -75,16 +93,16 @@ impl OpenPathDelegate {
..
} => {
let mut i = selected_match_index;
- if let Some(user_input) = user_input {
- if !user_input.exists || !user_input.is_dir {
- if i == 0 {
- return Some(CandidateInfo {
- path: user_input.file.clone(),
- is_dir: false,
- });
- } else {
- i -= 1;
- }
+ if let Some(user_input) = user_input
+ && (!user_input.exists || !user_input.is_dir)
+ {
+ if i == 0 {
+ return Some(CandidateInfo {
+ path: user_input.file.clone(),
+ is_dir: false,
+ });
+ } else {
+ i -= 1;
}
}
let id = self.string_matches.get(i)?.candidate_id;
@@ -112,7 +130,7 @@ impl OpenPathDelegate {
entries,
..
} => user_input
- .into_iter()
+ .iter()
.filter(|user_input| !user_input.exists || !user_input.is_dir)
.map(|user_input| user_input.file.string.clone())
.chain(self.string_matches.iter().filter_map(|string_match| {
@@ -125,6 +143,13 @@ impl OpenPathDelegate {
DirectoryState::None { .. } => Vec::new(),
}
}
+
+ fn current_dir(&self) -> &'static str {
+ match self.path_style {
+ PathStyle::Posix => "./",
+ PathStyle::Windows => ".\\",
+ }
+ }
}
#[derive(Debug)]
@@ -233,6 +258,7 @@ impl PickerDelegate for OpenPathDelegate {
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let lister = &self.lister;
+ let input_is_empty = query.is_empty();
let (dir, suffix) = get_dir_and_suffix(query, self.path_style);
let query = match &self.directory_state {
@@ -261,8 +287,9 @@ impl PickerDelegate for OpenPathDelegate {
self.cancel_flag.store(true, atomic::Ordering::Release);
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
-
+ let hidden_entries = self.hidden_entries;
let parent_path_is_root = self.prompt_root == dir;
+ let current_dir = self.current_dir();
cx.spawn_in(window, async move |this, cx| {
if let Some(query) = query {
let paths = query.await;
@@ -353,10 +380,39 @@ impl PickerDelegate for OpenPathDelegate {
return;
};
- if !suffix.starts_with('.') {
- new_entries.retain(|entry| !entry.path.string.starts_with('.'));
+ let mut max_id = 0;
+ if !suffix.starts_with('.') && !hidden_entries {
+ new_entries.retain(|entry| {
+ max_id = max_id.max(entry.path.id);
+ !entry.path.string.starts_with('.')
+ });
}
+
if suffix.is_empty() {
+ let should_prepend_with_current_dir = this
+ .read_with(cx, |picker, _| {
+ !input_is_empty
+ && match &picker.delegate.directory_state {
+ DirectoryState::List { error, .. } => error.is_none(),
+ DirectoryState::Create { .. } => false,
+ DirectoryState::None { .. } => false,
+ }
+ })
+ .unwrap_or(false);
+ if should_prepend_with_current_dir {
+ new_entries.insert(
+ 0,
+ CandidateInfo {
+ path: StringMatchCandidate {
+ id: max_id + 1,
+ string: current_dir.to_string(),
+ char_bag: CharBag::from(current_dir),
+ },
+ is_dir: true,
+ },
+ );
+ }
+
this.update(cx, |this, cx| {
this.delegate.selected_index = 0;
this.delegate.string_matches = new_entries
@@ -485,6 +541,10 @@ impl PickerDelegate for OpenPathDelegate {
_: &mut Context<Picker<Self>>,
) -> Option<String> {
let candidate = self.get_entry(self.selected_index)?;
+ if candidate.path.string.is_empty() || candidate.path.string == self.current_dir() {
+ return None;
+ }
+
let path_style = self.path_style;
Some(
maybe!({
@@ -629,15 +689,21 @@ impl PickerDelegate for OpenPathDelegate {
DirectoryState::None { .. } => Vec::new(),
};
+ let is_current_dir_candidate = candidate.path.string == self.current_dir();
+
let file_icon = maybe!({
if !settings.file_icons {
return None;
}
let icon = if candidate.is_dir {
- FileIcons::get_folder_icon(false, cx)?
+ if is_current_dir_candidate {
+ return Some(Icon::new(IconName::ReplyArrowRight).color(Color::Muted));
+ } else {
+ FileIcons::get_folder_icon(false, cx)?
+ }
} else {
let path = path::Path::new(&candidate.path.string);
- FileIcons::get_icon(&path, cx)?
+ FileIcons::get_icon(path, cx)?
};
Some(Icon::from_path(icon).color(Color::Muted))
});
@@ -652,8 +718,10 @@ impl PickerDelegate for OpenPathDelegate {
.child(HighlightedLabel::new(
if parent_path == &self.prompt_root {
format!("{}{}", self.prompt_root, candidate.path.string)
+ } else if is_current_dir_candidate {
+ "open this directory".to_string()
} else {
- candidate.path.string.clone()
+ candidate.path.string
},
match_positions,
)),
@@ -684,7 +752,7 @@ impl PickerDelegate for OpenPathDelegate {
};
StyledText::new(label)
.with_default_highlights(
- &window.text_style().clone(),
+ &window.text_style(),
vec![(
delta..delta + label_len,
HighlightStyle::color(Color::Conflict.color(cx)),
@@ -694,7 +762,7 @@ impl PickerDelegate for OpenPathDelegate {
} else {
StyledText::new(format!("{label} (create)"))
.with_default_highlights(
- &window.text_style().clone(),
+ &window.text_style(),
vec![(
delta..delta + label_len,
HighlightStyle::color(Color::Created.color(cx)),
@@ -728,10 +796,18 @@ impl PickerDelegate for OpenPathDelegate {
.child(LabelLike::new().child(label_with_highlights)),
)
}
- DirectoryState::None { .. } => return None,
+ DirectoryState::None { .. } => None,
}
}
+ fn render_footer(
+ &self,
+ window: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Option<AnyElement> {
+ (self.render_footer)(window, cx)
+ }
+
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some(match &self.directory_state {
DirectoryState::Create { .. } => SharedString::from("Type a path…"),
@@ -747,6 +823,17 @@ impl PickerDelegate for OpenPathDelegate {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
Arc::from(format!("[directory{MAIN_SEPARATOR_STR}]filename.ext"))
}
+
+ fn separators_after_indices(&self) -> Vec<usize> {
+ let Some(m) = self.string_matches.first() else {
+ return Vec::new();
+ };
+ if m.string == self.current_dir() {
+ vec![0]
+ } else {
+ Vec::new()
+ }
+ }
}
fn path_candidates(
@@ -39,16 +39,24 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
let (picker, cx) = build_open_path_prompt(project, false, PathStyle::current(), cx);
+ insert_query(path!("sadjaoislkdjasldj"), &picker, cx).await;
+ assert_eq!(collect_match_candidates(&picker, cx), Vec::<String>::new());
+
let query = path!("/root");
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["root"]);
+ #[cfg(not(windows))]
+ let expected_separator = "./";
+ #[cfg(windows)]
+ let expected_separator = ".\\";
+
// If the query ends with a slash, the picker should show the contents of the directory.
let query = path!("/root/");
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
- vec!["a1", "a2", "a3", "dir1", "dir2"]
+ vec![expected_separator, "a1", "a2", "a3", "dir1", "dir2"]
);
// Show candidates for the query "a".
@@ -72,7 +80,7 @@ async fn test_open_path_prompt(cx: &mut TestAppContext) {
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
- vec!["c", "d1", "d2", "d3", "dir3", "dir4"]
+ vec![expected_separator, "c", "d1", "d2", "d3", "dir3", "dir4"]
);
// Show candidates for the query "d".
@@ -116,71 +124,86 @@ async fn test_open_path_prompt_completion(cx: &mut TestAppContext) {
// Confirm completion for the query "/root", since it's a directory, it should add a trailing slash.
let query = path!("/root");
insert_query(query, &picker, cx).await;
- assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/"));
+ assert_eq!(
+ confirm_completion(query, 0, &picker, cx).unwrap(),
+ path!("/root/")
+ );
// Confirm completion for the query "/root/", selecting the first candidate "a", since it's a file, it should not add a trailing slash.
let query = path!("/root/");
insert_query(query, &picker, cx).await;
- assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a"));
+ assert_eq!(
+ confirm_completion(query, 0, &picker, cx),
+ None,
+ "First entry is `./` and when we confirm completion, it is tabbed below"
+ );
+ assert_eq!(
+ confirm_completion(query, 1, &picker, cx).unwrap(),
+ path!("/root/a"),
+ "Second entry is the first entry of a directory that we want to be completed"
+ );
// Confirm completion for the query "/root/", selecting the second candidate "dir1", since it's a directory, it should add a trailing slash.
let query = path!("/root/");
insert_query(query, &picker, cx).await;
assert_eq!(
- confirm_completion(query, 1, &picker, cx),
+ confirm_completion(query, 2, &picker, cx).unwrap(),
path!("/root/dir1/")
);
let query = path!("/root/a");
insert_query(query, &picker, cx).await;
- assert_eq!(confirm_completion(query, 0, &picker, cx), path!("/root/a"));
+ assert_eq!(
+ confirm_completion(query, 0, &picker, cx).unwrap(),
+ path!("/root/a")
+ );
let query = path!("/root/d");
insert_query(query, &picker, cx).await;
assert_eq!(
- confirm_completion(query, 1, &picker, cx),
+ confirm_completion(query, 1, &picker, cx).unwrap(),
path!("/root/dir2/")
);
let query = path!("/root/dir2");
insert_query(query, &picker, cx).await;
assert_eq!(
- confirm_completion(query, 0, &picker, cx),
+ confirm_completion(query, 0, &picker, cx).unwrap(),
path!("/root/dir2/")
);
let query = path!("/root/dir2/");
insert_query(query, &picker, cx).await;
assert_eq!(
- confirm_completion(query, 0, &picker, cx),
+ confirm_completion(query, 1, &picker, cx).unwrap(),
path!("/root/dir2/c")
);
let query = path!("/root/dir2/");
insert_query(query, &picker, cx).await;
assert_eq!(
- confirm_completion(query, 2, &picker, cx),
+ confirm_completion(query, 3, &picker, cx).unwrap(),
path!("/root/dir2/dir3/")
);
let query = path!("/root/dir2/d");
insert_query(query, &picker, cx).await;
assert_eq!(
- confirm_completion(query, 0, &picker, cx),
+ confirm_completion(query, 0, &picker, cx).unwrap(),
path!("/root/dir2/d")
);
let query = path!("/root/dir2/d");
insert_query(query, &picker, cx).await;
assert_eq!(
- confirm_completion(query, 1, &picker, cx),
+ confirm_completion(query, 1, &picker, cx).unwrap(),
path!("/root/dir2/dir3/")
);
let query = path!("/root/dir2/di");
insert_query(query, &picker, cx).await;
assert_eq!(
- confirm_completion(query, 1, &picker, cx),
+ confirm_completion(query, 1, &picker, cx).unwrap(),
path!("/root/dir2/dir4/")
);
}
@@ -211,42 +234,63 @@ async fn test_open_path_prompt_on_windows(cx: &mut TestAppContext) {
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
- vec!["a", "dir1", "dir2"]
+ vec![".\\", "a", "dir1", "dir2"]
+ );
+ assert_eq!(
+ confirm_completion(query, 0, &picker, cx),
+ None,
+ "First entry is `.\\` and when we confirm completion, it is tabbed below"
+ );
+ assert_eq!(
+ confirm_completion(query, 1, &picker, cx).unwrap(),
+ "C:/root/a",
+ "Second entry is the first entry of a directory that we want to be completed"
);
- assert_eq!(confirm_completion(query, 0, &picker, cx), "C:/root/a");
let query = "C:\\root/";
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
- vec!["a", "dir1", "dir2"]
+ vec![".\\", "a", "dir1", "dir2"]
+ );
+ assert_eq!(
+ confirm_completion(query, 1, &picker, cx).unwrap(),
+ "C:\\root/a"
);
- assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/a");
let query = "C:\\root\\";
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
- vec!["a", "dir1", "dir2"]
+ vec![".\\", "a", "dir1", "dir2"]
+ );
+ assert_eq!(
+ confirm_completion(query, 1, &picker, cx).unwrap(),
+ "C:\\root\\a"
);
- assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root\\a");
// Confirm completion for the query "C:/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
let query = "C:/root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
- assert_eq!(confirm_completion(query, 1, &picker, cx), "C:/root/dir2\\");
+ assert_eq!(
+ confirm_completion(query, 1, &picker, cx).unwrap(),
+ "C:/root/dir2\\"
+ );
let query = "C:\\root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
- assert_eq!(confirm_completion(query, 0, &picker, cx), "C:\\root/dir1\\");
+ assert_eq!(
+ confirm_completion(query, 0, &picker, cx).unwrap(),
+ "C:\\root/dir1\\"
+ );
let query = "C:\\root\\d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
assert_eq!(
- confirm_completion(query, 0, &picker, cx),
+ confirm_completion(query, 0, &picker, cx).unwrap(),
"C:\\root\\dir1\\"
);
}
@@ -276,20 +320,29 @@ async fn test_open_path_prompt_on_windows_with_remote(cx: &mut TestAppContext) {
insert_query(query, &picker, cx).await;
assert_eq!(
collect_match_candidates(&picker, cx),
- vec!["a", "dir1", "dir2"]
+ vec!["./", "a", "dir1", "dir2"]
+ );
+ assert_eq!(
+ confirm_completion(query, 1, &picker, cx).unwrap(),
+ "/root/a"
);
- assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/a");
// Confirm completion for the query "/root/d", selecting the second candidate "dir2", since it's a directory, it should add a trailing slash.
let query = "/root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
- assert_eq!(confirm_completion(query, 1, &picker, cx), "/root/dir2/");
+ assert_eq!(
+ confirm_completion(query, 1, &picker, cx).unwrap(),
+ "/root/dir2/"
+ );
let query = "/root/d";
insert_query(query, &picker, cx).await;
assert_eq!(collect_match_candidates(&picker, cx), vec!["dir1", "dir2"]);
- assert_eq!(confirm_completion(query, 0, &picker, cx), "/root/dir1/");
+ assert_eq!(
+ confirm_completion(query, 0, &picker, cx).unwrap(),
+ "/root/dir1/"
+ );
}
#[gpui::test]
@@ -396,15 +449,13 @@ fn confirm_completion(
select: usize,
picker: &Entity<Picker<OpenPathDelegate>>,
cx: &mut VisualTestContext,
-) -> String {
- picker
- .update_in(cx, |f, window, cx| {
- if f.delegate.selected_index() != select {
- f.delegate.set_selected_index(select, window, cx);
- }
- f.delegate.confirm_completion(query.to_string(), window, cx)
- })
- .unwrap()
+) -> Option<String> {
+ picker.update_in(cx, |f, window, cx| {
+ if f.delegate.selected_index() != select {
+ f.delegate.set_selected_index(select, window, cx);
+ }
+ f.delegate.confirm_completion(query.to_string(), window, cx)
+ })
}
fn collect_match_candidates(
@@ -72,7 +72,7 @@ impl FileIcons {
return maybe_path;
}
}
- return this.get_icon_for_type("default", cx);
+ this.get_icon_for_type("default", cx)
}
fn default_icon_theme(cx: &App) -> Option<Arc<IconTheme>> {
@@ -345,7 +345,7 @@ impl GitRepository for FakeGitRepository {
fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
self.with_state_async(true, move |state| {
- state.branches.insert(name.to_owned());
+ state.branches.insert(name);
Ok(())
})
}
@@ -603,9 +603,9 @@ mod tests {
assert_eq!(
fs.files_with_contents(Path::new("")),
[
- (Path::new("/bar/baz").into(), b"qux".into()),
- (Path::new("/foo/a").into(), b"lorem".into()),
- (Path::new("/foo/b").into(), b"ipsum".into())
+ (Path::new(path!("/bar/baz")).into(), b"qux".into()),
+ (Path::new(path!("/foo/a")).into(), b"lorem".into()),
+ (Path::new(path!("/foo/b")).into(), b"ipsum".into())
]
);
}
@@ -20,6 +20,9 @@ use std::os::fd::{AsFd, AsRawFd};
#[cfg(unix)]
use std::os::unix::fs::{FileTypeExt, MetadataExt};
+#[cfg(any(target_os = "macos", target_os = "freebsd"))]
+use std::mem::MaybeUninit;
+
use async_tar::Archive;
use futures::{AsyncRead, Stream, StreamExt, future::BoxFuture};
use git::repository::{GitRepository, RealGitRepository};
@@ -261,14 +264,15 @@ impl FileHandle for std::fs::File {
};
let fd = self.as_fd();
- let mut path_buf: [libc::c_char; libc::PATH_MAX as usize] = [0; libc::PATH_MAX as usize];
+ let mut path_buf = MaybeUninit::<[u8; libc::PATH_MAX as usize]>::uninit();
let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) };
if result == -1 {
anyhow::bail!("fcntl returned -1".to_string());
}
- let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr()) };
+ // SAFETY: `fcntl` will initialize the path buffer.
+ let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr().cast()) };
let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));
Ok(path)
}
@@ -296,15 +300,16 @@ impl FileHandle for std::fs::File {
};
let fd = self.as_fd();
- let mut kif: libc::kinfo_file = unsafe { std::mem::zeroed() };
+ let mut kif = MaybeUninit::<libc::kinfo_file>::uninit();
kif.kf_structsize = libc::KINFO_FILE_SIZE;
- let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, &mut kif) };
+ let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, kif.as_mut_ptr()) };
if result == -1 {
anyhow::bail!("fcntl returned -1".to_string());
}
- let c_str = unsafe { CStr::from_ptr(kif.kf_path.as_ptr()) };
+ // SAFETY: `fcntl` will initialize the kif.
+ let c_str = unsafe { CStr::from_ptr(kif.assume_init().kf_path.as_ptr()) };
let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));
Ok(path)
}
@@ -420,18 +425,19 @@ impl Fs for RealFs {
async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()> {
#[cfg(windows)]
- if let Ok(Some(metadata)) = self.metadata(path).await {
- if metadata.is_symlink && metadata.is_dir {
- self.remove_dir(
- path,
- RemoveOptions {
- recursive: false,
- ignore_if_not_exists: true,
- },
- )
- .await?;
- return Ok(());
- }
+ if let Ok(Some(metadata)) = self.metadata(path).await
+ && metadata.is_symlink
+ && metadata.is_dir
+ {
+ self.remove_dir(
+ path,
+ RemoveOptions {
+ recursive: false,
+ ignore_if_not_exists: true,
+ },
+ )
+ .await?;
+ return Ok(());
}
match smol::fs::remove_file(path).await {
@@ -467,11 +473,11 @@ impl Fs for RealFs {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
async fn trash_file(&self, path: &Path, _options: RemoveOptions) -> Result<()> {
- if let Ok(Some(metadata)) = self.metadata(path).await {
- if metadata.is_symlink {
- // TODO: trash_file does not support trashing symlinks yet - https://github.com/bilelmoussaoui/ashpd/issues/255
- return self.remove_file(path, RemoveOptions::default()).await;
- }
+ if let Ok(Some(metadata)) = self.metadata(path).await
+ && metadata.is_symlink
+ {
+ // TODO: trash_file does not support trashing symlinks yet - https://github.com/bilelmoussaoui/ashpd/issues/255
+ return self.remove_file(path, RemoveOptions::default()).await;
}
let file = smol::fs::File::open(path).await?;
match trash::trash_file(&file.as_fd()).await {
@@ -494,7 +500,8 @@ impl Fs for RealFs {
};
// todo(windows)
// When new version of `windows-rs` release, make this operation `async`
- let path = SanitizedPath::from(path.canonicalize()?);
+ let path = path.canonicalize()?;
+ let path = SanitizedPath::new(&path);
let path_string = path.to_string();
let file = StorageFile::GetFileFromPathAsync(&HSTRING::from(path_string))?.get()?;
file.DeleteAsync(StorageDeleteOption::Default)?.get()?;
@@ -521,7 +528,8 @@ impl Fs for RealFs {
// todo(windows)
// When new version of `windows-rs` release, make this operation `async`
- let path = SanitizedPath::from(path.canonicalize()?);
+ let path = path.canonicalize()?;
+ let path = SanitizedPath::new(&path);
let path_string = path.to_string();
let folder = StorageFolder::GetFolderFromPathAsync(&HSTRING::from(path_string))?.get()?;
folder.DeleteAsync(StorageDeleteOption::Default)?.get()?;
@@ -624,13 +632,13 @@ impl Fs for RealFs {
async fn is_file(&self, path: &Path) -> bool {
smol::fs::metadata(path)
.await
- .map_or(false, |metadata| metadata.is_file())
+ .is_ok_and(|metadata| metadata.is_file())
}
async fn is_dir(&self, path: &Path) -> bool {
smol::fs::metadata(path)
.await
- .map_or(false, |metadata| metadata.is_dir())
+ .is_ok_and(|metadata| metadata.is_dir())
}
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
@@ -766,24 +774,23 @@ impl Fs for RealFs {
let pending_paths: Arc<Mutex<Vec<PathEvent>>> = Default::default();
let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone()));
- if watcher.add(path).is_err() {
- // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
- if let Some(parent) = path.parent() {
- if let Err(e) = watcher.add(parent) {
- log::warn!("Failed to watch: {e}");
- }
- }
+ // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created.
+ if watcher.add(path).is_err()
+ && let Some(parent) = path.parent()
+ && let Err(e) = watcher.add(parent)
+ {
+ log::warn!("Failed to watch: {e}");
}
// Check if path is a symlink and follow the target parent
- if let Some(mut target) = self.read_link(&path).await.ok() {
+ if let Some(mut target) = self.read_link(path).await.ok() {
// Check if symlink target is relative path, if so make it absolute
- if target.is_relative() {
- if let Some(parent) = path.parent() {
- target = parent.join(target);
- if let Ok(canonical) = self.canonicalize(&target).await {
- target = SanitizedPath::from(canonical).as_path().to_path_buf();
- }
+ if target.is_relative()
+ && let Some(parent) = path.parent()
+ {
+ target = parent.join(target);
+ if let Ok(canonical) = self.canonicalize(&target).await {
+ target = SanitizedPath::new(&canonical).as_path().to_path_buf();
}
}
watcher.add(&target).ok();
@@ -1068,13 +1075,13 @@ impl FakeFsState {
let current_entry = *entry_stack.last()?;
if let FakeFsEntry::Dir { entries, .. } = current_entry {
let entry = entries.get(name.to_str().unwrap())?;
- if path_components.peek().is_some() || follow_symlink {
- if let FakeFsEntry::Symlink { target, .. } = entry {
- let mut target = target.clone();
- target.extend(path_components);
- path = target;
- continue 'outer;
- }
+ if (path_components.peek().is_some() || follow_symlink)
+ && let FakeFsEntry::Symlink { target, .. } = entry
+ {
+ let mut target = target.clone();
+ target.extend(path_components);
+ path = target;
+ continue 'outer;
}
entry_stack.push(entry);
canonical_path = canonical_path.join(name);
@@ -1101,7 +1108,9 @@ impl FakeFsState {
) -> Option<(&mut FakeFsEntry, PathBuf)> {
let canonical_path = self.canonicalize(target, follow_symlink)?;
- let mut components = canonical_path.components();
+ let mut components = canonical_path
+ .components()
+ .skip_while(|component| matches!(component, Component::Prefix(_)));
let Some(Component::RootDir) = components.next() else {
panic!(
"the path {:?} was not canonicalized properly {:?}",
@@ -1566,10 +1575,10 @@ impl FakeFs {
pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
self.with_git_state(dot_git, true, |state| {
- if let Some(first) = branches.first() {
- if state.current_branch_name.is_none() {
- state.current_branch_name = Some(first.to_string())
- }
+ if let Some(first) = branches.first()
+ && state.current_branch_name.is_none()
+ {
+ state.current_branch_name = Some(first.to_string())
}
state
.branches
@@ -1677,7 +1686,7 @@ impl FakeFs {
/// by mutating the head, index, and unmerged state.
pub fn set_status_for_repo(&self, dot_git: &Path, statuses: &[(&Path, FileStatus)]) {
let workdir_path = dot_git.parent().unwrap();
- let workdir_contents = self.files_with_contents(&workdir_path);
+ let workdir_contents = self.files_with_contents(workdir_path);
self.with_git_state(dot_git, true, |state| {
state.index_contents.clear();
state.head_contents.clear();
@@ -1958,7 +1967,7 @@ impl FileHandle for FakeHandle {
};
if state.try_entry(&target, false).is_some() {
- return Ok(target.clone());
+ return Ok(target);
}
anyhow::bail!("fake fd target not found")
}
@@ -2244,7 +2253,7 @@ impl Fs for FakeFs {
async fn open_handle(&self, path: &Path) -> Result<Arc<dyn FileHandle>> {
self.simulate_random_delay().await;
let mut state = self.state.lock();
- let inode = match state.entry(&path)? {
+ let inode = match state.entry(path)? {
FakeFsEntry::File { inode, .. } => *inode,
FakeFsEntry::Dir { inode, .. } => *inode,
_ => unreachable!(),
@@ -2254,7 +2263,7 @@ impl Fs for FakeFs {
async fn load(&self, path: &Path) -> Result<String> {
let content = self.load_internal(path).await?;
- Ok(String::from_utf8(content.clone())?)
+ Ok(String::from_utf8(content)?)
}
async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
@@ -2410,19 +2419,18 @@ impl Fs for FakeFs {
tx,
original_path: path.to_owned(),
fs_state: self.state.clone(),
- prefixes: Mutex::new(vec![path.to_owned()]),
+ prefixes: Mutex::new(vec![path]),
});
(
Box::pin(futures::StreamExt::filter(rx, {
let watcher = watcher.clone();
move |events| {
let result = events.iter().any(|evt_path| {
- let result = watcher
+ watcher
.prefixes
.lock()
.iter()
- .any(|prefix| evt_path.path.starts_with(prefix));
- result
+ .any(|prefix| evt_path.path.starts_with(prefix))
});
let executor = executor.clone();
async move {
@@ -42,7 +42,7 @@ impl Drop for FsWatcher {
impl Watcher for FsWatcher {
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
- let root_path = SanitizedPath::from(path);
+ let root_path = SanitizedPath::new_arc(path);
let tx = self.tx.clone();
let pending_paths = self.pending_path_events.clone();
@@ -70,7 +70,7 @@ impl Watcher for FsWatcher {
.paths
.iter()
.filter_map(|event_path| {
- let event_path = SanitizedPath::from(event_path);
+ let event_path = SanitizedPath::new(event_path);
event_path.starts_with(&root_path).then(|| PathEvent {
path: event_path.as_path().to_path_buf(),
kind,
@@ -159,7 +159,7 @@ impl GlobalWatcher {
path: path.clone(),
};
state.watchers.insert(id, registration_state);
- *state.path_registrations.entry(path.clone()).or_insert(0) += 1;
+ *state.path_registrations.entry(path).or_insert(0) += 1;
Ok(id)
}
@@ -41,10 +41,9 @@ impl Watcher for MacWatcher {
if let Some((watched_path, _)) = handles
.range::<Path, _>((Bound::Unbounded, Bound::Included(path)))
.next_back()
+ && path.starts_with(watched_path)
{
- if path.starts_with(watched_path) {
- return Ok(());
- }
+ return Ok(());
}
let (stream, handle) = EventStream::new(&[path], self.latency);
@@ -178,40 +178,39 @@ impl EventStream {
flags.contains(StreamFlags::USER_DROPPED)
|| flags.contains(StreamFlags::KERNEL_DROPPED)
})
+ && let Some(last_valid_event_id) = state.last_valid_event_id.take()
{
- if let Some(last_valid_event_id) = state.last_valid_event_id.take() {
- fs::FSEventStreamStop(state.stream);
- fs::FSEventStreamInvalidate(state.stream);
- fs::FSEventStreamRelease(state.stream);
-
- let stream_context = fs::FSEventStreamContext {
- version: 0,
- info,
- retain: None,
- release: None,
- copy_description: None,
- };
- let stream = fs::FSEventStreamCreate(
- cf::kCFAllocatorDefault,
- Self::trampoline,
- &stream_context,
- state.paths,
- last_valid_event_id,
- state.latency.as_secs_f64(),
- fs::kFSEventStreamCreateFlagFileEvents
- | fs::kFSEventStreamCreateFlagNoDefer
- | fs::kFSEventStreamCreateFlagWatchRoot,
- );
-
- state.stream = stream;
- fs::FSEventStreamScheduleWithRunLoop(
- state.stream,
- cf::CFRunLoopGetCurrent(),
- cf::kCFRunLoopDefaultMode,
- );
- fs::FSEventStreamStart(state.stream);
- stream_restarted = true;
- }
+ fs::FSEventStreamStop(state.stream);
+ fs::FSEventStreamInvalidate(state.stream);
+ fs::FSEventStreamRelease(state.stream);
+
+ let stream_context = fs::FSEventStreamContext {
+ version: 0,
+ info,
+ retain: None,
+ release: None,
+ copy_description: None,
+ };
+ let stream = fs::FSEventStreamCreate(
+ cf::kCFAllocatorDefault,
+ Self::trampoline,
+ &stream_context,
+ state.paths,
+ last_valid_event_id,
+ state.latency.as_secs_f64(),
+ fs::kFSEventStreamCreateFlagFileEvents
+ | fs::kFSEventStreamCreateFlagNoDefer
+ | fs::kFSEventStreamCreateFlagWatchRoot,
+ );
+
+ state.stream = stream;
+ fs::FSEventStreamScheduleWithRunLoop(
+ state.stream,
+ cf::CFRunLoopGetCurrent(),
+ cf::kCFRunLoopDefaultMode,
+ );
+ fs::FSEventStreamStart(state.stream);
+ stream_restarted = true;
}
if !stream_restarted {
@@ -289,14 +289,12 @@ fn parse_git_blame(output: &str) -> Result<Vec<BlameEntry>> {
}
};
- if done {
- if let Some(entry) = current_entry.take() {
- index.insert(entry.sha, entries.len());
+ if done && let Some(entry) = current_entry.take() {
+ index.insert(entry.sha, entries.len());
- // We only want annotations that have a commit.
- if !entry.sha.is_zero() {
- entries.push(entry);
- }
+ // We only want annotations that have a commit.
+ if !entry.sha.is_zero() {
+ entries.push(entry);
}
}
}
@@ -176,6 +176,7 @@ pub struct CommitSummary {
pub subject: SharedString,
/// This is a unix timestamp
pub commit_timestamp: i64,
+ pub author_name: SharedString,
pub has_parent: bool,
}
@@ -295,10 +296,8 @@ impl GitExcludeOverride {
pub async fn restore_original(&mut self) -> Result<()> {
if let Some(ref original) = self.original_excludes {
smol::fs::write(&self.git_exclude_path, original).await?;
- } else {
- if self.git_exclude_path.exists() {
- smol::fs::remove_file(&self.git_exclude_path).await?;
- }
+ } else if self.git_exclude_path.exists() {
+ smol::fs::remove_file(&self.git_exclude_path).await?;
}
self.added_excludes = None;
@@ -885,7 +884,7 @@ impl GitRepository for RealGitRepository {
let output = new_smol_command(&git_binary_path)
.current_dir(&working_directory)
.envs(env.iter())
- .args(["update-index", "--add", "--cacheinfo", "100644", &sha])
+ .args(["update-index", "--add", "--cacheinfo", "100644", sha])
.arg(path.to_unix_style())
.output()
.await?;
@@ -945,7 +944,7 @@ impl GitRepository for RealGitRepository {
.context("no stdin for git cat-file subprocess")?;
let mut stdin = BufWriter::new(stdin);
for rev in &revs {
- write!(&mut stdin, "{rev}\n")?;
+ writeln!(&mut stdin, "{rev}")?;
}
stdin.flush()?;
drop(stdin);
@@ -986,7 +985,7 @@ impl GitRepository for RealGitRepository {
Ok(working_directory) => working_directory,
Err(e) => return Task::ready(Err(e)),
};
- let args = git_status_args(&path_prefixes);
+ let args = git_status_args(path_prefixes);
log::debug!("Checking for git status in {path_prefixes:?}");
self.executor.spawn(async move {
let output = new_std_command(&git_binary_path)
@@ -1016,6 +1015,7 @@ impl GitRepository for RealGitRepository {
"%(upstream)",
"%(upstream:track)",
"%(committerdate:unix)",
+ "%(authorname)",
"%(contents:subject)",
]
.join("%00");
@@ -1507,12 +1507,11 @@ impl GitRepository for RealGitRepository {
let mut remote_branches = vec![];
let mut add_if_matching = async |remote_head: &str| {
- if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]).await {
- if merge_base.trim() == head {
- if let Some(s) = remote_head.strip_prefix("refs/remotes/") {
- remote_branches.push(s.to_owned().into());
- }
- }
+ if let Ok(merge_base) = git_cmd(&["merge-base", &head, remote_head]).await
+ && merge_base.trim() == head
+ && let Some(s) = remote_head.strip_prefix("refs/remotes/")
+ {
+ remote_branches.push(s.to_owned().into());
}
};
@@ -1634,10 +1633,9 @@ impl GitRepository for RealGitRepository {
Err(error) => {
if let Some(GitBinaryCommandError { status, .. }) =
error.downcast_ref::<GitBinaryCommandError>()
+ && status.code() == Some(1)
{
- if status.code() == Some(1) {
- return Ok(false);
- }
+ return Ok(false);
}
Err(error)
@@ -1926,23 +1924,13 @@ impl GitBinary {
}
#[derive(Error, Debug)]
-#[error("Git command failed: {}", .stderr.trim().if_empty(.stdout.trim()))]
+#[error("Git command failed:\n{stdout}{stderr}\n")]
struct GitBinaryCommandError {
stdout: String,
stderr: String,
status: ExitStatus,
}
-trait StringExt {
- fn if_empty<'a>(&'a self, fallback: &'a str) -> &'a str;
-}
-
-impl StringExt for str {
- fn if_empty<'a>(&'a self, fallback: &'a str) -> &'a str {
- if self.is_empty() { fallback } else { self }
- }
-}
-
async fn run_git_command(
env: Arc<HashMap<String, String>>,
ask_pass: AskPassDelegate,
@@ -2121,6 +2109,7 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
let upstream_name = fields.next().context("no upstream")?.to_string();
let upstream_tracking = parse_upstream_track(fields.next().context("no upstream:track")?)?;
let commiterdate = fields.next().context("no committerdate")?.parse::<i64>()?;
+ let author_name = fields.next().context("no authorname")?.to_string().into();
let subject: SharedString = fields
.next()
.context("no contents:subject")?
@@ -2129,11 +2118,12 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
branches.push(Branch {
is_head: is_current_branch,
- ref_name: ref_name,
+ ref_name,
most_recent_commit: Some(CommitSummary {
sha: head_sha,
subject,
commit_timestamp: commiterdate,
+ author_name: author_name,
has_parent: !parent_sha.is_empty(),
}),
upstream: if upstream_name.is_empty() {
@@ -2151,7 +2141,7 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
}
fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
- if upstream_track == "" {
+ if upstream_track.is_empty() {
return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead: 0,
behind: 0,
@@ -2444,9 +2434,9 @@ mod tests {
fn test_branches_parsing() {
// suppress "help: octal escapes are not supported, `\0` is always null"
#[allow(clippy::octal_escapes)]
- let input = "*\0060964da10574cd9bf06463a53bf6e0769c5c45e\0\0refs/heads/zed-patches\0refs/remotes/origin/zed-patches\0\01733187470\0generated protobuf\n";
+ let input = "*\0060964da10574cd9bf06463a53bf6e0769c5c45e\0\0refs/heads/zed-patches\0refs/remotes/origin/zed-patches\0\01733187470\0John Doe\0generated protobuf\n";
assert_eq!(
- parse_branch_input(&input).unwrap(),
+ parse_branch_input(input).unwrap(),
vec![Branch {
is_head: true,
ref_name: "refs/heads/zed-patches".into(),
@@ -2461,6 +2451,7 @@ mod tests {
sha: "060964da10574cd9bf06463a53bf6e0769c5c45e".into(),
subject: "generated protobuf".into(),
commit_timestamp: 1733187470,
+ author_name: SharedString::new("John Doe"),
has_parent: false,
})
}]
@@ -153,17 +153,11 @@ impl FileStatus {
}
pub fn is_conflicted(self) -> bool {
- match self {
- FileStatus::Unmerged { .. } => true,
- _ => false,
- }
+ matches!(self, FileStatus::Unmerged { .. })
}
pub fn is_ignored(self) -> bool {
- match self {
- FileStatus::Ignored => true,
- _ => false,
- }
+ matches!(self, FileStatus::Ignored)
}
pub fn has_changes(&self) -> bool {
@@ -176,40 +170,31 @@ impl FileStatus {
pub fn is_modified(self) -> bool {
match self {
- FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
- (StatusCode::Modified, _) | (_, StatusCode::Modified) => true,
- _ => false,
- },
+ FileStatus::Tracked(tracked) => matches!(
+ (tracked.index_status, tracked.worktree_status),
+ (StatusCode::Modified, _) | (_, StatusCode::Modified)
+ ),
_ => false,
}
}
pub fn is_created(self) -> bool {
match self {
- FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
- (StatusCode::Added, _) | (_, StatusCode::Added) => true,
- _ => false,
- },
+ FileStatus::Tracked(tracked) => matches!(
+ (tracked.index_status, tracked.worktree_status),
+ (StatusCode::Added, _) | (_, StatusCode::Added)
+ ),
FileStatus::Untracked => true,
_ => false,
}
}
pub fn is_deleted(self) -> bool {
- match self {
- FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
- (StatusCode::Deleted, _) | (_, StatusCode::Deleted) => true,
- _ => false,
- },
- _ => false,
- }
+ matches!(self, FileStatus::Tracked(tracked) if matches!((tracked.index_status, tracked.worktree_status), (StatusCode::Deleted, _) | (_, StatusCode::Deleted)))
}
pub fn is_untracked(self) -> bool {
- match self {
- FileStatus::Untracked => true,
- _ => false,
- }
+ matches!(self, FileStatus::Untracked)
}
pub fn summary(self) -> GitSummary {
@@ -468,7 +453,7 @@ impl FromStr for GitStatus {
Some((path, status))
})
.collect::<Vec<_>>();
- entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
+ entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
// When a file exists in HEAD, is deleted in the index, and exists again in the working copy,
// git produces two lines for it, one reading `D ` (deleted in index, unmodified in working copy)
// and the other reading `??` (untracked). Merge these two into the equivalent of `DA`.
@@ -49,13 +49,13 @@ pub fn register_additional_providers(
pub fn get_host_from_git_remote_url(remote_url: &str) -> Result<String> {
maybe!({
- if let Some(remote_url) = remote_url.strip_prefix("git@") {
- if let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':') {
- return Some(host.to_string());
- }
+ if let Some(remote_url) = remote_url.strip_prefix("git@")
+ && let Some((host, _)) = remote_url.trim_start_matches("git@").split_once(':')
+ {
+ return Some(host.to_string());
}
- Url::parse(&remote_url)
+ Url::parse(remote_url)
.ok()
.and_then(|remote_url| remote_url.host_str().map(|host| host.to_string()))
})
@@ -292,7 +292,7 @@ mod tests {
assert_eq!(
Chromium
- .extract_pull_request(&remote, &message)
+ .extract_pull_request(&remote, message)
.unwrap()
.url
.as_str(),
@@ -474,7 +474,7 @@ mod tests {
assert_eq!(
github
- .extract_pull_request(&remote, &message)
+ .extract_pull_request(&remote, message)
.unwrap()
.url
.as_str(),
@@ -488,6 +488,6 @@ mod tests {
See the original PR, this is a fix.
"#
};
- assert_eq!(github.extract_pull_request(&remote, &message), None);
+ assert_eq!(github.extract_pull_request(&remote, message), None);
}
}
@@ -5,7 +5,7 @@ use git::GitHostingProviderRegistry;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsStore};
+use settings::{Settings, SettingsKey, SettingsStore, SettingsUi};
use url::Url;
use util::ResultExt as _;
@@ -78,7 +78,8 @@ pub struct GitHostingProviderConfig {
pub name: String,
}
-#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(None)]
pub struct GitHostingProviderSettings {
/// The list of custom Git hosting providers.
#[serde(default)]
@@ -86,8 +87,6 @@ pub struct GitHostingProviderSettings {
}
impl Settings for GitHostingProviderSettings {
- const KEY: Option<&'static str> = None;
-
type FileContent = Self;
fn load(sources: settings::SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -90,6 +90,11 @@ impl BlameRenderer for GitBlameRenderer {
sha: blame_entry.sha.to_string().into(),
subject: blame_entry.summary.clone().unwrap_or_default().into(),
commit_timestamp: blame_entry.committer_time.unwrap_or_default(),
+ author_name: blame_entry
+ .committer_name
+ .clone()
+ .unwrap_or_default()
+ .into(),
has_parent: true,
},
repository.downgrade(),
@@ -172,7 +177,7 @@ impl BlameRenderer for GitBlameRenderer {
.clone()
.unwrap_or("<no name>".to_string())
.into(),
- author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(),
+ author_email: blame.author_mail.unwrap_or("".to_string()).into(),
message: details,
};
@@ -186,7 +191,7 @@ impl BlameRenderer for GitBlameRenderer {
.get(0..8)
.map(|sha| sha.to_string().into())
.unwrap_or_else(|| commit_details.sha.clone());
- let full_sha = commit_details.sha.to_string().clone();
+ let full_sha = commit_details.sha.to_string();
let absolute_timestamp = format_local_timestamp(
commit_details.commit_time,
OffsetDateTime::now_utc(),
@@ -229,6 +234,7 @@ impl BlameRenderer for GitBlameRenderer {
.into()
}),
commit_timestamp: commit_details.commit_time.unix_timestamp(),
+ author_name: commit_details.author_name.clone(),
has_parent: false,
};
@@ -374,10 +380,11 @@ impl BlameRenderer for GitBlameRenderer {
sha: blame_entry.sha.to_string().into(),
subject: blame_entry.summary.clone().unwrap_or_default().into(),
commit_timestamp: blame_entry.committer_time.unwrap_or_default(),
+ author_name: blame_entry.committer_name.unwrap_or_default().into(),
has_parent: true,
},
repository.downgrade(),
- workspace.clone(),
+ workspace,
window,
cx,
)
@@ -10,6 +10,8 @@ use gpui::{
};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use project::git_store::Repository;
+use project::project_settings::ProjectSettings;
+use settings::Settings;
use std::sync::Arc;
use time::OffsetDateTime;
use time_format::format_local_timestamp;
@@ -48,7 +50,7 @@ pub fn open(
window: &mut Window,
cx: &mut Context<Workspace>,
) {
- let repository = workspace.project().read(cx).active_repository(cx).clone();
+ let repository = workspace.project().read(cx).active_repository(cx);
let style = BranchListStyle::Modal;
workspace.toggle_modal(window, cx, |window, cx| {
BranchList::new(repository, style, rems(34.), window, cx)
@@ -122,10 +124,13 @@ impl BranchList {
all_branches.retain(|branch| !remote_upstreams.contains(&branch.ref_name));
all_branches.sort_by_key(|branch| {
- branch
- .most_recent_commit
- .as_ref()
- .map(|commit| 0 - commit.commit_timestamp)
+ (
+ !branch.is_head, // Current branch (is_head=true) comes first
+ branch
+ .most_recent_commit
+ .as_ref()
+ .map(|commit| 0 - commit.commit_timestamp),
+ )
});
all_branches
@@ -144,7 +149,7 @@ impl BranchList {
})
.detach_and_log_err(cx);
- let delegate = BranchListDelegate::new(repository.clone(), style);
+ let delegate = BranchListDelegate::new(repository, style);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
@@ -353,7 +358,6 @@ impl PickerDelegate for BranchListDelegate {
};
picker
.update(cx, |picker, _| {
- #[allow(clippy::nonminimal_bool)]
if !query.is_empty()
&& !matches
.first()
@@ -472,9 +476,9 @@ impl PickerDelegate for BranchListDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let entry = &self.matches[ix];
+ let entry = &self.matches.get(ix)?;
- let (commit_time, subject) = entry
+ let (commit_time, author_name, subject) = entry
.branch
.most_recent_commit
.as_ref()
@@ -487,9 +491,10 @@ impl PickerDelegate for BranchListDelegate {
OffsetDateTime::now_utc(),
time_format::TimestampFormat::Relative,
);
- (Some(formatted_time), Some(subject))
+ let author = commit.author_name.clone();
+ (Some(formatted_time), Some(author), Some(subject))
})
- .unwrap_or_else(|| (None, None));
+ .unwrap_or_else(|| (None, None, None));
let icon = if let Some(default_branch) = self.default_branch.clone()
&& entry.is_new
@@ -570,7 +575,19 @@ impl PickerDelegate for BranchListDelegate {
"based off the current branch".to_string()
}
} else {
- subject.unwrap_or("no commits found".into()).to_string()
+ let show_author_name = ProjectSettings::get_global(cx)
+ .git
+ .branch_picker
+ .unwrap_or_default()
+ .show_author_name;
+
+ subject.map_or("no commits found".into(), |subject| {
+ if show_author_name && author_name.is_some() {
+ format!("{} • {}", author_name.unwrap(), subject)
+ } else {
+ subject.to_string()
+ }
+ })
};
Label::new(message)
.size(LabelSize::Small)
@@ -35,7 +35,7 @@ impl ModalContainerProperties {
// Calculate width based on character width
let mut modal_width = 460.0;
- let style = window.text_style().clone();
+ let style = window.text_style();
let font_id = window.text_system().resolve_font(&style.font());
let font_size = style.font_size.to_pixels(window.rem_size());
@@ -135,11 +135,10 @@ impl CommitModal {
.as_ref()
.and_then(|repo| repo.read(cx).head_commit.as_ref())
.is_some()
+ && !git_panel.amend_pending()
{
- if !git_panel.amend_pending() {
- git_panel.set_amend_pending(true, cx);
- git_panel.load_last_commit_message_if_empty(cx);
- }
+ git_panel.set_amend_pending(true, cx);
+ git_panel.load_last_commit_message_if_empty(cx);
}
}
ForceMode::Commit => {
@@ -180,7 +179,7 @@ impl CommitModal {
let commit_editor = git_panel.update(cx, |git_panel, cx| {
git_panel.set_modal_open(true, cx);
- let buffer = git_panel.commit_message_buffer(cx).clone();
+ let buffer = git_panel.commit_message_buffer(cx);
let panel_editor = git_panel.commit_editor.clone();
let project = git_panel.project.clone();
@@ -195,12 +194,12 @@ impl CommitModal {
let commit_message = commit_editor.read(cx).text(cx);
- if let Some(suggested_commit_message) = suggested_commit_message {
- if commit_message.is_empty() {
- commit_editor.update(cx, |editor, cx| {
- editor.set_placeholder_text(suggested_commit_message, cx);
- });
- }
+ if let Some(suggested_commit_message) = suggested_commit_message
+ && commit_message.is_empty()
+ {
+ commit_editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text(&suggested_commit_message, window, cx);
+ });
}
let focus_handle = commit_editor.focus_handle(cx);
@@ -286,7 +285,7 @@ impl CommitModal {
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.when_some(keybinding_target.clone(), |el, keybinding_target| {
- el.context(keybinding_target.clone())
+ el.context(keybinding_target)
})
.when(has_previous_commit, |this| {
this.toggleable_entry(
@@ -392,15 +391,9 @@ impl CommitModal {
});
let focus_handle = self.focus_handle(cx);
- let close_kb_hint =
- if let Some(close_kb) = ui::KeyBinding::for_action(&menu::Cancel, window, cx) {
- Some(
- KeybindingHint::new(close_kb, cx.theme().colors().editor_background)
- .suffix("Cancel"),
- )
- } else {
- None
- };
+ let close_kb_hint = ui::KeyBinding::for_action(&menu::Cancel, window, cx).map(|close_kb| {
+ KeybindingHint::new(close_kb, cx.theme().colors().editor_background).suffix("Cancel")
+ });
h_flex()
.group("commit_editor_footer")
@@ -483,7 +476,7 @@ impl CommitModal {
}),
self.render_git_commit_menu(
ElementId::Name(format!("split-button-right-{}", commit_label).into()),
- Some(focus_handle.clone()),
+ Some(focus_handle),
)
.into_any_element(),
)),
@@ -181,7 +181,7 @@ impl Render for CommitTooltip {
.get(0..8)
.map(|sha| sha.to_string().into())
.unwrap_or_else(|| self.commit.sha.clone());
- let full_sha = self.commit.sha.to_string().clone();
+ let full_sha = self.commit.sha.to_string();
let absolute_timestamp = format_local_timestamp(
self.commit.commit_time,
OffsetDateTime::now_utc(),
@@ -229,6 +229,7 @@ impl Render for CommitTooltip {
.into()
}),
commit_timestamp: self.commit.commit_time.unix_timestamp(),
+ author_name: self.commit.author_name.clone(),
has_parent: false,
};
@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
-use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects};
+use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines};
use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath};
use gpui::{
AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,
@@ -88,11 +88,10 @@ impl CommitView {
let ix = pane.items().position(|item| {
let commit_view = item.downcast::<CommitView>();
commit_view
- .map_or(false, |view| view.read(cx).commit.sha == commit.sha)
+ .is_some_and(|view| view.read(cx).commit.sha == commit.sha)
});
if let Some(ix) = ix {
pane.activate_item(ix, true, true, window, cx);
- return;
} else {
pane.add_item(Box::new(commit_view), true, true, None, window, cx);
}
@@ -160,7 +159,7 @@ impl CommitView {
});
}
- cx.spawn(async move |this, mut cx| {
+ cx.spawn(async move |this, cx| {
for file in commit_diff.files {
let is_deleted = file.new_text.is_none();
let new_text = file.new_text.unwrap_or_default();
@@ -179,9 +178,9 @@ impl CommitView {
worktree_id,
}) as Arc<dyn language::File>;
- let buffer = build_buffer(new_text, file, &language_registry, &mut cx).await?;
+ let buffer = build_buffer(new_text, file, &language_registry, cx).await?;
let buffer_diff =
- build_buffer_diff(old_text, &buffer, &language_registry, &mut cx).await?;
+ build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multibuffer, cx| {
@@ -196,7 +195,7 @@ impl CommitView {
PathKey::namespaced(FILE_NAMESPACE, path),
buffer,
diff_hunk_ranges,
- editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff, cx);
@@ -55,7 +55,7 @@ pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mu
buffers: Default::default(),
});
- let buffers = buffer.read(cx).all_buffers().clone();
+ let buffers = buffer.read(cx).all_buffers();
for buffer in buffers {
buffer_added(editor, buffer, cx);
}
@@ -112,7 +112,7 @@ fn excerpt_for_buffer_updated(
}
fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
- let Some(project) = &editor.project else {
+ let Some(project) = editor.project() else {
return;
};
let git_store = project.read(cx).git_store().clone();
@@ -129,7 +129,7 @@ fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Ed
let subscription = cx.subscribe(&conflict_set, conflicts_updated);
BufferConflicts {
block_ids: Vec::new(),
- conflict_set: conflict_set.clone(),
+ conflict_set,
_subscription: subscription,
}
});
@@ -156,7 +156,7 @@ fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mu
.unwrap()
.buffers
.retain(|buffer_id, buffer| {
- if removed_buffer_ids.contains(&buffer_id) {
+ if removed_buffer_ids.contains(buffer_id) {
removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
false
} else {
@@ -222,12 +222,12 @@ fn conflicts_updated(
let precedes_start = range
.context
.start
- .cmp(&conflict_range.start, &buffer_snapshot)
+ .cmp(&conflict_range.start, buffer_snapshot)
.is_le();
let follows_end = range
.context
.end
- .cmp(&conflict_range.start, &buffer_snapshot)
+ .cmp(&conflict_range.start, buffer_snapshot)
.is_ge();
precedes_start && follows_end
}) else {
@@ -268,12 +268,12 @@ fn conflicts_updated(
let precedes_start = range
.context
.start
- .cmp(&conflict.range.start, &buffer_snapshot)
+ .cmp(&conflict.range.start, buffer_snapshot)
.is_le();
let follows_end = range
.context
.end
- .cmp(&conflict.range.start, &buffer_snapshot)
+ .cmp(&conflict.range.start, buffer_snapshot)
.is_ge();
precedes_start && follows_end
}) else {
@@ -437,7 +437,6 @@ fn render_conflict_buttons(
Button::new("both", "Use Both")
.label_size(LabelSize::Small)
.on_click({
- let editor = editor.clone();
let conflict = conflict.clone();
let ours = conflict.ours.clone();
let theirs = conflict.theirs.clone();
@@ -469,7 +468,7 @@ pub(crate) fn resolve_conflict(
let Some((workspace, project, multibuffer, buffer)) = editor
.update(cx, |editor, cx| {
let workspace = editor.workspace()?;
- let project = editor.project.clone()?;
+ let project = editor.project()?.clone();
let multibuffer = editor.buffer().clone();
let buffer_id = resolved_conflict.ours.end.buffer_id?;
let buffer = multibuffer.read(cx).buffer(buffer_id)?;
@@ -123,7 +123,7 @@ impl FileDiffView {
old_buffer,
new_buffer,
_recalculate_diff_task: cx.spawn(async move |this, cx| {
- while let Ok(_) = buffer_changes_rx.recv().await {
+ while buffer_changes_rx.recv().await.is_ok() {
loop {
let mut timer = cx
.background_executor()
@@ -398,7 +398,7 @@ mod tests {
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
- let (workspace, mut cx) =
+ let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let diff_view = workspace
@@ -417,7 +417,7 @@ mod tests {
// Verify initial diff
assert_state_with_diff(
&diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
- &mut cx,
+ cx,
&unindent(
"
- old line 1
@@ -452,7 +452,7 @@ mod tests {
cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE);
assert_state_with_diff(
&diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
- &mut cx,
+ cx,
&unindent(
"
- old line 1
@@ -487,7 +487,7 @@ mod tests {
cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE);
assert_state_with_diff(
&diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()),
- &mut cx,
+ cx,
&unindent(
"
ˇnew line 1
@@ -31,17 +31,16 @@ use git::{
UnstageAll,
};
use gpui::{
- Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
- DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
- ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point,
- PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
- WeakEntity, actions, anchored, deferred, percentage, uniform_list,
+ Action, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, DismissEvent, Entity,
+ EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
+ ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy,
+ Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred,
+ uniform_list,
};
use itertools::Itertools;
use language::{Buffer, File};
use language_model::{
- ConfiguredModel, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
- LanguageModelRequestMessage, Role,
+ ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use multi_buffer::ExcerptInfo;
@@ -63,8 +62,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime;
use ui::{
- Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu, Scrollbar,
- ScrollbarState, SplitButton, Tooltip, prelude::*,
+ Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize,
+ PopoverMenu, Scrollbar, ScrollbarState, SplitButton, Tooltip, prelude::*,
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::SERIALIZATION_THROTTLE_TIME;
@@ -91,6 +90,8 @@ actions!(
FocusChanges,
/// Toggles automatic co-author suggestions.
ToggleFillCoAuthors,
+ /// Toggles sorting entries by path vs status.
+ ToggleSortByPath,
]
);
@@ -103,7 +104,7 @@ fn prompt<T>(
where
T: IntoEnumIterator + VariantNames + 'static,
{
- let rx = window.prompt(PromptLevel::Info, msg, detail, &T::VARIANTS, cx);
+ let rx = window.prompt(PromptLevel::Info, msg, detail, T::VARIANTS, cx);
cx.spawn(async move |_| Ok(T::iter().nth(rx.await?).unwrap()))
}
@@ -119,6 +120,7 @@ struct GitMenuState {
has_staged_changes: bool,
has_unstaged_changes: bool,
has_new_changes: bool,
+ sort_by_path: bool,
}
fn git_panel_context_menu(
@@ -160,6 +162,16 @@ fn git_panel_context_menu(
"Trash Untracked Files",
TrashUntrackedFiles.boxed_clone(),
)
+ .separator()
+ .entry(
+ if state.sort_by_path {
+ "Sort by Status"
+ } else {
+ "Sort by Path"
+ },
+ Some(Box::new(ToggleSortByPath)),
+ move |window, cx| window.dispatch_action(Box::new(ToggleSortByPath), cx),
+ )
})
}
@@ -351,6 +363,7 @@ pub struct GitPanel {
pending: Vec<PendingOperation>,
pending_commit: Option<Task<()>>,
amend_pending: bool,
+ original_commit_message: Option<String>,
signoff_enabled: bool,
pending_serialization: Task<()>,
pub(crate) project: Entity<Project>,
@@ -388,9 +401,6 @@ pub(crate) fn commit_message_editor(
window: &mut Window,
cx: &mut Context<Editor>,
) -> Editor {
- project.update(cx, |this, cx| {
- this.mark_buffer_as_non_searchable(commit_message_buffer.read(cx).remote_id(), cx);
- });
let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 };
let mut commit_editor = Editor::new(
@@ -410,7 +420,7 @@ pub(crate) fn commit_message_editor(
commit_editor.set_show_wrap_guides(false, cx);
commit_editor.set_show_indent_guides(false, cx);
let placeholder = placeholder.unwrap_or("Enter commit message".into());
- commit_editor.set_placeholder_text(placeholder, cx);
+ commit_editor.set_placeholder_text(&placeholder, window, cx);
commit_editor
}
@@ -426,7 +436,7 @@ impl GitPanel {
let git_store = project.read(cx).git_store().clone();
let active_repository = project.read(cx).active_repository(cx);
- let git_panel = cx.new(|cx| {
+ cx.new(|cx| {
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
@@ -435,10 +445,10 @@ impl GitPanel {
.detach();
let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
- cx.observe_global::<SettingsStore>(move |this, cx| {
+ cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
if is_sort_by_path != was_sort_by_path {
- this.update_visible_entries(cx);
+ this.update_visible_entries(window, cx);
}
was_sort_by_path = is_sort_by_path
})
@@ -535,6 +545,7 @@ impl GitPanel {
pending: Vec::new(),
pending_commit: None,
amend_pending: false,
+ original_commit_message: None,
signoff_enabled: false,
pending_serialization: Task::ready(()),
single_staged_entry: None,
@@ -563,9 +574,7 @@ impl GitPanel {
this.schedule_update(false, window, cx);
this
- });
-
- git_panel
+ })
}
fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -652,14 +661,14 @@ impl GitPanel {
if GitPanelSettings::get_global(cx).sort_by_path {
return self
.entries
- .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
+ .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path))
.ok();
}
if self.conflicted_count > 0 {
let conflicted_start = 1;
if let Ok(ix) = self.entries[conflicted_start..conflicted_start + self.conflicted_count]
- .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
+ .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path))
{
return Some(conflicted_start + ix);
}
@@ -671,7 +680,7 @@ impl GitPanel {
0
} + 1;
if let Ok(ix) = self.entries[tracked_start..tracked_start + self.tracked_count]
- .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
+ .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path))
{
return Some(tracked_start + ix);
}
@@ -687,7 +696,7 @@ impl GitPanel {
0
} + 1;
if let Ok(ix) = self.entries[untracked_start..untracked_start + self.new_count]
- .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
+ .binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(path))
{
return Some(untracked_start + ix);
}
@@ -775,7 +784,7 @@ impl GitPanel {
if window
.focused(cx)
- .map_or(false, |focused| self.focus_handle == focused)
+ .is_some_and(|focused| self.focus_handle == focused)
{
dispatch_context.add("menu");
dispatch_context.add("ChangesList");
@@ -894,9 +903,7 @@ impl GitPanel {
let have_entries = self
.active_repository
.as_ref()
- .map_or(false, |active_repository| {
- active_repository.read(cx).status_summary().count > 0
- });
+ .is_some_and(|active_repository| active_repository.read(cx).status_summary().count > 0);
if have_entries && self.selected_entry.is_none() {
self.selected_entry = Some(1);
self.scroll_to_selected_entry(cx);
@@ -926,19 +933,17 @@ impl GitPanel {
let workspace = self.workspace.upgrade()?;
let git_repo = self.active_repository.as_ref()?;
- if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx) {
- if let Some(project_path) = project_diff.read(cx).active_path(cx) {
- if Some(&entry.repo_path)
- == git_repo
- .read(cx)
- .project_path_to_repo_path(&project_path, cx)
- .as_ref()
- {
- project_diff.focus_handle(cx).focus(window);
- project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx));
- return None;
- }
- }
+ if let Some(project_diff) = workspace.read(cx).active_item_as::<ProjectDiff>(cx)
+ && let Some(project_path) = project_diff.read(cx).active_path(cx)
+ && Some(&entry.repo_path)
+ == git_repo
+ .read(cx)
+ .project_path_to_repo_path(&project_path, cx)
+ .as_ref()
+ {
+ project_diff.focus_handle(cx).focus(window);
+ project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx));
+ return None;
};
self.workspace
@@ -1048,7 +1053,7 @@ impl GitPanel {
let filename = path.path.file_name()?.to_string_lossy();
if !entry.status.is_created() {
- self.perform_checkout(vec![entry.clone()], cx);
+ self.perform_checkout(vec![entry.clone()], window, cx);
} else {
let prompt = prompt(&format!("Trash {}?", filename), None, window, cx);
cx.spawn_in(window, async move |_, cx| {
@@ -1077,7 +1082,12 @@ impl GitPanel {
});
}
- fn perform_checkout(&mut self, entries: Vec<GitStatusEntry>, cx: &mut Context<Self>) {
+ fn perform_checkout(
+ &mut self,
+ entries: Vec<GitStatusEntry>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
let workspace = self.workspace.clone();
let Some(active_repository) = self.active_repository.clone() else {
return;
@@ -1090,7 +1100,7 @@ impl GitPanel {
entries: entries.clone(),
finished: false,
});
- self.update_visible_entries(cx);
+ self.update_visible_entries(window, cx);
let task = cx.spawn(async move |_, cx| {
let tasks: Vec<_> = workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
@@ -1137,16 +1147,16 @@ impl GitPanel {
Ok(())
});
- cx.spawn(async move |this, cx| {
+ cx.spawn_in(window, async move |this, cx| {
let result = task.await;
- this.update(cx, |this, cx| {
+ this.update_in(cx, |this, window, cx| {
for pending in this.pending.iter_mut() {
if pending.op_id == op_id {
pending.finished = true;
if result.is_err() {
pending.target_status = TargetStatus::Unchanged;
- this.update_visible_entries(cx);
+ this.update_visible_entries(window, cx);
}
break;
}
@@ -1202,16 +1212,13 @@ impl GitPanel {
window,
cx,
);
- cx.spawn(async move |this, cx| match prompt.await {
- Ok(RestoreCancel::RestoreTrackedFiles) => {
- this.update(cx, |this, cx| {
- this.perform_checkout(entries, cx);
+ cx.spawn_in(window, async move |this, cx| {
+ if let Ok(RestoreCancel::RestoreTrackedFiles) = prompt.await {
+ this.update_in(cx, |this, window, cx| {
+ this.perform_checkout(entries, window, cx);
})
.ok();
}
- _ => {
- return;
- }
})
.detach();
}
@@ -1341,10 +1348,10 @@ impl GitPanel {
.iter()
.filter_map(|entry| entry.status_entry())
.filter(|status_entry| {
- section.contains(&status_entry, repository)
+ section.contains(status_entry, repository)
&& status_entry.staging.as_bool() != Some(goal_staged_state)
})
- .map(|status_entry| status_entry.clone())
+ .cloned()
.collect::<Vec<_>>();
(goal_staged_state, entries)
@@ -1476,7 +1483,6 @@ impl GitPanel {
.read(cx)
.as_singleton()
.unwrap()
- .clone()
}
fn toggle_staged_for_selected(
@@ -1642,13 +1648,12 @@ impl GitPanel {
fn has_commit_message(&self, cx: &mut Context<Self>) -> bool {
let text = self.commit_editor.read(cx).text(cx);
if !text.trim().is_empty() {
- return true;
+ true
} else if text.is_empty() {
- return self
- .suggest_commit_message(cx)
- .is_some_and(|text| !text.trim().is_empty());
+ self.suggest_commit_message(cx)
+ .is_some_and(|text| !text.trim().is_empty())
} else {
- return false;
+ false
}
}
@@ -1727,6 +1732,7 @@ impl GitPanel {
Ok(()) => {
this.commit_editor
.update(cx, |editor, cx| editor.clear(window, cx));
+ this.original_commit_message = None;
}
Err(e) => this.show_error_toast("commit", e, cx),
}
@@ -1737,7 +1743,7 @@ impl GitPanel {
self.pending_commit = Some(task);
}
- fn uncommit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ pub(crate) fn uncommit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(repo) = self.active_repository.clone() else {
return;
};
@@ -1833,7 +1839,9 @@ impl GitPanel {
let git_status_entry = if let Some(staged_entry) = &self.single_staged_entry {
Some(staged_entry)
- } else if let Some(single_tracked_entry) = &self.single_tracked_entry {
+ } else if self.total_staged_count() == 0
+ && let Some(single_tracked_entry) = &self.single_tracked_entry
+ {
Some(single_tracked_entry)
} else {
None
@@ -1869,13 +1877,17 @@ impl GitPanel {
/// Generates a commit message using an LLM.
pub fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
- if !self.can_commit() || DisableAiSettings::get_global(cx).disable_ai {
+ if !self.can_commit()
+ || DisableAiSettings::get_global(cx).disable_ai
+ || !agent_settings::AgentSettings::get_global(cx).enabled
+ {
return;
}
- let model = match current_language_model(cx) {
- Some(value) => value,
- None => return,
+ let Some(ConfiguredModel { provider, model }) =
+ LanguageModelRegistry::read_global(cx).commit_message_model()
+ else {
+ return;
};
let Some(repo) = self.active_repository.as_ref() else {
@@ -1900,6 +1912,16 @@ impl GitPanel {
this.generate_commit_message_task.take();
});
+ if let Some(task) = cx.update(|cx| {
+ if !provider.is_authenticated(cx) {
+ Some(provider.authenticate(cx))
+ } else {
+ None
+ }
+ })? {
+ task.await.log_err();
+ };
+
let mut diff_text = match diff.await {
Ok(result) => match result {
Ok(text) => text,
@@ -1950,7 +1972,7 @@ impl GitPanel {
thinking_allowed: false,
};
- let stream = model.stream_completion_text(request, &cx);
+ let stream = model.stream_completion_text(request, cx);
match stream.await {
Ok(mut messages) => {
if !text_empty {
@@ -2086,6 +2108,7 @@ impl GitPanel {
files: false,
directories: true,
multiple: false,
+ prompt: Some("Select as Repository Destination".into()),
});
let workspace = self.workspace.clone();
@@ -2183,7 +2206,7 @@ impl GitPanel {
let worktree = if worktrees.len() == 1 {
Task::ready(Some(worktrees.first().unwrap().clone()))
- } else if worktrees.len() == 0 {
+ } else if worktrees.is_empty() {
let result = window.prompt(
PromptLevel::Warning,
"Unable to initialize a git repository",
@@ -2511,10 +2534,11 @@ impl GitPanel {
new_co_authors.push((name.clone(), email.clone()))
}
}
- if !project.is_local() && !project.is_read_only(cx) {
- if let Some(local_committer) = self.local_committer(room, cx) {
- new_co_authors.push(local_committer);
- }
+ if !project.is_local()
+ && !project.is_read_only(cx)
+ && let Some(local_committer) = self.local_committer(room, cx)
+ {
+ new_co_authors.push(local_committer);
}
new_co_authors
}
@@ -2541,6 +2565,24 @@ impl GitPanel {
cx.notify();
}
+ fn toggle_sort_by_path(
+ &mut self,
+ _: &ToggleSortByPath,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let current_setting = GitPanelSettings::get_global(cx).sort_by_path;
+ if let Some(workspace) = self.workspace.upgrade() {
+ let workspace = workspace.read(cx);
+ let fs = workspace.app_state().fs.clone();
+ cx.update_global::<SettingsStore, _>(|store, _cx| {
+ store.update_settings_file::<GitPanelSettings>(fs, move |settings, _cx| {
+ settings.sort_by_path = Some(!current_setting);
+ });
+ });
+ }
+ }
+
fn fill_co_authors(&mut self, message: &mut String, cx: &mut Context<Self>) {
const CO_AUTHOR_PREFIX: &str = "Co-authored-by: ";
@@ -2605,7 +2647,7 @@ impl GitPanel {
if clear_pending {
git_panel.clear_pending();
}
- git_panel.update_visible_entries(cx);
+ git_panel.update_visible_entries(window, cx);
git_panel.update_scrollbar_properties(window, cx);
})
.ok();
@@ -2658,7 +2700,7 @@ impl GitPanel {
self.pending.retain(|v| !v.finished)
}
- fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
+ fn update_visible_entries(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let bulk_staging = self.bulk_staging.take();
let last_staged_path_prev_index = bulk_staging
.as_ref()
@@ -2753,35 +2795,34 @@ impl GitPanel {
for pending in self.pending.iter() {
if pending.target_status == TargetStatus::Staged {
pending_staged_count += pending.entries.len();
- last_pending_staged = pending.entries.iter().next().cloned();
+ last_pending_staged = pending.entries.first().cloned();
}
- if let Some(single_staged) = &single_staged_entry {
- if pending
+ if let Some(single_staged) = &single_staged_entry
+ && pending
.entries
.iter()
.any(|entry| entry.repo_path == single_staged.repo_path)
- {
- pending_status_for_single_staged = Some(pending.target_status);
- }
+ {
+ pending_status_for_single_staged = Some(pending.target_status);
}
}
- if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 {
+ if conflict_entries.is_empty() && staged_count == 1 && pending_staged_count == 0 {
match pending_status_for_single_staged {
Some(TargetStatus::Staged) | None => {
self.single_staged_entry = single_staged_entry;
}
_ => {}
}
- } else if conflict_entries.len() == 0 && pending_staged_count == 1 {
+ } else if conflict_entries.is_empty() && pending_staged_count == 1 {
self.single_staged_entry = last_pending_staged;
}
- if conflict_entries.len() == 0 && changed_entries.len() == 1 {
+ if conflict_entries.is_empty() && changed_entries.len() == 1 {
self.single_tracked_entry = changed_entries.first().cloned();
}
- if conflict_entries.len() > 0 {
+ if !conflict_entries.is_empty() {
self.entries.push(GitListEntry::Header(GitHeaderEntry {
header: Section::Conflict,
}));
@@ -2789,7 +2830,7 @@ impl GitPanel {
.extend(conflict_entries.into_iter().map(GitListEntry::Status));
}
- if changed_entries.len() > 0 {
+ if !changed_entries.is_empty() {
if !sort_by_path {
self.entries.push(GitListEntry::Header(GitHeaderEntry {
header: Section::Tracked,
@@ -2798,7 +2839,7 @@ impl GitPanel {
self.entries
.extend(changed_entries.into_iter().map(GitListEntry::Status));
}
- if new_entries.len() > 0 {
+ if !new_entries.is_empty() {
self.entries.push(GitListEntry::Header(GitHeaderEntry {
header: Section::New,
}));
@@ -2834,7 +2875,7 @@ impl GitPanel {
let placeholder_text = suggested_commit_message.unwrap_or("Enter commit message".into());
self.commit_editor.update(cx, |editor, cx| {
- editor.set_placeholder_text(Arc::from(placeholder_text), cx)
+ editor.set_placeholder_text(&placeholder_text, window, cx)
});
cx.notify();
@@ -2937,8 +2978,7 @@ impl GitPanel {
.matches(git::repository::REMOTE_CANCELLED_BY_USER)
.next()
.is_some()
- {
- return; // Hide the cancelled by user message
+ { // Hide the cancelled by user message
} else {
workspace.update(cx, |workspace, cx| {
let workspace_weak = cx.weak_entity();
@@ -2992,9 +3032,7 @@ impl GitPanel {
let status_toast = StatusToast::new(message, cx, move |this, _cx| {
use remote_output::SuccessStyle::*;
match style {
- Toast { .. } => {
- this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted))
- }
+ Toast => this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)),
ToastWithLog { output } => this
.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted))
.action("View Log", move |window, cx| {
@@ -3079,6 +3117,7 @@ impl GitPanel {
has_staged_changes,
has_unstaged_changes,
has_new_changes,
+ sort_by_path: GitPanelSettings::get_global(cx).sort_by_path,
},
window,
cx,
@@ -3091,32 +3130,37 @@ impl GitPanel {
&self,
cx: &Context<Self>,
) -> Option<AnyElement> {
- current_language_model(cx).is_some().then(|| {
- if self.generate_commit_message_task.is_some() {
- return h_flex()
+ if !agent_settings::AgentSettings::get_global(cx).enabled
+ || DisableAiSettings::get_global(cx).disable_ai
+ || LanguageModelRegistry::read_global(cx)
+ .commit_message_model()
+ .is_none()
+ {
+ return None;
+ }
+
+ if self.generate_commit_message_task.is_some() {
+ return Some(
+ h_flex()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(percentage(delta)))
- },
- ),
+ .with_rotate_animation(2),
)
.child(
Label::new("Generating Commit...")
.size(LabelSize::Small)
.color(Color::Muted),
)
- .into_any_element();
- }
+ .into_any_element(),
+ );
+ }
- let can_commit = self.can_commit();
- let editor_focus_handle = self.commit_editor.focus_handle(cx);
+ let can_commit = self.can_commit();
+ let editor_focus_handle = self.commit_editor.focus_handle(cx);
+ Some(
IconButton::new("generate-commit-message", IconName::AiEdit)
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Muted)
@@ -3137,8 +3181,8 @@ impl GitPanel {
.on_click(cx.listener(move |this, _event, _window, cx| {
this.generate_commit_message(cx);
}))
- .into_any_element()
- })
+ .into_any_element(),
+ )
}
pub(crate) fn render_co_authors(&self, cx: &Context<Self>) -> Option<AnyElement> {
@@ -3215,7 +3259,7 @@ impl GitPanel {
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.when_some(keybinding_target.clone(), |el, keybinding_target| {
- el.context(keybinding_target.clone())
+ el.context(keybinding_target)
})
.when(has_previous_commit, |this| {
this.toggleable_entry(
@@ -3271,12 +3315,10 @@ impl GitPanel {
} else {
"Amend Tracked"
}
+ } else if self.has_staged_changes() {
+ "Commit"
} else {
- if self.has_staged_changes() {
- "Commit"
- } else {
- "Commit Tracked"
- }
+ "Commit Tracked"
}
}
@@ -3397,7 +3439,7 @@ impl GitPanel {
let enable_coauthors = self.render_co_authors(cx);
let editor_focus_handle = self.commit_editor.focus_handle(cx);
- let expand_tooltip_focus_handle = editor_focus_handle.clone();
+ let expand_tooltip_focus_handle = editor_focus_handle;
let branch = active_repository.read(cx).branch.clone();
let head_commit = active_repository.read(cx).head_commit.clone();
@@ -3410,7 +3452,7 @@ impl GitPanel {
* MAX_PANEL_EDITOR_LINES
+ gap;
- let git_panel = cx.entity().clone();
+ let git_panel = cx.entity();
let display_name = SharedString::from(Arc::from(
active_repository
.read(cx)
@@ -3426,7 +3468,7 @@ impl GitPanel {
display_name,
branch,
head_commit,
- Some(git_panel.clone()),
+ Some(git_panel),
))
.child(
panel_editor_container(window, cx)
@@ -3577,7 +3619,7 @@ impl GitPanel {
}),
self.render_git_commit_menu(
ElementId::Name(format!("split-button-right-{}", title).into()),
- Some(commit_tooltip_focus_handle.clone()),
+ Some(commit_tooltip_focus_handle),
cx,
)
.into_any_element(),
@@ -3643,7 +3685,7 @@ impl GitPanel {
CommitView::open(
commit.clone(),
repo.clone(),
- workspace.clone().clone(),
+ workspace.clone(),
window,
cx,
);
@@ -4128,6 +4170,7 @@ impl GitPanel {
has_staged_changes: self.has_staged_changes(),
has_unstaged_changes: self.has_unstaged_changes(),
has_new_changes: self.new_count > 0,
+ sort_by_path: GitPanelSettings::get_global(cx).sort_by_path,
},
window,
cx,
@@ -4351,7 +4394,7 @@ impl GitPanel {
}
})
.child(
- self.entry_label(display_name.clone(), label_color)
+ self.entry_label(display_name, label_color)
.when(status.is_deleted(), |this| this.strikethrough()),
),
)
@@ -4367,6 +4410,22 @@ impl GitPanel {
}
pub fn set_amend_pending(&mut self, value: bool, cx: &mut Context<Self>) {
+ if value && !self.amend_pending {
+ let current_message = self.commit_message_buffer(cx).read(cx).text();
+ self.original_commit_message = if current_message.trim().is_empty() {
+ None
+ } else {
+ Some(current_message)
+ };
+ } else if !value && self.amend_pending {
+ let message = self.original_commit_message.take().unwrap_or_default();
+ self.commit_message_buffer(cx).update(cx, |buffer, cx| {
+ let start = buffer.anchor_before(0);
+ let end = buffer.anchor_after(buffer.len());
+ buffer.edit([(start..end, message)], None, cx);
+ });
+ }
+
self.amend_pending = value;
self.serialize(cx);
cx.notify();
@@ -4472,24 +4531,10 @@ impl GitPanel {
}
}
-fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
- let is_enabled = agent_settings::AgentSettings::get_global(cx).enabled
- && !DisableAiSettings::get_global(cx).disable_ai;
-
- is_enabled
- .then(|| {
- let ConfiguredModel { provider, model } =
- LanguageModelRegistry::read_global(cx).commit_message_model()?;
-
- provider.is_authenticated(cx).then(|| model)
- })
- .flatten()
-}
-
impl Render for GitPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let project = self.project.read(cx);
- let has_entries = self.entries.len() > 0;
+ let has_entries = !self.entries.is_empty();
let room = self
.workspace
.upgrade()
@@ -4497,7 +4542,7 @@ impl Render for GitPanel {
let has_write_access = self.has_write_access(cx);
- let has_co_authors = room.map_or(false, |room| {
+ let has_co_authors = room.is_some_and(|room| {
self.load_local_committer(cx);
let room = room.read(cx);
room.remote_participants()
@@ -4539,6 +4584,7 @@ impl Render for GitPanel {
.when(has_write_access && has_co_authors, |git_panel| {
git_panel.on_action(cx.listener(Self::toggle_fill_co_authors))
})
+ .on_action(cx.listener(Self::toggle_sort_by_path))
.on_hover(cx.listener(move |this, hovered, window, cx| {
if *hovered {
this.horizontal_scrollbar.show(cx);
@@ -4617,7 +4663,7 @@ impl editor::Addon for GitPanelAddon {
git_panel
.read(cx)
- .render_buffer_header_controls(&git_panel, &file, window, cx)
+ .render_buffer_header_controls(&git_panel, file, window, cx)
}
}
@@ -4700,7 +4746,7 @@ impl GitPanelMessageTooltip {
author_email: details.author_email.clone(),
commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?,
message: Some(ParsedCommitMessage {
- message: details.message.clone(),
+ message: details.message,
..Default::default()
}),
};
@@ -4813,12 +4859,10 @@ impl RenderOnce for PanelRepoFooter {
// ideally, show the whole branch and repo names but
// when we can't, use a budget to allocate space between the two
- let (repo_display_len, branch_display_len) = if branch_actual_len + repo_actual_len
- <= LABEL_CHARACTER_BUDGET
- {
- (repo_actual_len, branch_actual_len)
- } else {
- if branch_actual_len <= MAX_BRANCH_LEN {
+ let (repo_display_len, branch_display_len) =
+ if branch_actual_len + repo_actual_len <= LABEL_CHARACTER_BUDGET {
+ (repo_actual_len, branch_actual_len)
+ } else if branch_actual_len <= MAX_BRANCH_LEN {
let repo_space = (LABEL_CHARACTER_BUDGET - branch_actual_len).min(MAX_REPO_LEN);
(repo_space, branch_actual_len)
} else if repo_actual_len <= MAX_REPO_LEN {
@@ -4826,8 +4870,7 @@ impl RenderOnce for PanelRepoFooter {
(repo_actual_len, branch_space)
} else {
(MAX_REPO_LEN, MAX_BRANCH_LEN)
- }
- };
+ };
let truncated_repo_name = if repo_actual_len <= repo_display_len {
active_repo_name.to_string()
@@ -4836,7 +4879,7 @@ impl RenderOnce for PanelRepoFooter {
};
let truncated_branch_name = if branch_actual_len <= branch_display_len {
- branch_name.to_string()
+ branch_name
} else {
util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len)
};
@@ -4849,7 +4892,7 @@ impl RenderOnce for PanelRepoFooter {
let repo_selector = PopoverMenu::new("repository-switcher")
.menu({
- let project = project.clone();
+ let project = project;
move |window, cx| {
let project = project.clone()?;
Some(cx.new(|cx| RepositorySelector::new(project, rems(16.), window, cx)))
@@ -4979,6 +5022,7 @@ impl Component for PanelRepoFooter {
sha: "abc123".into(),
subject: "Modify stuff".into(),
commit_timestamp: 1710932954,
+ author_name: "John Doe".into(),
has_parent: true,
}),
}
@@ -4996,6 +5040,7 @@ impl Component for PanelRepoFooter {
sha: "abc123".into(),
subject: "Modify stuff".into(),
commit_timestamp: 1710932954,
+ author_name: "John Doe".into(),
has_parent: true,
}),
}
@@ -5020,10 +5065,7 @@ impl Component for PanelRepoFooter {
div()
.w(example_width)
.overflow_hidden()
- .child(PanelRepoFooter::new_preview(
- active_repository(1).clone(),
- None,
- ))
+ .child(PanelRepoFooter::new_preview(active_repository(1), None))
.into_any_element(),
),
single_example(
@@ -5032,7 +5074,7 @@ impl Component for PanelRepoFooter {
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
- active_repository(2).clone(),
+ active_repository(2),
Some(branch(unknown_upstream)),
))
.into_any_element(),
@@ -5043,7 +5085,7 @@ impl Component for PanelRepoFooter {
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
- active_repository(3).clone(),
+ active_repository(3),
Some(branch(no_remote_upstream)),
))
.into_any_element(),
@@ -5054,7 +5096,7 @@ impl Component for PanelRepoFooter {
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
- active_repository(4).clone(),
+ active_repository(4),
Some(branch(not_ahead_or_behind_upstream)),
))
.into_any_element(),
@@ -5065,7 +5107,7 @@ impl Component for PanelRepoFooter {
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
- active_repository(5).clone(),
+ active_repository(5),
Some(branch(behind_upstream)),
))
.into_any_element(),
@@ -5076,7 +5118,7 @@ impl Component for PanelRepoFooter {
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
- active_repository(6).clone(),
+ active_repository(6),
Some(branch(ahead_of_upstream)),
))
.into_any_element(),
@@ -5087,7 +5129,7 @@ impl Component for PanelRepoFooter {
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
- active_repository(7).clone(),
+ active_repository(7),
Some(branch(ahead_and_behind_upstream)),
))
.into_any_element(),
@@ -5258,7 +5300,7 @@ mod tests {
project
.read(cx)
.worktrees(cx)
- .nth(0)
+ .next()
.unwrap()
.read(cx)
.as_local()
@@ -5383,7 +5425,7 @@ mod tests {
project
.read(cx)
.worktrees(cx)
- .nth(0)
+ .next()
.unwrap()
.read(cx)
.as_local()
@@ -5434,7 +5476,7 @@ mod tests {
project
.read(cx)
.worktrees(cx)
- .nth(0)
+ .next()
.unwrap()
.read(cx)
.as_local()
@@ -5483,7 +5525,7 @@ mod tests {
project
.read(cx)
.worktrees(cx)
- .nth(0)
+ .next()
.unwrap()
.read(cx)
.as_local()
@@ -5518,4 +5560,73 @@ mod tests {
],
);
}
+
+ #[gpui::test]
+ async fn test_amend_commit_message_handling(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/root",
+ json!({
+ "project": {
+ ".git": {},
+ "src": {
+ "main.rs": "fn main() {}"
+ }
+ }
+ }),
+ )
+ .await;
+
+ fs.set_status_for_repo(
+ Path::new(path!("/root/project/.git")),
+ &[(Path::new("src/main.rs"), StatusCode::Modified.worktree())],
+ );
+
+ let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await;
+ let workspace =
+ cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+ let panel = workspace.update(cx, GitPanel::new).unwrap();
+
+ // Test: User has commit message, enables amend (saves message), then disables (restores message)
+ panel.update(cx, |panel, cx| {
+ panel.commit_message_buffer(cx).update(cx, |buffer, cx| {
+ let start = buffer.anchor_before(0);
+ let end = buffer.anchor_after(buffer.len());
+ buffer.edit([(start..end, "Initial commit message")], None, cx);
+ });
+
+ panel.set_amend_pending(true, cx);
+ assert!(panel.original_commit_message.is_some());
+
+ panel.set_amend_pending(false, cx);
+ let current_message = panel.commit_message_buffer(cx).read(cx).text();
+ assert_eq!(current_message, "Initial commit message");
+ assert!(panel.original_commit_message.is_none());
+ });
+
+ // Test: User has empty commit message, enables amend, then disables (clears message)
+ panel.update(cx, |panel, cx| {
+ panel.commit_message_buffer(cx).update(cx, |buffer, cx| {
+ let start = buffer.anchor_before(0);
+ let end = buffer.anchor_after(buffer.len());
+ buffer.edit([(start..end, "")], None, cx);
+ });
+
+ panel.set_amend_pending(true, cx);
+ assert!(panel.original_commit_message.is_none());
+
+ panel.commit_message_buffer(cx).update(cx, |buffer, cx| {
+ let start = buffer.anchor_before(0);
+ let end = buffer.anchor_after(buffer.len());
+ buffer.edit([(start..end, "Previous commit message")], None, cx);
+ });
+
+ panel.set_amend_pending(false, cx);
+ let current_message = panel.commit_message_buffer(cx).read(cx).text();
+ assert_eq!(current_message, "");
+ });
+ }
}
@@ -2,7 +2,7 @@ use editor::ShowScrollbar;
use gpui::Pixels;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use workspace::dock::DockPosition;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -36,7 +36,8 @@ pub enum StatusStyle {
LabelColor,
}
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
+#[settings_key(key = "git_panel")]
pub struct GitPanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
@@ -90,8 +91,6 @@ pub struct GitPanelSettings {
}
impl Settings for GitPanelSettings {
- const KEY: Option<&'static str> = Some("git_panel");
-
type FileContent = GitPanelSettingsContent;
fn load(
@@ -3,7 +3,7 @@ use std::any::Any;
use ::settings::Settings;
use command_palette_hooks::CommandPaletteFilter;
use commit_modal::CommitModal;
-use editor::{Editor, EditorElement, EditorStyle, actions::DiffClipboardWithSelectionData};
+use editor::{Editor, actions::DiffClipboardWithSelectionData};
use ui::{
Headline, HeadlineSize, Icon, IconName, IconSize, IntoElement, ParentElement, Render, Styled,
StyledExt, div, h_flex, rems, v_flex,
@@ -18,13 +18,12 @@ use git::{
use git_panel_settings::GitPanelSettings;
use gpui::{
Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, SharedString,
- TextStyle, Window, actions,
+ Window, actions,
};
use menu::{Cancel, Confirm};
use onboarding::GitOnboardingModal;
use project::git_store::Repository;
use project_diff::ProjectDiff;
-use theme::ThemeSettings;
use ui::prelude::*;
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr};
use zed_actions;
@@ -158,6 +157,14 @@ pub fn init(cx: &mut App) {
panel.unstage_all(action, window, cx);
});
});
+ workspace.register_action(|workspace, _: &git::Uncommit, window, cx| {
+ let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
+ return;
+ };
+ panel.update(cx, |panel, cx| {
+ panel.uncommit(window, cx);
+ })
+ });
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_action_types(&[
zed_actions::OpenGitIntegrationOnboarding.type_id(),
@@ -373,12 +380,12 @@ fn render_remote_button(
}
(0, 0) => None,
(ahead, 0) => Some(remote_button::render_push_button(
- keybinding_target.clone(),
+ keybinding_target,
id,
ahead,
)),
(ahead, behind) => Some(remote_button::render_pull_button(
- keybinding_target.clone(),
+ keybinding_target,
id,
ahead,
behind,
@@ -553,16 +560,9 @@ mod remote_button {
let command = command.into();
if let Some(handle) = focus_handle {
- Tooltip::with_meta_in(
- label.clone(),
- Some(action),
- command.clone(),
- &handle,
- window,
- cx,
- )
+ Tooltip::with_meta_in(label, Some(action), command, &handle, window, cx)
} else {
- Tooltip::with_meta(label.clone(), Some(action), command.clone(), window, cx)
+ Tooltip::with_meta(label, Some(action), command, window, cx)
}
}
@@ -585,7 +585,7 @@ mod remote_button {
Some(ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.when_some(keybinding_target.clone(), |el, keybinding_target| {
- el.context(keybinding_target.clone())
+ el.context(keybinding_target)
})
.action("Fetch", git::Fetch.boxed_clone())
.action("Fetch From", git::FetchFrom.boxed_clone())
@@ -764,7 +764,7 @@ impl GitCloneModal {
pub fn show(panel: Entity<GitPanel>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let repo_input = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("Enter repository", cx);
+ editor.set_placeholder_text("Enter repository URL…", window, cx);
editor
});
let focus_handle = repo_input.focus_handle(cx);
@@ -777,46 +777,6 @@ impl GitCloneModal {
focus_handle,
}
}
-
- fn render_editor(&self, window: &Window, cx: &App) -> impl IntoElement {
- let settings = ThemeSettings::get_global(cx);
- let theme = cx.theme();
-
- let text_style = TextStyle {
- color: cx.theme().colors().text,
- font_family: settings.buffer_font.family.clone(),
- font_features: settings.buffer_font.features.clone(),
- font_size: settings.buffer_font_size(cx).into(),
- font_weight: settings.buffer_font.weight,
- line_height: relative(settings.buffer_line_height.value()),
- background_color: Some(theme.colors().editor_background),
- ..Default::default()
- };
-
- let element = EditorElement::new(
- &self.repo_input,
- EditorStyle {
- background: theme.colors().editor_background,
- local_player: theme.players().local(),
- text: text_style,
- ..Default::default()
- },
- );
-
- div()
- .rounded_md()
- .p_1()
- .border_1()
- .border_color(theme.colors().border_variant)
- .when(
- self.repo_input
- .focus_handle(cx)
- .contains_focused(window, cx),
- |this| this.border_color(theme.colors().border_focused),
- )
- .child(element)
- .bg(theme.colors().editor_background)
- }
}
impl Focusable for GitCloneModal {
@@ -826,12 +786,42 @@ impl Focusable for GitCloneModal {
}
impl Render for GitCloneModal {
- fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
- .size_full()
- .w(rems(34.))
.elevation_3(cx)
- .child(self.render_editor(window, cx))
+ .w(rems(34.))
+ .flex_1()
+ .overflow_hidden()
+ .child(
+ div()
+ .w_full()
+ .p_2()
+ .border_b_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(self.repo_input.clone()),
+ )
+ .child(
+ h_flex()
+ .w_full()
+ .p_2()
+ .gap_0p5()
+ .rounded_b_sm()
+ .bg(cx.theme().colors().editor_background)
+ .child(
+ Label::new("Clone a repository from GitHub or other sources.")
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ )
+ .child(
+ Button::new("learn-more", "Learn More")
+ .label_size(LabelSize::Small)
+ .icon(IconName::ArrowUpRight)
+ .icon_size(IconSize::XSmall)
+ .on_click(|_, _, cx| {
+ cx.open_url("https://github.com/git-guides/git-clone");
+ }),
+ ),
+ )
.on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
cx.emit(DismissEvent);
}))
@@ -152,7 +152,7 @@ impl PickerDelegate for PickerPromptDelegate {
.all_options
.iter()
.enumerate()
- .map(|(ix, option)| StringMatchCandidate::new(ix, &option))
+ .map(|(ix, option)| StringMatchCandidate::new(ix, option))
.collect::<Vec<StringMatchCandidate>>()
});
let Some(candidates) = candidates.log_err() else {
@@ -216,7 +216,7 @@ impl PickerDelegate for PickerPromptDelegate {
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let hit = &self.matches[ix];
+ let hit = &self.matches.get(ix)?;
let shortened_option = util::truncate_and_trailoff(&hit.string, self.max_match_length);
Some(
@@ -10,6 +10,7 @@ use collections::HashSet;
use editor::{
Editor, EditorEvent, SelectionEffects,
actions::{GoToHunk, GoToPreviousHunk},
+ multibuffer_context_lines,
scroll::Autoscroll,
};
use futures::StreamExt;
@@ -242,7 +243,7 @@ impl ProjectDiff {
TRACKED_NAMESPACE
};
- let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone());
+ let path_key = PathKey::namespaced(namespace, entry.repo_path.0);
self.move_to_path(path_key, window, cx)
}
@@ -280,7 +281,7 @@ impl ProjectDiff {
fn button_states(&self, cx: &App) -> ButtonStates {
let editor = self.editor.read(cx);
let snapshot = self.multibuffer.read(cx).snapshot(cx);
- let prev_next = snapshot.diff_hunks().skip(1).next().is_some();
+ let prev_next = snapshot.diff_hunks().nth(1).is_some();
let mut selection = true;
let mut ranges = editor
@@ -329,14 +330,14 @@ impl ProjectDiff {
})
.ok();
- return ButtonStates {
+ ButtonStates {
stage: has_unstaged_hunks,
unstage: has_staged_hunks,
prev_next,
selection,
stage_all,
unstage_all,
- };
+ }
}
fn handle_editor_event(
@@ -346,27 +347,24 @@ impl ProjectDiff {
window: &mut Window,
cx: &mut Context<Self>,
) {
- match event {
- EditorEvent::SelectionsChanged { local: true } => {
- let Some(project_path) = self.active_path(cx) else {
- return;
- };
- self.workspace
- .update(cx, |workspace, cx| {
- if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
- git_panel.update(cx, |git_panel, cx| {
- git_panel.select_entry_by_path(project_path, window, cx)
- })
- }
- })
- .ok();
- }
- _ => {}
+ if let EditorEvent::SelectionsChanged { local: true } = event {
+ let Some(project_path) = self.active_path(cx) else {
+ return;
+ };
+ self.workspace
+ .update(cx, |workspace, cx| {
+ if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
+ git_panel.update(cx, |git_panel, cx| {
+ git_panel.select_entry_by_path(project_path, window, cx)
+ })
+ }
+ })
+ .ok();
}
- if editor.focus_handle(cx).contains_focused(window, cx) {
- if self.multibuffer.read(cx).is_empty() {
- self.focus_handle.focus(window)
- }
+ if editor.focus_handle(cx).contains_focused(window, cx)
+ && self.multibuffer.read(cx).is_empty()
+ {
+ self.focus_handle.focus(window)
}
}
@@ -451,10 +449,10 @@ impl ProjectDiff {
let diff = diff.read(cx);
let diff_hunk_ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
- .map(|diff_hunk| diff_hunk.buffer_range.clone());
+ .map(|diff_hunk| diff_hunk.buffer_range);
let conflicts = conflict_addon
.conflict_set(snapshot.remote_id())
- .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts.clone())
+ .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts)
.unwrap_or_default();
let conflicts = conflicts.iter().map(|conflict| conflict.range.clone());
@@ -468,7 +466,7 @@ impl ProjectDiff {
path_key.clone(),
buffer,
excerpt_ranges,
- editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ multibuffer_context_lines(cx),
cx,
);
(was_empty, is_newly_added)
@@ -513,7 +511,7 @@ impl ProjectDiff {
mut recv: postage::watch::Receiver<()>,
cx: &mut AsyncWindowContext,
) -> Result<()> {
- while let Some(_) = recv.next().await {
+ while (recv.next().await).is_some() {
let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?;
for buffer_to_load in buffers_to_load {
if let Some(buffer) = buffer_to_load.await.log_err() {
@@ -740,7 +738,7 @@ impl Render for ProjectDiff {
} else {
None
};
- let keybinding_focus_handle = self.focus_handle(cx).clone();
+ let keybinding_focus_handle = self.focus_handle(cx);
el.child(
v_flex()
.gap_1()
@@ -1073,8 +1071,7 @@ pub struct ProjectDiffEmptyState {
impl RenderOnce for ProjectDiffEmptyState {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let status_against_remote = |ahead_by: usize, behind_by: usize| -> bool {
- match self.current_branch {
- Some(Branch {
+ matches!(self.current_branch, Some(Branch {
upstream:
Some(Upstream {
tracking:
@@ -1084,9 +1081,7 @@ impl RenderOnce for ProjectDiffEmptyState {
..
}),
..
- }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0) => true,
- _ => false,
- }
+ }) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0))
};
let change_count = |current_branch: &Branch| -> (usize, usize) {
@@ -1173,7 +1168,7 @@ impl RenderOnce for ProjectDiffEmptyState {
.child(Label::new("No Changes").color(Color::Muted))
} else {
this.when_some(self.current_branch.as_ref(), |this, branch| {
- this.child(has_branch_container(&branch))
+ this.child(has_branch_container(branch))
})
}
}),
@@ -1225,6 +1220,7 @@ mod preview {
sha: "abc123".into(),
subject: "Modify stuff".into(),
commit_timestamp: 1710932954,
+ author_name: "John Doe".into(),
has_parent: true,
}),
}
@@ -1332,14 +1328,14 @@ fn merge_anchor_ranges<'a>(
loop {
if let Some(left_range) = left
.peek()
- .filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le())
+ .filter(|range| range.start.cmp(&next_range.end, snapshot).is_le())
.cloned()
{
left.next();
next_range.end = left_range.end;
} else if let Some(right_range) = right
.peek()
- .filter(|range| range.start.cmp(&next_range.end, &snapshot).is_le())
+ .filter(|range| range.start.cmp(&next_range.end, snapshot).is_le())
.cloned()
{
right.next();
@@ -48,7 +48,7 @@ impl TextDiffView {
let selection_data = source_editor.update(cx, |editor, cx| {
let multibuffer = editor.buffer().read(cx);
- let source_buffer = multibuffer.as_singleton()?.clone();
+ let source_buffer = multibuffer.as_singleton()?;
let selections = editor.selections.all::<Point>(cx);
let buffer_snapshot = source_buffer.read(cx);
let first_selection = selections.first()?;
@@ -207,7 +207,7 @@ impl TextDiffView {
path: Some(format!("Clipboard ↔ {selection_location_path}").into()),
buffer_changes_tx,
_recalculate_diff_task: cx.spawn(async move |_, cx| {
- while let Ok(_) = buffer_changes_rx.recv().await {
+ while buffer_changes_rx.recv().await.is_ok() {
loop {
let mut timer = cx
.background_executor()
@@ -259,7 +259,7 @@ async fn update_diff_buffer(
let source_buffer_snapshot = source_buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let base_buffer_snapshot = clipboard_buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
- let base_text = base_buffer_snapshot.text().to_string();
+ let base_text = base_buffer_snapshot.text();
let diff_snapshot = cx
.update(|cx| {
@@ -686,7 +686,7 @@ mod tests {
let project = Project::test(fs, [project_root.as_ref()], cx).await;
- let (workspace, mut cx) =
+ let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let buffer = project
@@ -725,7 +725,7 @@ mod tests {
assert_state_with_diff(
&diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()),
- &mut cx,
+ cx,
expected_diff,
);
@@ -1,8 +1,8 @@
-use editor::{Editor, MultiBufferSnapshot};
+use editor::{Editor, EditorSettings, MultiBufferSnapshot};
use gpui::{App, Entity, FocusHandle, Focusable, Subscription, Task, WeakEntity};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use std::{fmt::Write, num::NonZeroU32, time::Duration};
use text::{Point, Selection};
use ui::{
@@ -95,10 +95,8 @@ impl CursorPosition {
.ok()
.unwrap_or(true);
- if !is_singleton {
- if let Some(debounce) = debounce {
- cx.background_executor().timer(debounce).await;
- }
+ if !is_singleton && let Some(debounce) = debounce {
+ cx.background_executor().timer(debounce).await;
}
editor
@@ -108,7 +106,7 @@ impl CursorPosition {
cursor_position.selected_count.selections = editor.selections.count();
match editor.mode() {
editor::EditorMode::AutoHeight { .. }
- | editor::EditorMode::SingleLine { .. }
+ | editor::EditorMode::SingleLine
| editor::EditorMode::Minimap { .. } => {
cursor_position.position = None;
cursor_position.context = None;
@@ -131,7 +129,7 @@ impl CursorPosition {
cursor_position.selected_count.lines += 1;
}
}
- if last_selection.as_ref().map_or(true, |last_selection| {
+ if last_selection.as_ref().is_none_or(|last_selection| {
selection.id > last_selection.id
}) {
last_selection = Some(selection);
@@ -209,6 +207,13 @@ impl CursorPosition {
impl Render for CursorPosition {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ if !EditorSettings::get_global(cx)
+ .status_bar
+ .cursor_position_button
+ {
+ return div();
+ }
+
div().when_some(self.position, |el, position| {
let mut text = format!(
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
@@ -227,13 +232,11 @@ impl Render for CursorPosition {
if let Some(editor) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
+ && let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx)
{
- if let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx)
- {
- workspace.toggle_modal(window, cx, |window, cx| {
- crate::GoToLine::new(editor, buffer, window, cx)
- })
- }
+ workspace.toggle_modal(window, cx, |window, cx| {
+ crate::GoToLine::new(editor, buffer, window, cx)
+ })
}
});
}
@@ -298,14 +301,13 @@ pub(crate) enum LineIndicatorFormat {
Long,
}
-#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
+#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)]
#[serde(transparent)]
+#[settings_key(key = "line_indicator_format")]
pub(crate) struct LineIndicatorFormatContent(LineIndicatorFormat);
impl Settings for LineIndicatorFormat {
- const KEY: Option<&'static str> = Some("line_indicator_format");
-
- type FileContent = Option<LineIndicatorFormatContent>;
+ type FileContent = LineIndicatorFormatContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
let format = [
@@ -314,8 +316,8 @@ impl Settings for LineIndicatorFormat {
sources.user,
]
.into_iter()
- .find_map(|value| value.copied().flatten())
- .unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
+ .find_map(|value| value.copied())
+ .unwrap_or(*sources.default);
Ok(format.0)
}
@@ -103,17 +103,20 @@ impl GoToLine {
return;
};
editor.update(cx, |editor, cx| {
- if let Some(placeholder_text) = editor.placeholder_text() {
- if editor.text(cx).is_empty() {
- let placeholder_text = placeholder_text.to_string();
- editor.set_text(placeholder_text, window, cx);
- }
+ if let Some(placeholder_text) = editor.placeholder_text(cx)
+ && editor.text(cx).is_empty()
+ {
+ editor.set_text(placeholder_text, window, cx);
}
});
}
})
.detach();
- editor.set_placeholder_text(format!("{line}{FILE_ROW_COLUMN_DELIMITER}{column}"), cx);
+ editor.set_placeholder_text(
+ &format!("{line}{FILE_ROW_COLUMN_DELIMITER}{column}"),
+ window,
+ cx,
+ );
editor
});
let line_editor_change = cx.subscribe_in(&line_editor, window, Self::on_line_editor_event);
@@ -157,7 +160,7 @@ impl GoToLine {
self.prev_scroll_position.take();
cx.emit(DismissEvent)
}
- editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
+ editor::EditorEvent::BufferEdited => self.highlight_current_line(cx),
_ => {}
}
}
@@ -691,11 +694,11 @@ mod tests {
let go_to_line_view = open_go_to_line_view(workspace, cx);
go_to_line_view.update(cx, |go_to_line_view, cx| {
assert_eq!(
- go_to_line_view
- .line_editor
- .read(cx)
- .placeholder_text()
- .expect("No placeholder text"),
+ go_to_line_view.line_editor.update(cx, |line_editor, cx| {
+ line_editor
+ .placeholder_text(cx)
+ .expect("No placeholder text")
+ }),
format!(
"{}:{}",
expected_placeholder.line, expected_placeholder.character
@@ -712,7 +715,7 @@ mod tests {
) -> Entity<GoToLine> {
cx.dispatch_action(editor::actions::ToggleGoToLine);
workspace.update(cx, |workspace, cx| {
- workspace.active_modal::<GoToLine>(cx).unwrap().clone()
+ workspace.active_modal::<GoToLine>(cx).unwrap()
})
}
@@ -13,6 +13,7 @@ pub async fn stream_generate_content(
api_key: &str,
mut request: GenerateContentRequest,
) -> Result<BoxStream<'static, Result<GenerateContentResponse>>> {
+ let api_key = api_key.trim();
validate_generate_content_request(&request)?;
// The `model` field is emptied as it is provided as a path parameter.
@@ -106,10 +107,9 @@ pub fn validate_generate_content_request(request: &GenerateContentRequest) -> Re
.contents
.iter()
.find(|content| content.role == Role::User)
+ && user_content.parts.is_empty()
{
- if user_content.parts.is_empty() {
- bail!("User content must contain at least one part");
- }
+ bail!("User content must contain at least one part");
}
Ok(())
@@ -267,7 +267,7 @@ pub struct CitationMetadata {
pub struct PromptFeedback {
#[serde(skip_serializing_if = "Option::is_none")]
pub block_reason: Option<String>,
- pub safety_ratings: Vec<SafetyRating>,
+ pub safety_ratings: Option<Vec<SafetyRating>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub block_reason_message: Option<String>,
}
@@ -478,10 +478,10 @@ impl<'de> Deserialize<'de> for ModelName {
model_id: id.to_string(),
})
} else {
- return Err(serde::de::Error::custom(format!(
+ Err(serde::de::Error::custom(format!(
"Expected model name to begin with {}, got: {}",
MODEL_NAME_PREFIX, string
- )));
+ )))
}
}
}
@@ -12,13 +12,13 @@ license = "Apache-2.0"
workspace = true
[features]
-default = ["http_client", "font-kit", "wayland", "x11", "windows-manifest"]
+default = ["font-kit", "wayland", "x11", "windows-manifest"]
test-support = [
"leak-detection",
"collections/test-support",
"rand",
"util/test-support",
- "http_client?/test-support",
+ "http_client/test-support",
"wayland",
"x11",
]
@@ -91,7 +91,7 @@ derive_more.workspace = true
etagere = "0.2"
futures.workspace = true
gpui_macros.workspace = true
-http_client = { optional = true, workspace = true }
+http_client.workspace = true
image.workspace = true
inventory.workspace = true
itertools.workspace = true
@@ -119,6 +119,7 @@ serde_json.workspace = true
slotmap = "1.0.6"
smallvec.workspace = true
smol.workspace = true
+stacksafe.workspace = true
strum.workspace = true
sum_tree.workspace = true
taffy = "=0.9.0"
@@ -209,7 +210,7 @@ xkbcommon = { version = "0.8.0", features = [
"wayland",
"x11",
], optional = true }
-xim = { git = "https://github.com/XDeme1/xim-rs", rev = "d50d461764c2213655cd9cf65a0ea94c70d3c4fd", features = [
+xim = { git = "https://github.com/zed-industries/xim-rs", rev = "c0a70c1bd2ce197364216e5e818a2cb3adb99a8d" , features = [
"x11rb-xcb",
"x11rb-client",
], optional = true }
@@ -23,7 +23,7 @@ On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do t
- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account.
-> Ensure you launch XCode after installing, and install the macOS components, which is the default option.
+> Ensure you launch Xcode after installing, and install the macOS components, which is the default option. If you are on macOS 26 (Tahoe) you will need to use `--features gpui/runtime_shaders` or add the feature in the root `Cargo.toml`
- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/)
@@ -327,10 +327,10 @@ mod windows {
/// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
fn find_fxc_compiler() -> String {
// Check environment variable
- if let Ok(path) = std::env::var("GPUI_FXC_PATH") {
- if Path::new(&path).exists() {
- return path;
- }
+ if let Ok(path) = std::env::var("GPUI_FXC_PATH")
+ && Path::new(&path).exists()
+ {
+ return path;
}
// Try to find in PATH
@@ -338,11 +338,10 @@ mod windows {
if let Ok(output) = std::process::Command::new("where.exe")
.arg("fxc.exe")
.output()
+ && output.status.success()
{
- if output.status.success() {
- let path = String::from_utf8_lossy(&output.stdout);
- return path.trim().to_string();
- }
+ let path = String::from_utf8_lossy(&output.stdout);
+ return path.trim().to_string();
}
// Check the default path
@@ -374,7 +373,7 @@ mod windows {
shader_path,
"vs_4_1",
);
- generate_rust_binding(&const_name, &output_file, &rust_binding_path);
+ generate_rust_binding(&const_name, &output_file, rust_binding_path);
// Compile fragment shader
let output_file = format!("{}/{}_ps.h", out_dir, module);
@@ -387,7 +386,7 @@ mod windows {
shader_path,
"ps_4_1",
);
- generate_rust_binding(&const_name, &output_file, &rust_binding_path);
+ generate_rust_binding(&const_name, &output_file, rust_binding_path);
}
fn compile_shader_impl(
@@ -38,58 +38,58 @@ pub struct Quote {
impl Quote {
pub fn random() -> Self {
use rand::Rng;
- let mut rng = rand::thread_rng();
+ let mut rng = rand::rng();
// simulate a base price in a realistic range
- let prev_close = rng.gen_range(100.0..200.0);
- let change = rng.gen_range(-5.0..5.0);
+ let prev_close = rng.random_range(100.0..200.0);
+ let change = rng.random_range(-5.0..5.0);
let last_done = prev_close + change;
- let open = prev_close + rng.gen_range(-3.0..3.0);
- let high = (prev_close + rng.gen_range::<f64, _>(0.0..10.0)).max(open);
- let low = (prev_close - rng.gen_range::<f64, _>(0.0..10.0)).min(open);
- let timestamp = Duration::from_secs(rng.gen_range(0..86400));
- let volume = rng.gen_range(1_000_000..100_000_000);
+ let open = prev_close + rng.random_range(-3.0..3.0);
+ let high = (prev_close + rng.random_range::<f64, _>(0.0..10.0)).max(open);
+ let low = (prev_close - rng.random_range::<f64, _>(0.0..10.0)).min(open);
+ let timestamp = Duration::from_secs(rng.random_range(0..86400));
+ let volume = rng.random_range(1_000_000..100_000_000);
let turnover = last_done * volume as f64;
let symbol = {
let mut ticker = String::new();
- if rng.gen_bool(0.5) {
+ if rng.random_bool(0.5) {
ticker.push_str(&format!(
"{:03}.{}",
- rng.gen_range(100..1000),
- rng.gen_range(0..10)
+ rng.random_range(100..1000),
+ rng.random_range(0..10)
));
} else {
ticker.push_str(&format!(
"{}{}",
- rng.gen_range('A'..='Z'),
- rng.gen_range('A'..='Z')
+ rng.random_range('A'..='Z'),
+ rng.random_range('A'..='Z')
));
}
- ticker.push_str(&format!(".{}", rng.gen_range('A'..='Z')));
+ ticker.push_str(&format!(".{}", rng.random_range('A'..='Z')));
ticker
};
let name = format!(
"{} {} - #{}",
symbol,
- rng.gen_range(1..100),
- rng.gen_range(10000..100000)
+ rng.random_range(1..100),
+ rng.random_range(10000..100000)
);
- let ttm = rng.gen_range(0.0..10.0);
- let market_cap = rng.gen_range(1_000_000.0..10_000_000.0);
- let float_cap = market_cap + rng.gen_range(1_000.0..10_000.0);
- let shares = rng.gen_range(100.0..1000.0);
+ let ttm = rng.random_range(0.0..10.0);
+ let market_cap = rng.random_range(1_000_000.0..10_000_000.0);
+ let float_cap = market_cap + rng.random_range(1_000.0..10_000.0);
+ let shares = rng.random_range(100.0..1000.0);
let pb = market_cap / shares;
let pe = market_cap / shares;
let eps = market_cap / shares;
- let dividend = rng.gen_range(0.0..10.0);
- let dividend_yield = rng.gen_range(0.0..10.0);
- let dividend_per_share = rng.gen_range(0.0..10.0);
+ let dividend = rng.random_range(0.0..10.0);
+ let dividend_yield = rng.random_range(0.0..10.0);
+ let dividend_per_share = rng.random_range(0.0..10.0);
let dividend_date = SharedString::new(format!(
"{}-{}-{}",
- rng.gen_range(2000..2023),
- rng.gen_range(1..12),
- rng.gen_range(1..28)
+ rng.random_range(2000..2023),
+ rng.random_range(1..12),
+ rng.random_range(1..28)
));
- let dividend_payment = rng.gen_range(0.0..10.0);
+ let dividend_payment = rng.random_range(0.0..10.0);
Self {
name: name.into(),
@@ -75,65 +75,71 @@ impl Render for ImageShowcase {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("main")
+ .bg(gpui::white())
.overflow_y_scroll()
.p_5()
.size_full()
- .flex()
- .flex_col()
- .justify_center()
- .items_center()
- .gap_8()
- .bg(rgb(0xffffff))
.child(
div()
.flex()
- .flex_row()
+ .flex_col()
.justify_center()
.items_center()
.gap_8()
- .child(ImageContainer::new(
- "Image loaded from a local file",
- self.local_resource.clone(),
- ))
- .child(ImageContainer::new(
- "Image loaded from a remote resource",
- self.remote_resource.clone(),
+ .child(img(
+ "https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg",
))
- .child(ImageContainer::new(
- "Image loaded from an asset",
- self.asset_resource.clone(),
- )),
- )
- .child(
- div()
- .flex()
- .flex_row()
- .gap_8()
.child(
div()
- .flex_col()
- .child("Auto Width")
- .child(img("https://picsum.photos/800/400").h(px(180.))),
+ .flex()
+ .flex_row()
+ .justify_center()
+ .items_center()
+ .gap_8()
+ .child(ImageContainer::new(
+ "Image loaded from a local file",
+ self.local_resource.clone(),
+ ))
+ .child(ImageContainer::new(
+ "Image loaded from a remote resource",
+ self.remote_resource.clone(),
+ ))
+ .child(ImageContainer::new(
+ "Image loaded from an asset",
+ self.asset_resource.clone(),
+ )),
+ )
+ .child(
+ div()
+ .flex()
+ .flex_row()
+ .gap_8()
+ .child(
+ div()
+ .flex_col()
+ .child("Auto Width")
+ .child(img("https://picsum.photos/800/400").h(px(180.))),
+ )
+ .child(
+ div()
+ .flex_col()
+ .child("Auto Height")
+ .child(img("https://picsum.photos/800/400").w(px(180.))),
+ ),
)
.child(
div()
+ .flex()
.flex_col()
- .child("Auto Height")
- .child(img("https://picsum.photos/800/400").w(px(180.))),
+ .justify_center()
+ .items_center()
+ .w_full()
+ .border_1()
+ .border_color(rgb(0xC0C0C0))
+ .child("image with max width 100%")
+ .child(img("https://picsum.photos/800/400").max_w_full()),
),
)
- .child(
- div()
- .flex()
- .flex_col()
- .justify_center()
- .items_center()
- .w_full()
- .border_1()
- .border_color(rgb(0xC0C0C0))
- .child("image with max width 100%")
- .child(img("https://picsum.photos/800/400").max_w_full()),
- )
}
}
@@ -137,14 +137,14 @@ impl TextInput {
fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
cx.write_to_clipboard(ClipboardItem::new_string(
- (&self.content[self.selected_range.clone()]).to_string(),
+ self.content[self.selected_range.clone()].to_string(),
));
}
}
fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context<Self>) {
if !self.selected_range.is_empty() {
cx.write_to_clipboard(ClipboardItem::new_string(
- (&self.content[self.selected_range.clone()]).to_string(),
+ self.content[self.selected_range.clone()].to_string(),
));
self.replace_text_in_range(None, "", window, cx)
}
@@ -446,7 +446,7 @@ impl Element for TextElement {
let (display_text, text_color) = if content.is_empty() {
(input.placeholder.clone(), hsla(0., 0., 0., 0.2))
} else {
- (content.clone(), style.color)
+ (content, style.color)
};
let run = TextRun {
@@ -474,7 +474,7 @@ impl Element for TextElement {
},
TextRun {
len: display_text.len() - marked_range.end,
- ..run.clone()
+ ..run
},
]
.into_iter()
@@ -549,10 +549,10 @@ impl Element for TextElement {
line.paint(bounds.origin, window.line_height(), window, cx)
.unwrap();
- if focus_handle.is_focused(window) {
- if let Some(cursor) = prepaint.cursor.take() {
- window.paint_quad(cursor);
- }
+ if focus_handle.is_focused(window)
+ && let Some(cursor) = prepaint.cursor.take()
+ {
+ window.paint_quad(cursor);
}
self.input.update(cx, |input, _cx| {
@@ -595,9 +595,7 @@ impl Render for TextInput {
.w_full()
.p(px(4.))
.bg(white())
- .child(TextElement {
- input: cx.entity().clone(),
- }),
+ .child(TextElement { input: cx.entity() }),
)
}
}
@@ -155,7 +155,7 @@ impl RenderOnce for Specimen {
.text_size(px(font_size * scale))
.line_height(relative(line_height))
.p(px(10.0))
- .child(self.string.clone())
+ .child(self.string)
}
}
@@ -152,6 +152,36 @@ impl Render for WindowDemo {
)
.unwrap();
}))
+ .child(button("Unresizable", move |_, cx| {
+ cx.open_window(
+ WindowOptions {
+ is_resizable: false,
+ window_bounds: Some(window_bounds),
+ ..Default::default()
+ },
+ |_, cx| {
+ cx.new(|_| SubWindow {
+ custom_titlebar: false,
+ })
+ },
+ )
+ .unwrap();
+ }))
+ .child(button("Unminimizable", move |_, cx| {
+ cx.open_window(
+ WindowOptions {
+ is_minimizable: false,
+ window_bounds: Some(window_bounds),
+ ..Default::default()
+ },
+ |_, cx| {
+ cx.new(|_| SubWindow {
+ custom_titlebar: false,
+ })
+ },
+ )
+ .unwrap();
+ }))
.child(button("Hide Application", |window, cx| {
cx.hide();
@@ -62,6 +62,8 @@ fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> Window
app_id: None,
window_min_size: None,
window_decorations: None,
+ tabbing_identifier: None,
+ ..Default::default()
}
}
@@ -73,18 +73,18 @@ macro_rules! actions {
/// - `name = "ActionName"` overrides the action's name. This must not contain `::`.
///
/// - `no_json` causes the `build` method to always error and `action_json_schema` to return `None`,
-/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`.
+/// and allows actions not implement `serde::Serialize` and `schemars::JsonSchema`.
///
/// - `no_register` skips registering the action. This is useful for implementing the `Action` trait
-/// while not supporting invocation by name or JSON deserialization.
+/// while not supporting invocation by name or JSON deserialization.
///
/// - `deprecated_aliases = ["editor::SomeAction"]` specifies deprecated old names for the action.
-/// These action names should *not* correspond to any actions that are registered. These old names
-/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will
-/// accept these old names and provide warnings.
+/// These action names should *not* correspond to any actions that are registered. These old names
+/// can then still be used to refer to invoke this action. In Zed, the keymap JSON schema will
+/// accept these old names and provide warnings.
///
/// - `deprecated = "Message about why this action is deprecation"` specifies a deprecation message.
-/// In Zed, the keymap JSON schema will cause this to be displayed as a warning.
+/// In Zed, the keymap JSON schema will cause this to be displayed as a warning.
///
/// # Manual Implementation
///
@@ -7,7 +7,7 @@ use std::{
path::{Path, PathBuf},
rc::{Rc, Weak},
sync::{Arc, atomic::Ordering::SeqCst},
- time::Duration,
+ time::{Duration, Instant},
};
use anyhow::{Context as _, Result, anyhow};
@@ -17,6 +17,7 @@ use futures::{
channel::oneshot,
future::{LocalBoxFuture, Shared},
};
+use itertools::Itertools;
use parking_lot::RwLock;
use slotmap::SlotMap;
@@ -37,10 +38,10 @@ use crate::{
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
- PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
- PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
- SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
- WindowHandle, WindowId, WindowInvalidator,
+ PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
+ PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
+ Reservation, ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
+ TextSystem, Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
colors::{Colors, GlobalColors},
current_platform, hash, init_app_menus,
};
@@ -237,6 +238,303 @@ type WindowClosedHandler = Box<dyn FnMut(&mut App)>;
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut App) + 'static>;
type NewEntityListener = Box<dyn FnMut(AnyEntity, &mut Option<&mut Window>, &mut App) + 'static>;
+#[doc(hidden)]
+#[derive(Clone, PartialEq, Eq)]
+pub struct SystemWindowTab {
+ pub id: WindowId,
+ pub title: SharedString,
+ pub handle: AnyWindowHandle,
+ pub last_active_at: Instant,
+}
+
+impl SystemWindowTab {
+ /// Create a new instance of the window tab.
+ pub fn new(title: SharedString, handle: AnyWindowHandle) -> Self {
+ Self {
+ id: handle.id,
+ title,
+ handle,
+ last_active_at: Instant::now(),
+ }
+ }
+}
+
+/// A controller for managing window tabs.
+#[derive(Default)]
+pub struct SystemWindowTabController {
+ visible: Option<bool>,
+ tab_groups: FxHashMap<usize, Vec<SystemWindowTab>>,
+}
+
+impl Global for SystemWindowTabController {}
+
+impl SystemWindowTabController {
+ /// Create a new instance of the window tab controller.
+ pub fn new() -> Self {
+ Self {
+ visible: None,
+ tab_groups: FxHashMap::default(),
+ }
+ }
+
+ /// Initialize the global window tab controller.
+ pub fn init(cx: &mut App) {
+ cx.set_global(SystemWindowTabController::new());
+ }
+
+ /// Get all tab groups.
+ pub fn tab_groups(&self) -> &FxHashMap<usize, Vec<SystemWindowTab>> {
+ &self.tab_groups
+ }
+
+ /// Get the next tab group window handle.
+ pub fn get_next_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> {
+ let controller = cx.global::<SystemWindowTabController>();
+ let current_group = controller
+ .tab_groups
+ .iter()
+ .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
+
+ let current_group = current_group?;
+ let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
+ let idx = group_ids.iter().position(|g| *g == current_group)?;
+ let next_idx = (idx + 1) % group_ids.len();
+
+ controller
+ .tab_groups
+ .get(group_ids[next_idx])
+ .and_then(|tabs| {
+ tabs.iter()
+ .max_by_key(|tab| tab.last_active_at)
+ .or_else(|| tabs.first())
+ .map(|tab| &tab.handle)
+ })
+ }
+
+ /// Get the previous tab group window handle.
+ pub fn get_prev_tab_group_window(cx: &mut App, id: WindowId) -> Option<&AnyWindowHandle> {
+ let controller = cx.global::<SystemWindowTabController>();
+ let current_group = controller
+ .tab_groups
+ .iter()
+ .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| group));
+
+ let current_group = current_group?;
+ let mut group_ids: Vec<_> = controller.tab_groups.keys().collect();
+ let idx = group_ids.iter().position(|g| *g == current_group)?;
+ let prev_idx = if idx == 0 {
+ group_ids.len() - 1
+ } else {
+ idx - 1
+ };
+
+ controller
+ .tab_groups
+ .get(group_ids[prev_idx])
+ .and_then(|tabs| {
+ tabs.iter()
+ .max_by_key(|tab| tab.last_active_at)
+ .or_else(|| tabs.first())
+ .map(|tab| &tab.handle)
+ })
+ }
+
+ /// Get all tabs in the same window.
+ pub fn tabs(&self, id: WindowId) -> Option<&Vec<SystemWindowTab>> {
+ let tab_group = self
+ .tab_groups
+ .iter()
+ .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group));
+
+ if let Some(tab_group) = tab_group {
+ self.tab_groups.get(&tab_group)
+ } else {
+ None
+ }
+ }
+
+ /// Initialize the visibility of the system window tab controller.
+ pub fn init_visible(cx: &mut App, visible: bool) {
+ let mut controller = cx.global_mut::<SystemWindowTabController>();
+ if controller.visible.is_none() {
+ controller.visible = Some(visible);
+ }
+ }
+
+ /// Get the visibility of the system window tab controller.
+ pub fn is_visible(&self) -> bool {
+ self.visible.unwrap_or(false)
+ }
+
+ /// Set the visibility of the system window tab controller.
+ pub fn set_visible(cx: &mut App, visible: bool) {
+ let mut controller = cx.global_mut::<SystemWindowTabController>();
+ controller.visible = Some(visible);
+ }
+
+ /// Update the last active of a window.
+ pub fn update_last_active(cx: &mut App, id: WindowId) {
+ let mut controller = cx.global_mut::<SystemWindowTabController>();
+ for windows in controller.tab_groups.values_mut() {
+ for tab in windows.iter_mut() {
+ if tab.id == id {
+ tab.last_active_at = Instant::now();
+ }
+ }
+ }
+ }
+
+ /// Update the position of a tab within its group.
+ pub fn update_tab_position(cx: &mut App, id: WindowId, ix: usize) {
+ let mut controller = cx.global_mut::<SystemWindowTabController>();
+ for (_, windows) in controller.tab_groups.iter_mut() {
+ if let Some(current_pos) = windows.iter().position(|tab| tab.id == id) {
+ if ix < windows.len() && current_pos != ix {
+ let window_tab = windows.remove(current_pos);
+ windows.insert(ix, window_tab);
+ }
+ break;
+ }
+ }
+ }
+
+ /// Update the title of a tab.
+ pub fn update_tab_title(cx: &mut App, id: WindowId, title: SharedString) {
+ let controller = cx.global::<SystemWindowTabController>();
+ let tab = controller
+ .tab_groups
+ .values()
+ .flat_map(|windows| windows.iter())
+ .find(|tab| tab.id == id);
+
+ if tab.map_or(true, |t| t.title == title) {
+ return;
+ }
+
+ let mut controller = cx.global_mut::<SystemWindowTabController>();
+ for windows in controller.tab_groups.values_mut() {
+ for tab in windows.iter_mut() {
+ if tab.id == id {
+ tab.title = title.clone();
+ }
+ }
+ }
+ }
+
+ /// Insert a tab into a tab group.
+ pub fn add_tab(cx: &mut App, id: WindowId, tabs: Vec<SystemWindowTab>) {
+ let mut controller = cx.global_mut::<SystemWindowTabController>();
+ let Some(tab) = tabs.clone().into_iter().find(|tab| tab.id == id) else {
+ return;
+ };
+
+ let mut expected_tab_ids: Vec<_> = tabs
+ .iter()
+ .filter(|tab| tab.id != id)
+ .map(|tab| tab.id)
+ .sorted()
+ .collect();
+
+ let mut tab_group_id = None;
+ for (group_id, group_tabs) in &controller.tab_groups {
+ let tab_ids: Vec<_> = group_tabs.iter().map(|tab| tab.id).sorted().collect();
+ if tab_ids == expected_tab_ids {
+ tab_group_id = Some(*group_id);
+ break;
+ }
+ }
+
+ if let Some(tab_group_id) = tab_group_id {
+ if let Some(tabs) = controller.tab_groups.get_mut(&tab_group_id) {
+ tabs.push(tab);
+ }
+ } else {
+ let new_group_id = controller.tab_groups.len();
+ controller.tab_groups.insert(new_group_id, tabs);
+ }
+ }
+
+ /// Remove a tab from a tab group.
+ pub fn remove_tab(cx: &mut App, id: WindowId) -> Option<SystemWindowTab> {
+ let mut controller = cx.global_mut::<SystemWindowTabController>();
+ let mut removed_tab = None;
+
+ controller.tab_groups.retain(|_, tabs| {
+ if let Some(pos) = tabs.iter().position(|tab| tab.id == id) {
+ removed_tab = Some(tabs.remove(pos));
+ }
+ !tabs.is_empty()
+ });
+
+ removed_tab
+ }
+
+ /// Move a tab to a new tab group.
+ pub fn move_tab_to_new_window(cx: &mut App, id: WindowId) {
+ let mut removed_tab = Self::remove_tab(cx, id);
+ let mut controller = cx.global_mut::<SystemWindowTabController>();
+
+ if let Some(tab) = removed_tab {
+ let new_group_id = controller.tab_groups.keys().max().map_or(0, |k| k + 1);
+ controller.tab_groups.insert(new_group_id, vec![tab]);
+ }
+ }
+
+ /// Merge all tab groups into a single group.
+ pub fn merge_all_windows(cx: &mut App, id: WindowId) {
+ let mut controller = cx.global_mut::<SystemWindowTabController>();
+ let Some(initial_tabs) = controller.tabs(id) else {
+ return;
+ };
+
+ let mut all_tabs = initial_tabs.clone();
+ for tabs in controller.tab_groups.values() {
+ all_tabs.extend(
+ tabs.iter()
+ .filter(|tab| !initial_tabs.contains(tab))
+ .cloned(),
+ );
+ }
+
+ controller.tab_groups.clear();
+ controller.tab_groups.insert(0, all_tabs);
+ }
+
+ /// Selects the next tab in the tab group in the trailing direction.
+ pub fn select_next_tab(cx: &mut App, id: WindowId) {
+ let mut controller = cx.global_mut::<SystemWindowTabController>();
+ let Some(tabs) = controller.tabs(id) else {
+ return;
+ };
+
+ let current_index = tabs.iter().position(|tab| tab.id == id).unwrap();
+ let next_index = (current_index + 1) % tabs.len();
+
+ let _ = &tabs[next_index].handle.update(cx, |_, window, _| {
+ window.activate_window();
+ });
+ }
+
+ /// Selects the previous tab in the tab group in the leading direction.
+ pub fn select_previous_tab(cx: &mut App, id: WindowId) {
+ let mut controller = cx.global_mut::<SystemWindowTabController>();
+ let Some(tabs) = controller.tabs(id) else {
+ return;
+ };
+
+ let current_index = tabs.iter().position(|tab| tab.id == id).unwrap();
+ let previous_index = if current_index == 0 {
+ tabs.len() - 1
+ } else {
+ current_index - 1
+ };
+
+ let _ = &tabs[previous_index].handle.update(cx, |_, window, _| {
+ window.activate_window();
+ });
+ }
+}
+
/// Contains the state of the full application, and passed as a reference to a variety of callbacks.
/// Other [Context] derefs to this type.
/// You need a reference to an `App` to access the state of a [Entity].
@@ -263,6 +561,7 @@ pub struct App {
pub(crate) focus_handles: Arc<FocusMap>,
pub(crate) keymap: Rc<RefCell<Keymap>>,
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
+ pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
pub(crate) global_action_listeners:
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
pending_effects: VecDeque<Effect>,
@@ -312,6 +611,7 @@ impl App {
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let entities = EntityMap::new();
let keyboard_layout = platform.keyboard_layout();
+ let keyboard_mapper = platform.keyboard_mapper();
let app = Rc::new_cyclic(|this| AppCell {
app: RefCell::new(App {
@@ -337,6 +637,7 @@ impl App {
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
keymap: Rc::new(RefCell::new(Keymap::default())),
keyboard_layout,
+ keyboard_mapper,
global_action_listeners: FxHashMap::default(),
pending_effects: VecDeque::new(),
pending_notifications: FxHashSet::default(),
@@ -368,7 +669,8 @@ impl App {
}),
});
- init_app_menus(platform.as_ref(), &mut app.borrow_mut());
+ init_app_menus(platform.as_ref(), &app.borrow());
+ SystemWindowTabController::init(&mut app.borrow_mut());
platform.on_keyboard_layout_change(Box::new({
let app = Rc::downgrade(&app);
@@ -376,6 +678,7 @@ impl App {
if let Some(app) = app.upgrade() {
let cx = &mut app.borrow_mut();
cx.keyboard_layout = cx.platform.keyboard_layout();
+ cx.keyboard_mapper = cx.platform.keyboard_mapper();
cx.keyboard_layout_observers
.clone()
.retain(&(), move |callback| (callback)(cx));
@@ -424,6 +727,11 @@ impl App {
self.keyboard_layout.as_ref()
}
+ /// Get the current keyboard mapper.
+ pub fn keyboard_mapper(&self) -> &Rc<dyn PlatformKeyboardMapper> {
+ &self.keyboard_mapper
+ }
+
/// Invokes a handler when the current keyboard layout changes
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
where
@@ -816,8 +1124,9 @@ impl App {
pub fn prompt_for_new_path(
&self,
directory: &Path,
+ suggested_name: Option<&str>,
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
- self.platform.prompt_for_new_path(directory)
+ self.platform.prompt_for_new_path(directory, suggested_name)
}
/// Reveals the specified path at the platform level, such as in Finder on macOS.
@@ -1049,12 +1358,7 @@ impl App {
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
{
self.update(|cx| {
- let mut window = cx
- .windows
- .get_mut(id)
- .context("window not found")?
- .take()
- .context("window not found")?;
+ let mut window = cx.windows.get_mut(id)?.take()?;
let root_view = window.root.clone().unwrap();
@@ -1071,15 +1375,14 @@ impl App {
true
});
} else {
- cx.windows
- .get_mut(id)
- .context("window not found")?
- .replace(window);
+ cx.windows.get_mut(id)?.replace(window);
}
- Ok(result)
+ Some(result)
})
+ .context("window not found")
}
+
/// Creates an `AsyncApp`, which can be cloned and has a static lifetime
/// so it can be held across `await` points.
pub fn to_async(&self) -> AsyncApp {
@@ -1309,7 +1612,7 @@ impl App {
T: 'static,
{
let window_handle = window.handle;
- self.observe_release(&handle, move |entity, cx| {
+ self.observe_release(handle, move |entity, cx| {
let _ = window_handle.update(cx, |_, window, cx| on_release(entity, window, cx));
})
}
@@ -1331,7 +1634,7 @@ impl App {
}
inner(
- &mut self.keystroke_observers,
+ &self.keystroke_observers,
Box::new(move |event, window, cx| {
f(event, window, cx);
true
@@ -1357,7 +1660,7 @@ impl App {
}
inner(
- &mut self.keystroke_interceptors,
+ &self.keystroke_interceptors,
Box::new(move |event, window, cx| {
f(event, window, cx);
true
@@ -1515,12 +1818,11 @@ impl App {
/// the bindings in the element tree, and any global action listeners.
pub fn is_action_available(&mut self, action: &dyn Action) -> bool {
let mut action_available = false;
- if let Some(window) = self.active_window() {
- if let Ok(window_action_available) =
+ if let Some(window) = self.active_window()
+ && let Ok(window_action_available) =
window.update(self, |_, window, cx| window.is_action_available(action, cx))
- {
- action_available = window_action_available;
- }
+ {
+ action_available = window_action_available;
}
action_available
@@ -1605,27 +1907,26 @@ impl App {
.insert(action.as_any().type_id(), global_listeners);
}
- if self.propagate_event {
- if let Some(mut global_listeners) = self
+ if self.propagate_event
+ && let Some(mut global_listeners) = self
.global_action_listeners
.remove(&action.as_any().type_id())
- {
- for listener in global_listeners.iter().rev() {
- listener(action.as_any(), DispatchPhase::Bubble, self);
- if !self.propagate_event {
- break;
- }
+ {
+ for listener in global_listeners.iter().rev() {
+ listener(action.as_any(), DispatchPhase::Bubble, self);
+ if !self.propagate_event {
+ break;
}
+ }
- global_listeners.extend(
- self.global_action_listeners
- .remove(&action.as_any().type_id())
- .unwrap_or_default(),
- );
-
+ global_listeners.extend(
self.global_action_listeners
- .insert(action.as_any().type_id(), global_listeners);
- }
+ .remove(&action.as_any().type_id())
+ .unwrap_or_default(),
+ );
+
+ self.global_action_listeners
+ .insert(action.as_any().type_id(), global_listeners);
}
}
@@ -1708,8 +2009,8 @@ impl App {
.unwrap_or_else(|| {
is_first = true;
let future = A::load(source.clone(), self);
- let task = self.background_executor().spawn(future).shared();
- task
+
+ self.background_executor().spawn(future).shared()
});
self.loading_assets.insert(asset_id, Box::new(task.clone()));
@@ -1916,7 +2217,7 @@ impl AppContext for App {
G: Global,
{
let mut g = self.global::<G>();
- callback(&g, self)
+ callback(g, self)
}
}
@@ -2006,7 +2307,7 @@ pub struct AnyDrag {
}
/// Contains state associated with a tooltip. You'll only need this struct if you're implementing
-/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip].
+/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip](crate::Interactivity::tooltip).
#[derive(Clone)]
pub struct AnyTooltip {
/// The view used to display the tooltip
@@ -218,7 +218,24 @@ impl AsyncApp {
Some(read(app.try_global()?, &app))
}
- /// A convenience method for [App::update_global]
+ /// 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.
+ ///
+ /// # 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,
+ ) -> Result<R> {
+ let app = self.app.upgrade().context("app was released")?;
+ let mut app = app.borrow_mut();
+ app.update(|cx| {
+ cx.default_global::<G>();
+ });
+ 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,
@@ -293,7 +310,7 @@ impl AsyncWindowContext {
.update(self, |_, window, cx| read(cx.global(), window, cx))
}
- /// A convenience method for [`App::update_global`].
+ /// A convenience method for [`App::update_global`](BorrowAppContext::update_global).
/// for updating the global state of the specified type.
pub fn update_global<G, R>(
&mut self,
@@ -465,7 +482,7 @@ impl VisualContext for AsyncWindowContext {
V: Focusable,
{
self.window.update(self, |_, window, cx| {
- view.read(cx).focus_handle(cx).clone().focus(window);
+ view.read(cx).focus_handle(cx).focus(window);
})
}
}
@@ -472,7 +472,7 @@ impl<'a, T: 'static> Context<'a, T> {
let view = self.weak_entity();
inner(
- &mut self.keystroke_observers,
+ &self.keystroke_observers,
Box::new(move |event, window, cx| {
if let Some(view) = view.upgrade() {
view.update(cx, |view, cx| f(view, event, window, cx));
@@ -610,16 +610,16 @@ impl<'a, T: 'static> Context<'a, T> {
let (subscription, activate) =
window.new_focus_listener(Box::new(move |event, window, cx| {
view.update(cx, |view, cx| {
- if let Some(blurred_id) = event.previous_focus_path.last().copied() {
- if event.is_focus_out(focus_id) {
- let event = FocusOutEvent {
- blurred: WeakFocusHandle {
- id: blurred_id,
- handles: Arc::downgrade(&cx.focus_handles),
- },
- };
- listener(view, event, window, cx)
- }
+ if let Some(blurred_id) = event.previous_focus_path.last().copied()
+ && event.is_focus_out(focus_id)
+ {
+ let event = FocusOutEvent {
+ blurred: WeakFocusHandle {
+ id: blurred_id,
+ handles: Arc::downgrade(&cx.focus_handles),
+ },
+ };
+ listener(view, event, window, cx)
}
})
.is_ok()
@@ -231,14 +231,15 @@ impl AnyEntity {
Self {
entity_id: id,
entity_type,
- entity_map: entity_map.clone(),
#[cfg(any(test, feature = "leak-detection"))]
handle_id: entity_map
+ .clone()
.upgrade()
.unwrap()
.write()
.leak_detector
.handle_created(id),
+ entity_map,
}
}
@@ -661,7 +662,7 @@ pub struct WeakEntity<T> {
impl<T> std::fmt::Debug for WeakEntity<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct(&type_name::<Self>())
+ f.debug_struct(type_name::<Self>())
.field("entity_id", &self.any_entity.entity_id)
.field("entity_type", &type_name::<T>())
.finish()
@@ -786,7 +787,7 @@ impl<T: 'static> PartialOrd for WeakEntity<T> {
#[cfg(any(test, feature = "leak-detection"))]
static LEAK_BACKTRACE: std::sync::LazyLock<bool> =
- std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").map_or(false, |b| !b.is_empty()));
+ std::sync::LazyLock::new(|| std::env::var("LEAK_BACKTRACE").is_ok_and(|b| !b.is_empty()));
#[cfg(any(test, feature = "leak-detection"))]
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
@@ -134,7 +134,7 @@ impl TestAppContext {
app: App::new_app(platform.clone(), asset_source, http_client),
background_executor,
foreground_executor,
- dispatcher: dispatcher.clone(),
+ dispatcher,
test_platform: platform,
text_system,
fn_name,
@@ -144,7 +144,7 @@ impl TestAppContext {
/// Create a single TestAppContext, for non-multi-client tests
pub fn single() -> Self {
- let dispatcher = TestDispatcher::new(StdRng::from_entropy());
+ let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
Self::build(dispatcher, None)
}
@@ -192,6 +192,7 @@ impl TestAppContext {
&self.foreground_executor
}
+ #[expect(clippy::wrong_self_convention)]
fn new<T: 'static>(&mut self, build_entity: impl FnOnce(&mut Context<T>) -> T) -> Entity<T> {
let mut cx = self.app.borrow_mut();
cx.new(build_entity)
@@ -219,7 +220,7 @@ impl TestAppContext {
let mut cx = self.app.borrow_mut();
// Some tests rely on the window size matching the bounds of the test display
- let bounds = Bounds::maximized(None, &mut cx);
+ let bounds = Bounds::maximized(None, &cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
@@ -233,7 +234,7 @@ impl TestAppContext {
/// Adds a new window with no content.
pub fn add_empty_window(&mut self) -> &mut VisualTestContext {
let mut cx = self.app.borrow_mut();
- let bounds = Bounds::maximized(None, &mut cx);
+ let bounds = Bounds::maximized(None, &cx);
let window = cx
.open_window(
WindowOptions {
@@ -244,7 +245,7 @@ impl TestAppContext {
)
.unwrap();
drop(cx);
- let cx = VisualTestContext::from_window(*window.deref(), self).as_mut();
+ let cx = VisualTestContext::from_window(*window.deref(), self).into_mut();
cx.run_until_parked();
cx
}
@@ -261,7 +262,7 @@ impl TestAppContext {
V: 'static + Render,
{
let mut cx = self.app.borrow_mut();
- let bounds = Bounds::maximized(None, &mut cx);
+ let bounds = Bounds::maximized(None, &cx);
let window = cx
.open_window(
WindowOptions {
@@ -273,7 +274,7 @@ impl TestAppContext {
.unwrap();
drop(cx);
let view = window.root(self).unwrap();
- let cx = VisualTestContext::from_window(*window.deref(), self).as_mut();
+ let cx = VisualTestContext::from_window(*window.deref(), self).into_mut();
cx.run_until_parked();
// it might be nice to try and cleanup these at the end of each test.
@@ -338,7 +339,7 @@ impl TestAppContext {
/// Returns all windows open in the test.
pub fn windows(&self) -> Vec<AnyWindowHandle> {
- self.app.borrow().windows().clone()
+ self.app.borrow().windows()
}
/// Run the given task on the main thread.
@@ -585,7 +586,7 @@ impl<V: 'static> Entity<V> {
cx.executor().advance_clock(advance_clock_by);
async move {
- let notification = crate::util::timeout(duration, rx.recv())
+ let notification = crate::util::smol_timeout(duration, rx.recv())
.await
.expect("next notification timed out");
drop(subscription);
@@ -618,7 +619,7 @@ impl<V> Entity<V> {
}
}),
cx.subscribe(self, {
- let mut tx = tx.clone();
+ let mut tx = tx;
move |_, _: &Evt, _| {
tx.blocking_send(()).ok();
}
@@ -629,7 +630,7 @@ impl<V> Entity<V> {
let handle = self.downgrade();
async move {
- crate::util::timeout(Duration::from_secs(1), async move {
+ crate::util::smol_timeout(Duration::from_secs(1), async move {
loop {
{
let cx = cx.borrow();
@@ -882,7 +883,7 @@ impl VisualTestContext {
/// Get an &mut VisualTestContext (which is mostly what you need to pass to other methods).
/// This method internally retains the VisualTestContext until the end of the test.
- pub fn as_mut(self) -> &'static mut Self {
+ pub fn into_mut(self) -> &'static mut Self {
let ptr = Box::into_raw(Box::new(self));
// safety: on_quit will be called after the test has finished.
// the executor will ensure that all tasks related to the test have stopped.
@@ -1025,7 +1026,7 @@ impl VisualContext for VisualTestContext {
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).clone().focus(window)
+ view.read(cx).focus_handle(cx).focus(window)
})
.unwrap()
}
@@ -1,8 +1,9 @@
use std::{
alloc::{self, handle_alloc_error},
cell::Cell,
+ num::NonZeroUsize,
ops::{Deref, DerefMut},
- ptr,
+ ptr::{self, NonNull},
rc::Rc,
};
@@ -30,23 +31,23 @@ impl Drop for Chunk {
fn drop(&mut self) {
unsafe {
let chunk_size = self.end.offset_from_unsigned(self.start);
- // this never fails as it succeeded during allocation
- let layout = alloc::Layout::from_size_align(chunk_size, 1).unwrap();
+ // SAFETY: This succeeded during allocation.
+ let layout = alloc::Layout::from_size_align_unchecked(chunk_size, 1);
alloc::dealloc(self.start, layout);
}
}
}
impl Chunk {
- fn new(chunk_size: usize) -> Self {
+ fn new(chunk_size: NonZeroUsize) -> Self {
unsafe {
// this only fails if chunk_size is unreasonably huge
- let layout = alloc::Layout::from_size_align(chunk_size, 1).unwrap();
+ let layout = alloc::Layout::from_size_align(chunk_size.get(), 1).unwrap();
let start = alloc::alloc(layout);
if start.is_null() {
handle_alloc_error(layout);
}
- let end = start.add(chunk_size);
+ let end = start.add(chunk_size.get());
Self {
start,
end,
@@ -55,14 +56,14 @@ impl Chunk {
}
}
- fn allocate(&mut self, layout: alloc::Layout) -> Option<*mut u8> {
+ fn allocate(&mut self, layout: alloc::Layout) -> Option<NonNull<u8>> {
unsafe {
let aligned = self.offset.add(self.offset.align_offset(layout.align()));
let next = aligned.add(layout.size());
if next <= self.end {
self.offset = next;
- Some(aligned)
+ NonNull::new(aligned)
} else {
None
}
@@ -79,7 +80,7 @@ pub struct Arena {
elements: Vec<ArenaElement>,
valid: Rc<Cell<bool>>,
current_chunk_index: usize,
- chunk_size: usize,
+ chunk_size: NonZeroUsize,
}
impl Drop for Arena {
@@ -90,7 +91,7 @@ impl Drop for Arena {
impl Arena {
pub fn new(chunk_size: usize) -> Self {
- assert!(chunk_size > 0);
+ let chunk_size = NonZeroUsize::try_from(chunk_size).unwrap();
Self {
chunks: vec![Chunk::new(chunk_size)],
elements: Vec::new(),
@@ -101,7 +102,7 @@ impl Arena {
}
pub fn capacity(&self) -> usize {
- self.chunks.len() * self.chunk_size
+ self.chunks.len() * self.chunk_size.get()
}
pub fn clear(&mut self) {
@@ -136,20 +137,20 @@ impl Arena {
let layout = alloc::Layout::new::<T>();
let mut current_chunk = &mut self.chunks[self.current_chunk_index];
let ptr = if let Some(ptr) = current_chunk.allocate(layout) {
- ptr
+ ptr.as_ptr()
} else {
self.current_chunk_index += 1;
if self.current_chunk_index >= self.chunks.len() {
self.chunks.push(Chunk::new(self.chunk_size));
assert_eq!(self.current_chunk_index, self.chunks.len() - 1);
- log::info!(
+ log::trace!(
"increased element arena capacity to {}kb",
self.capacity() / 1024,
);
}
current_chunk = &mut self.chunks[self.current_chunk_index];
if let Some(ptr) = current_chunk.allocate(layout) {
- ptr
+ ptr.as_ptr()
} else {
panic!(
"Arena chunk_size of {} is too small to allocate {} bytes",
@@ -1,4 +1,4 @@
-use crate::{DevicePixels, Result, SharedString, Size, size};
+use crate::{DevicePixels, Pixels, Result, SharedString, Size, size};
use smallvec::SmallVec;
use image::{Delay, Frame};
@@ -42,6 +42,8 @@ pub(crate) struct RenderImageParams {
pub struct RenderImage {
/// The ID associated with this image
pub id: ImageId,
+ /// The scale factor of this image on render.
+ pub(crate) scale_factor: f32,
data: SmallVec<[Frame; 1]>,
}
@@ -60,6 +62,7 @@ impl RenderImage {
Self {
id: ImageId(NEXT_ID.fetch_add(1, SeqCst)),
+ scale_factor: 1.0,
data: data.into(),
}
}
@@ -77,6 +80,12 @@ impl RenderImage {
size(width.into(), height.into())
}
+ /// Get the size of this image, in pixels for display, adjusted for the scale factor.
+ pub(crate) fn render_size(&self, frame_index: usize) -> Size<Pixels> {
+ self.size(frame_index)
+ .map(|v| (v.0 as f32 / self.scale_factor).into())
+ }
+
/// Get the delay of this frame from the previous
pub fn delay(&self, frame_index: usize) -> Delay {
self.data[frame_index].delay()
@@ -309,12 +309,12 @@ mod tests {
let mut expected_quads: Vec<(Bounds<f32>, u32)> = Vec::new();
// Insert a random number of random AABBs into the tree.
- let num_bounds = rng.gen_range(1..=max_bounds);
+ let num_bounds = rng.random_range(1..=max_bounds);
for _ in 0..num_bounds {
- let min_x: f32 = rng.gen_range(-100.0..100.0);
- let min_y: f32 = rng.gen_range(-100.0..100.0);
- let width: f32 = rng.gen_range(0.0..50.0);
- let height: f32 = rng.gen_range(0.0..50.0);
+ let min_x: f32 = rng.random_range(-100.0..100.0);
+ let min_y: f32 = rng.random_range(-100.0..100.0);
+ let width: f32 = rng.random_range(0.0..50.0);
+ let height: f32 = rng.random_range(0.0..50.0);
let bounds = Bounds {
origin: Point { x: min_x, y: min_y },
size: Size { width, height },
@@ -473,6 +473,11 @@ impl Hsla {
self.a == 0.0
}
+ /// Returns true if the HSLA color is fully opaque, false otherwise.
+ pub fn is_opaque(&self) -> bool {
+ self.a == 1.0
+ }
+
/// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
///
/// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
@@ -905,9 +910,9 @@ mod tests {
assert_eq!(background.solid, color);
assert_eq!(background.opacity(0.5).solid, color.opacity(0.5));
- assert_eq!(background.is_transparent(), false);
+ assert!(!background.is_transparent());
background.solid = hsla(0.0, 0.0, 0.0, 0.0);
- assert_eq!(background.is_transparent(), true);
+ assert!(background.is_transparent());
}
#[test]
@@ -921,7 +926,7 @@ mod tests {
assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
- assert_eq!(background.is_transparent(), false);
- assert_eq!(background.opacity(0.0).is_transparent(), true);
+ assert!(!background.is_transparent());
+ assert!(background.opacity(0.0).is_transparent());
}
}
@@ -88,9 +88,9 @@ impl Deref for GlobalColors {
impl Global for GlobalColors {}
-/// Implement this trait to allow global [Color] access via `cx.default_colors()`.
+/// Implement this trait to allow global [Colors] access via `cx.default_colors()`.
pub trait DefaultColors {
- /// Returns the default [`gpui::Colors`]
+ /// Returns the default [`Colors`]
fn default_colors(&self) -> &Arc<Colors>;
}
@@ -14,13 +14,13 @@
//! tree and any callbacks they have registered with GPUI are dropped and the process repeats.
//!
//! But some state is too simple and voluminous to store in every view that needs it, e.g.
-//! whether a hover has been started or not. For this, GPUI provides the [`Element::State`], associated type.
+//! whether a hover has been started or not. For this, GPUI provides the [`Element::PrepaintState`], associated type.
//!
//! # Implementing your own elements
//!
//! Elements are intended to be the low level, imperative API to GPUI. They are responsible for upholding,
//! or breaking, GPUI's features as they deem necessary. As an example, most GPUI elements are expected
-//! to stay in the bounds that their parent element gives them. But with [`WindowContext::break_content_mask`],
+//! to stay in the bounds that their parent element gives them. But with [`Window::with_content_mask`],
//! you can ignore this restriction and paint anywhere inside of the window's bounds. This is useful for overlays
//! and popups and anything else that shows up 'on top' of other elements.
//! With great power, comes great responsibility.
@@ -603,10 +603,8 @@ impl AnyElement {
self.0.prepaint(window, cx);
- if !focus_assigned {
- if let Some(focus_id) = window.next_frame.focus {
- return FocusHandle::for_id(focus_id, &cx.focus_handles);
- }
+ if !focus_assigned && let Some(focus_id) = window.next_frame.focus {
+ return FocusHandle::for_id(focus_id, &cx.focus_handles);
}
None
@@ -87,7 +87,7 @@ pub trait AnimationExt {
}
}
-impl<E> AnimationExt for E {}
+impl<E: IntoElement + 'static> AnimationExt for E {}
/// A GPUI element that applies an animation to another element
pub struct AnimationElement<E> {
@@ -27,6 +27,7 @@ use crate::{
use collections::HashMap;
use refineable::Refineable;
use smallvec::SmallVec;
+use stacksafe::{StackSafe, stacksafe};
use std::{
any::{Any, TypeId},
cell::RefCell,
@@ -285,21 +286,20 @@ impl Interactivity {
{
self.mouse_move_listeners
.push(Box::new(move |event, phase, hitbox, window, cx| {
- if phase == DispatchPhase::Capture {
- if let Some(drag) = &cx.active_drag {
- if drag.value.as_ref().type_id() == TypeId::of::<T>() {
- (listener)(
- &DragMoveEvent {
- event: event.clone(),
- bounds: hitbox.bounds,
- drag: PhantomData,
- dragged_item: Arc::clone(&drag.value),
- },
- window,
- cx,
- );
- }
- }
+ if phase == DispatchPhase::Capture
+ && let Some(drag) = &cx.active_drag
+ && drag.value.as_ref().type_id() == TypeId::of::<T>()
+ {
+ (listener)(
+ &DragMoveEvent {
+ event: event.clone(),
+ bounds: hitbox.bounds,
+ drag: PhantomData,
+ dragged_item: Arc::clone(&drag.value),
+ },
+ window,
+ cx,
+ );
}
}));
}
@@ -533,7 +533,7 @@ impl Interactivity {
}
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
- /// The imperative API equivalent to [`InteractiveElement::tooltip`]
+ /// The imperative API equivalent to [`StatefulInteractiveElement::tooltip`]
pub fn tooltip(&mut self, build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static)
where
Self: Sized,
@@ -550,7 +550,7 @@ impl Interactivity {
/// Use the given callback to construct a new tooltip view when the mouse hovers over this element.
/// The tooltip itself is also hoverable and won't disappear when the user moves the mouse into
- /// the tooltip. The imperative API equivalent to [`InteractiveElement::hoverable_tooltip`]
+ /// the tooltip. The imperative API equivalent to [`StatefulInteractiveElement::hoverable_tooltip`]
pub fn hoverable_tooltip(
&mut self,
build_tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
@@ -676,7 +676,7 @@ pub trait InteractiveElement: Sized {
#[cfg(any(test, feature = "test-support"))]
/// Set a key that can be used to look up this element's bounds
- /// in the [`VisualTestContext::debug_bounds`] map
+ /// in the [`crate::VisualTestContext::debug_bounds`] map
/// This is a noop in release builds
fn debug_selector(mut self, f: impl FnOnce() -> String) -> Self {
self.interactivity().debug_selector = Some(f());
@@ -685,7 +685,7 @@ pub trait InteractiveElement: Sized {
#[cfg(not(any(test, feature = "test-support")))]
/// Set a key that can be used to look up this element's bounds
- /// in the [`VisualTestContext::debug_bounds`] map
+ /// in the [`crate::VisualTestContext::debug_bounds`] map
/// This is a noop in release builds
#[inline]
fn debug_selector(self, _: impl FnOnce() -> String) -> Self {
@@ -1087,7 +1087,7 @@ pub trait StatefulInteractiveElement: InteractiveElement {
/// On drag initiation, this callback will be used to create a new view to render the dragged value for a
/// drag and drop operation. This API should also be used as the equivalent of 'on drag start' with
- /// the [`Self::on_drag_move`] API.
+ /// the [`InteractiveElement::on_drag_move`] API.
/// The callback also has access to the offset of triggering click from the origin of parent element.
/// The fluent API equivalent to [`Interactivity::on_drag`]
///
@@ -1195,7 +1195,7 @@ pub fn div() -> Div {
/// A [`Div`] element, the all-in-one element for building complex UIs in GPUI
pub struct Div {
interactivity: Interactivity,
- children: SmallVec<[AnyElement; 2]>,
+ children: SmallVec<[StackSafe<AnyElement>; 2]>,
prepaint_listener: Option<Box<dyn Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static>>,
image_cache: Option<Box<dyn ImageCacheProvider>>,
}
@@ -1256,7 +1256,8 @@ impl InteractiveElement for Div {
impl ParentElement for Div {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
- self.children.extend(elements)
+ self.children
+ .extend(elements.into_iter().map(StackSafe::new))
}
}
@@ -1272,6 +1273,7 @@ impl Element for Div {
self.interactivity.source_location()
}
+ #[stacksafe]
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
@@ -1307,6 +1309,7 @@ impl Element for Div {
(layout_id, DivFrameState { child_layout_ids })
}
+ #[stacksafe]
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
@@ -1376,6 +1379,7 @@ impl Element for Div {
)
}
+ #[stacksafe]
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
@@ -1509,15 +1513,14 @@ impl Interactivity {
let mut element_state =
element_state.map(|element_state| element_state.unwrap_or_default());
- if let Some(element_state) = element_state.as_ref() {
- if cx.has_active_drag() {
- if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref()
- {
- *pending_mouse_down.borrow_mut() = None;
- }
- if let Some(clicked_state) = element_state.clicked_state.as_ref() {
- *clicked_state.borrow_mut() = ElementClickedState::default();
- }
+ if let Some(element_state) = element_state.as_ref()
+ && cx.has_active_drag()
+ {
+ if let Some(pending_mouse_down) = element_state.pending_mouse_down.as_ref() {
+ *pending_mouse_down.borrow_mut() = None;
+ }
+ if let Some(clicked_state) = element_state.clicked_state.as_ref() {
+ *clicked_state.borrow_mut() = ElementClickedState::default();
}
}
@@ -1525,35 +1528,35 @@ impl Interactivity {
// If there's an explicit focus handle we're tracking, use that. Otherwise
// create a new handle and store it in the element state, which lives for as
// as frames contain an element with this id.
- if self.focusable && self.tracked_focus_handle.is_none() {
- if let Some(element_state) = element_state.as_mut() {
- let mut handle = element_state
- .focus_handle
- .get_or_insert_with(|| cx.focus_handle())
- .clone()
- .tab_stop(false);
-
- if let Some(index) = self.tab_index {
- handle = handle.tab_index(index).tab_stop(true);
- }
-
- self.tracked_focus_handle = Some(handle);
+ if self.focusable
+ && self.tracked_focus_handle.is_none()
+ && let Some(element_state) = element_state.as_mut()
+ {
+ let mut handle = element_state
+ .focus_handle
+ .get_or_insert_with(|| cx.focus_handle())
+ .clone()
+ .tab_stop(false);
+
+ if let Some(index) = self.tab_index {
+ handle = handle.tab_index(index).tab_stop(true);
}
+
+ self.tracked_focus_handle = Some(handle);
}
if let Some(scroll_handle) = self.tracked_scroll_handle.as_ref() {
self.scroll_offset = Some(scroll_handle.0.borrow().offset.clone());
- } else if self.base_style.overflow.x == Some(Overflow::Scroll)
- || self.base_style.overflow.y == Some(Overflow::Scroll)
+ } else if (self.base_style.overflow.x == Some(Overflow::Scroll)
+ || self.base_style.overflow.y == Some(Overflow::Scroll))
+ && let Some(element_state) = element_state.as_mut()
{
- if let Some(element_state) = element_state.as_mut() {
- self.scroll_offset = Some(
- element_state
- .scroll_offset
- .get_or_insert_with(Rc::default)
- .clone(),
- );
- }
+ self.scroll_offset = Some(
+ element_state
+ .scroll_offset
+ .get_or_insert_with(Rc::default)
+ .clone(),
+ );
}
let style = self.compute_style_internal(None, element_state.as_mut(), window, cx);
@@ -2026,26 +2029,27 @@ impl Interactivity {
let hitbox = hitbox.clone();
window.on_mouse_event({
move |_: &MouseUpEvent, phase, window, cx| {
- if let Some(drag) = &cx.active_drag {
- if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
- let drag_state_type = drag.value.as_ref().type_id();
- for (drop_state_type, listener) in &drop_listeners {
- if *drop_state_type == drag_state_type {
- let drag = cx
- .active_drag
- .take()
- .expect("checked for type drag state type above");
-
- let mut can_drop = true;
- if let Some(predicate) = &can_drop_predicate {
- can_drop = predicate(drag.value.as_ref(), window, cx);
- }
+ if let Some(drag) = &cx.active_drag
+ && phase == DispatchPhase::Bubble
+ && hitbox.is_hovered(window)
+ {
+ let drag_state_type = drag.value.as_ref().type_id();
+ for (drop_state_type, listener) in &drop_listeners {
+ if *drop_state_type == drag_state_type {
+ let drag = cx
+ .active_drag
+ .take()
+ .expect("checked for type drag state type above");
+
+ let mut can_drop = true;
+ if let Some(predicate) = &can_drop_predicate {
+ can_drop = predicate(drag.value.as_ref(), window, cx);
+ }
- if can_drop {
- listener(drag.value.as_ref(), window, cx);
- window.refresh();
- cx.stop_propagation();
- }
+ if can_drop {
+ listener(drag.value.as_ref(), window, cx);
+ window.refresh();
+ cx.stop_propagation();
}
}
}
@@ -2089,31 +2093,24 @@ impl Interactivity {
}
let mut pending_mouse_down = pending_mouse_down.borrow_mut();
- if let Some(mouse_down) = pending_mouse_down.clone() {
- if !cx.has_active_drag()
- && (event.position - mouse_down.position).magnitude()
- > DRAG_THRESHOLD
- {
- if let Some((drag_value, drag_listener)) = drag_listener.take() {
- *clicked_state.borrow_mut() = ElementClickedState::default();
- let cursor_offset = event.position - hitbox.origin;
- let drag = (drag_listener)(
- drag_value.as_ref(),
- cursor_offset,
- window,
- cx,
- );
- cx.active_drag = Some(AnyDrag {
- view: drag,
- value: drag_value,
- cursor_offset,
- cursor_style: drag_cursor_style,
- });
- pending_mouse_down.take();
- window.refresh();
- cx.stop_propagation();
- }
- }
+ if let Some(mouse_down) = pending_mouse_down.clone()
+ && !cx.has_active_drag()
+ && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
+ && let Some((drag_value, drag_listener)) = drag_listener.take()
+ {
+ *clicked_state.borrow_mut() = ElementClickedState::default();
+ let cursor_offset = event.position - hitbox.origin;
+ let drag =
+ (drag_listener)(drag_value.as_ref(), cursor_offset, window, cx);
+ cx.active_drag = Some(AnyDrag {
+ view: drag,
+ value: drag_value,
+ cursor_offset,
+ cursor_style: drag_cursor_style,
+ });
+ pending_mouse_down.take();
+ window.refresh();
+ cx.stop_propagation();
}
}
});
@@ -2277,7 +2274,7 @@ impl Interactivity {
window.on_mouse_event(move |_: &MouseDownEvent, phase, window, _cx| {
if phase == DispatchPhase::Bubble && !window.default_prevented() {
let group_hovered = active_group_hitbox
- .map_or(false, |group_hitbox_id| group_hitbox_id.is_hovered(window));
+ .is_some_and(|group_hitbox_id| group_hitbox_id.is_hovered(window));
let element_hovered = hitbox.is_hovered(window);
if group_hovered || element_hovered {
*active_state.borrow_mut() = ElementClickedState {
@@ -2423,33 +2420,32 @@ impl Interactivity {
style.refine(&self.base_style);
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
- if let Some(in_focus_style) = self.in_focus_style.as_ref() {
- if focus_handle.within_focused(window, cx) {
- style.refine(in_focus_style);
- }
+ if let Some(in_focus_style) = self.in_focus_style.as_ref()
+ && focus_handle.within_focused(window, cx)
+ {
+ style.refine(in_focus_style);
}
- if let Some(focus_style) = self.focus_style.as_ref() {
- if focus_handle.is_focused(window) {
- style.refine(focus_style);
- }
+ if let Some(focus_style) = self.focus_style.as_ref()
+ && focus_handle.is_focused(window)
+ {
+ style.refine(focus_style);
}
}
if let Some(hitbox) = hitbox {
if !cx.has_active_drag() {
- if let Some(group_hover) = self.group_hover_style.as_ref() {
- if let Some(group_hitbox_id) = GroupHitboxes::get(&group_hover.group, cx) {
- if group_hitbox_id.is_hovered(window) {
- style.refine(&group_hover.style);
- }
- }
+ 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() {
- if hitbox.is_hovered(window) {
- style.refine(hover_style);
- }
+ if let Some(hover_style) = self.hover_style.as_ref()
+ && hitbox.is_hovered(window)
+ {
+ style.refine(hover_style);
}
}
@@ -2463,12 +2459,10 @@ impl Interactivity {
for (state_type, group_drag_style) in &self.group_drag_over_styles {
if let Some(group_hitbox_id) =
GroupHitboxes::get(&group_drag_style.group, cx)
+ && *state_type == drag.value.as_ref().type_id()
+ && group_hitbox_id.is_hovered(window)
{
- if *state_type == drag.value.as_ref().type_id()
- && group_hitbox_id.is_hovered(window)
- {
- style.refine(&group_drag_style.style);
- }
+ style.refine(&group_drag_style.style);
}
}
@@ -2490,16 +2484,16 @@ impl Interactivity {
.clicked_state
.get_or_insert_with(Default::default)
.borrow();
- if clicked_state.group {
- if let Some(group) = self.group_active_style.as_ref() {
- style.refine(&group.style)
- }
+ if clicked_state.group
+ && let Some(group) = self.group_active_style.as_ref()
+ {
+ style.refine(&group.style)
}
- if let Some(active_style) = self.active_style.as_ref() {
- if clicked_state.element {
- style.refine(active_style)
- }
+ if let Some(active_style) = self.active_style.as_ref()
+ && clicked_state.element
+ {
+ style.refine(active_style)
}
}
@@ -2620,7 +2614,7 @@ pub(crate) fn register_tooltip_mouse_handlers(
window.on_mouse_event({
let active_tooltip = active_tooltip.clone();
move |_: &MouseDownEvent, _phase, window: &mut Window, _cx| {
- if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) {
+ if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) {
clear_active_tooltip_if_not_hoverable(&active_tooltip, window);
}
}
@@ -2629,7 +2623,7 @@ pub(crate) fn register_tooltip_mouse_handlers(
window.on_mouse_event({
let active_tooltip = active_tooltip.clone();
move |_: &ScrollWheelEvent, _phase, window: &mut Window, _cx| {
- if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(window)) {
+ if !tooltip_id.is_some_and(|tooltip_id| tooltip_id.is_hovered(window)) {
clear_active_tooltip_if_not_hoverable(&active_tooltip, window);
}
}
@@ -2785,7 +2779,7 @@ fn handle_tooltip_check_visible_and_update(
match action {
Action::None => {}
- Action::Hide => clear_active_tooltip(&active_tooltip, window),
+ Action::Hide => clear_active_tooltip(active_tooltip, window),
Action::ScheduleHide(tooltip) => {
let delayed_hide_task = window.spawn(cx, {
let active_tooltip = active_tooltip.clone();
@@ -64,7 +64,7 @@ mod any_image_cache {
cx: &mut App,
) -> Option<Result<Arc<RenderImage>, ImageCacheError>> {
let image_cache = image_cache.clone().downcast::<I>().unwrap();
- return image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx));
+ image_cache.update(cx, |image_cache, cx| image_cache.load(resource, window, cx))
}
}
@@ -297,10 +297,10 @@ impl RetainAllImageCache {
/// Remove the image from the cache by the given source.
pub fn remove(&mut self, source: &Resource, window: &mut Window, cx: &mut App) {
let hash = hash(source);
- if let Some(mut item) = self.0.remove(&hash) {
- if let Some(Ok(image)) = item.get() {
- cx.drop_image(image, Some(window));
- }
+ if let Some(mut item) = self.0.remove(&hash)
+ && let Some(Ok(image)) = item.get()
+ {
+ cx.drop_image(image, Some(window));
}
}
@@ -332,20 +332,18 @@ impl Element for Img {
state.started_loading = None;
}
- let image_size = data.size(frame_index);
- style.aspect_ratio =
- Some(image_size.width.0 as f32 / image_size.height.0 as f32);
+ let image_size = data.render_size(frame_index);
+ style.aspect_ratio = Some(image_size.width / image_size.height);
if let Length::Auto = style.size.width {
style.size.width = match style.size.height {
Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(height),
)) => Length::Definite(
- px(image_size.width.0 as f32 * height.0
- / image_size.height.0 as f32)
- .into(),
+ px(image_size.width.0 * height.0 / image_size.height.0)
+ .into(),
),
- _ => Length::Definite(px(image_size.width.0 as f32).into()),
+ _ => Length::Definite(image_size.width.into()),
};
}
@@ -354,11 +352,10 @@ impl Element for Img {
Length::Definite(DefiniteLength::Absolute(
AbsoluteLength::Pixels(width),
)) => Length::Definite(
- px(image_size.height.0 as f32 * width.0
- / image_size.width.0 as f32)
- .into(),
+ px(image_size.height.0 * width.0 / image_size.width.0)
+ .into(),
),
- _ => Length::Definite(px(image_size.height.0 as f32).into()),
+ _ => Length::Definite(image_size.height.into()),
};
}
@@ -379,13 +376,12 @@ impl Element for Img {
None => {
if let Some(state) = &mut state {
if let Some((started_loading, _)) = state.started_loading {
- if started_loading.elapsed() > LOADING_DELAY {
- if let Some(loading) = self.style.loading.as_ref() {
- let mut element = loading();
- replacement_id =
- Some(element.request_layout(window, cx));
- layout_state.replacement = Some(element);
- }
+ if started_loading.elapsed() > LOADING_DELAY
+ && let Some(loading) = self.style.loading.as_ref()
+ {
+ let mut element = loading();
+ replacement_id = Some(element.request_layout(window, cx));
+ layout_state.replacement = Some(element);
}
} else {
let current_view = window.current_view();
@@ -476,7 +472,7 @@ impl Element for Img {
.paint_image(
new_bounds,
corner_radii,
- data.clone(),
+ data,
layout_state.frame_index,
self.style.grayscale,
)
@@ -702,7 +698,9 @@ impl Asset for ImageAssetLoader {
swap_rgba_pa_to_bgra(pixel);
}
- RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1))
+ let mut image = RenderImage::new(SmallVec::from_elem(Frame::new(buffer), 1));
+ image.scale_factor = SMOOTH_SVG_SCALE_FACTOR;
+ image
};
Ok(Arc::new(data))
@@ -5,7 +5,7 @@
//! In order to minimize re-renders, this element's state is stored intrusively
//! on your own views, so that your code can coordinate directly with the list element's cached state.
//!
-//! If all of your elements are the same height, see [`UniformList`] for a simpler API
+//! If all of your elements are the same height, see [`crate::UniformList`] for a simpler API
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
@@ -235,7 +235,7 @@ impl ListState {
}
/// Register with the list state that the items in `old_range` have been replaced
- /// by new items. As opposed to [`splice`], this method allows an iterator of optional focus handles
+ /// by new items. As opposed to [`Self::splice`], this method allows an iterator of optional focus handles
/// to be supplied to properly integrate with items in the list that can be focused. If a focused item
/// is scrolled out of view, the list will continue to render it to allow keyboard interaction.
pub fn splice_focusable(
@@ -732,46 +732,44 @@ impl StateInner {
item.element.prepaint_at(item_origin, window, cx);
});
- if let Some(autoscroll_bounds) = window.take_autoscroll() {
- if autoscroll {
- if autoscroll_bounds.top() < bounds.top() {
- return Err(ListOffset {
- item_ix: item.index,
- offset_in_item: autoscroll_bounds.top() - item_origin.y,
- });
- } else if autoscroll_bounds.bottom() > bounds.bottom() {
- let mut cursor = self.items.cursor::<Count>(&());
- cursor.seek(&Count(item.index), Bias::Right);
- let mut height = bounds.size.height - padding.top - padding.bottom;
-
- // Account for the height of the element down until the autoscroll bottom.
- height -= autoscroll_bounds.bottom() - item_origin.y;
-
- // Keep decreasing the scroll top until we fill all the available space.
- while height > Pixels::ZERO {
- cursor.prev();
- let Some(item) = cursor.item() else { break };
-
- let size = item.size().unwrap_or_else(|| {
- let mut item = render_item(cursor.start().0, window, cx);
- let item_available_size = size(
- bounds.size.width.into(),
- AvailableSpace::MinContent,
- );
- item.layout_as_root(item_available_size, window, cx)
- });
- height -= size.height;
- }
-
- return Err(ListOffset {
- item_ix: cursor.start().0,
- offset_in_item: if height < Pixels::ZERO {
- -height
- } else {
- Pixels::ZERO
- },
+ if let Some(autoscroll_bounds) = window.take_autoscroll()
+ && autoscroll
+ {
+ if autoscroll_bounds.top() < bounds.top() {
+ return Err(ListOffset {
+ item_ix: item.index,
+ offset_in_item: autoscroll_bounds.top() - item_origin.y,
+ });
+ } else if autoscroll_bounds.bottom() > bounds.bottom() {
+ let mut cursor = self.items.cursor::<Count>(&());
+ cursor.seek(&Count(item.index), Bias::Right);
+ let mut height = bounds.size.height - padding.top - padding.bottom;
+
+ // Account for the height of the element down until the autoscroll bottom.
+ height -= autoscroll_bounds.bottom() - item_origin.y;
+
+ // Keep decreasing the scroll top until we fill all the available space.
+ while height > Pixels::ZERO {
+ cursor.prev();
+ let Some(item) = cursor.item() else { break };
+
+ let size = item.size().unwrap_or_else(|| {
+ let mut item = render_item(cursor.start().0, window, cx);
+ let item_available_size =
+ size(bounds.size.width.into(), AvailableSpace::MinContent);
+ item.layout_as_root(item_available_size, window, cx)
});
+ height -= size.height;
}
+
+ return Err(ListOffset {
+ item_ix: cursor.start().0,
+ offset_in_item: if height < Pixels::ZERO {
+ -height
+ } else {
+ Pixels::ZERO
+ },
+ });
}
}
@@ -940,9 +938,10 @@ impl Element for List {
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
// If the width of the list has changed, invalidate all cached item heights
- if state.last_layout_bounds.map_or(true, |last_bounds| {
- last_bounds.size.width != bounds.size.width
- }) {
+ if state
+ .last_layout_bounds
+ .is_none_or(|last_bounds| last_bounds.size.width != bounds.size.width)
+ {
let new_items = SumTree::from_iter(
state.items.iter().map(|item| ListItem::Unmeasured {
focus_handle: item.focus_handle(),
@@ -326,7 +326,7 @@ impl TextLayout {
vec![text_style.to_run(text.len())]
};
- let layout_id = window.request_measured_layout(Default::default(), {
+ window.request_measured_layout(Default::default(), {
let element_state = self.clone();
move |known_dimensions, available_space, window, cx| {
@@ -356,12 +356,11 @@ impl TextLayout {
(None, "".into())
};
- if let Some(text_layout) = element_state.0.borrow().as_ref() {
- if text_layout.size.is_some()
- && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
- {
- return text_layout.size.unwrap();
- }
+ if let Some(text_layout) = element_state.0.borrow().as_ref()
+ && text_layout.size.is_some()
+ && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
+ {
+ return text_layout.size.unwrap();
}
let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
@@ -417,9 +416,7 @@ impl TextLayout {
size
}
- });
-
- layout_id
+ })
}
fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
@@ -763,14 +760,13 @@ impl Element for InteractiveText {
let mut interactive_state = interactive_state.unwrap_or_default();
if let Some(click_listener) = self.click_listener.take() {
let mouse_position = window.mouse_position();
- if let Ok(ix) = text_layout.index_for_position(mouse_position) {
- if self
+ if let Ok(ix) = text_layout.index_for_position(mouse_position)
+ && self
.clickable_ranges
.iter()
.any(|range| range.contains(&ix))
- {
- window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
- }
+ {
+ window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
}
let text_layout = text_layout.clone();
@@ -803,13 +799,13 @@ impl Element for InteractiveText {
} else {
let hitbox = hitbox.clone();
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
- if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
- if let Ok(mouse_down_index) =
+ if phase == DispatchPhase::Bubble
+ && hitbox.is_hovered(window)
+ && let Ok(mouse_down_index) =
text_layout.index_for_position(event.position)
- {
- mouse_down.set(Some(mouse_down_index));
- window.refresh();
- }
+ {
+ mouse_down.set(Some(mouse_down_index));
+ window.refresh();
}
});
}
@@ -391,7 +391,7 @@ impl BackgroundExecutor {
}
/// in tests, run all tasks that are ready to run. If after doing so
- /// the test still has outstanding tasks, this will panic. (See also `allow_parking`)
+ /// the test still has outstanding tasks, this will panic. (See also [`Self::allow_parking`])
#[cfg(any(test, feature = "test-support"))]
pub fn run_until_parked(&self) {
self.dispatcher.as_test().unwrap().run_until_parked()
@@ -405,7 +405,7 @@ impl BackgroundExecutor {
self.dispatcher.as_test().unwrap().allow_parking();
}
- /// undoes the effect of [`allow_parking`].
+ /// undoes the effect of [`Self::allow_parking`].
#[cfg(any(test, feature = "test-support"))]
pub fn forbid_parking(&self) {
self.dispatcher.as_test().unwrap().forbid_parking();
@@ -480,7 +480,7 @@ impl ForegroundExecutor {
/// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics.
///
/// Copy-modified from:
-/// https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L405
+/// <https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L405>
#[track_caller]
fn spawn_local_with_source_location<Fut, S>(
future: Fut,
@@ -1046,7 +1046,7 @@ where
size: self.size.clone()
+ size(
amount.left.clone() + amount.right.clone(),
- amount.top.clone() + amount.bottom.clone(),
+ amount.top.clone() + amount.bottom,
),
}
}
@@ -1159,10 +1159,10 @@ where
/// Computes the space available within outer bounds.
pub fn space_within(&self, outer: &Self) -> Edges<T> {
Edges {
- top: self.top().clone() - outer.top().clone(),
- right: outer.right().clone() - self.right().clone(),
- bottom: outer.bottom().clone() - self.bottom().clone(),
- left: self.left().clone() - outer.left().clone(),
+ top: self.top() - outer.top(),
+ right: outer.right() - self.right(),
+ bottom: outer.bottom() - self.bottom(),
+ left: self.left() - outer.left(),
}
}
}
@@ -1641,7 +1641,7 @@ impl Bounds<Pixels> {
}
/// Convert the bounds from logical pixels to physical pixels
- pub fn to_device_pixels(&self, factor: f32) -> Bounds<DevicePixels> {
+ pub fn to_device_pixels(self, factor: f32) -> Bounds<DevicePixels> {
Bounds {
origin: point(
DevicePixels((self.origin.x.0 * factor).round() as i32),
@@ -1712,7 +1712,7 @@ where
top: self.top.clone() * rhs.top,
right: self.right.clone() * rhs.right,
bottom: self.bottom.clone() * rhs.bottom,
- left: self.left.clone() * rhs.left,
+ left: self.left * rhs.left,
}
}
}
@@ -1957,7 +1957,7 @@ impl Edges<DefiniteLength> {
/// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems
/// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width
/// ```
- pub fn to_pixels(&self, parent_size: Size<AbsoluteLength>, rem_size: Pixels) -> Edges<Pixels> {
+ pub fn to_pixels(self, parent_size: Size<AbsoluteLength>, rem_size: Pixels) -> Edges<Pixels> {
Edges {
top: self.top.to_pixels(parent_size.height, rem_size),
right: self.right.to_pixels(parent_size.width, rem_size),
@@ -2027,7 +2027,7 @@ impl Edges<AbsoluteLength> {
/// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels
/// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels
/// ```
- pub fn to_pixels(&self, rem_size: Pixels) -> Edges<Pixels> {
+ pub fn to_pixels(self, rem_size: Pixels) -> Edges<Pixels> {
Edges {
top: self.top.to_pixels(rem_size),
right: self.right.to_pixels(rem_size),
@@ -2272,7 +2272,7 @@ impl Corners<AbsoluteLength> {
/// assert_eq!(corners_in_pixels.bottom_right, Pixels(30.0));
/// assert_eq!(corners_in_pixels.bottom_left, Pixels(32.0)); // 2 rems converted to pixels
/// ```
- pub fn to_pixels(&self, rem_size: Pixels) -> Corners<Pixels> {
+ pub fn to_pixels(self, rem_size: Pixels) -> Corners<Pixels> {
Corners {
top_left: self.top_left.to_pixels(rem_size),
top_right: self.top_right.to_pixels(rem_size),
@@ -2411,7 +2411,7 @@ where
top_left: self.top_left.clone() * rhs.top_left,
top_right: self.top_right.clone() * rhs.top_right,
bottom_right: self.bottom_right.clone() * rhs.bottom_right,
- bottom_left: self.bottom_left.clone() * rhs.bottom_left,
+ bottom_left: self.bottom_left * rhs.bottom_left,
}
}
}
@@ -2858,7 +2858,7 @@ impl DevicePixels {
/// let total_bytes = pixels.to_bytes(bytes_per_pixel);
/// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes
/// ```
- pub fn to_bytes(&self, bytes_per_pixel: u8) -> u32 {
+ pub fn to_bytes(self, bytes_per_pixel: u8) -> u32 {
self.0 as u32 * bytes_per_pixel as u32
}
}
@@ -3073,8 +3073,8 @@ pub struct Rems(pub f32);
impl Rems {
/// Convert this Rem value to pixels.
- pub fn to_pixels(&self, rem_size: Pixels) -> Pixels {
- *self * rem_size
+ pub fn to_pixels(self, rem_size: Pixels) -> Pixels {
+ self * rem_size
}
}
@@ -3168,9 +3168,9 @@ impl AbsoluteLength {
/// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels(42.0));
/// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels(32.0));
/// ```
- pub fn to_pixels(&self, rem_size: Pixels) -> Pixels {
+ pub fn to_pixels(self, rem_size: Pixels) -> Pixels {
match self {
- AbsoluteLength::Pixels(pixels) => *pixels,
+ AbsoluteLength::Pixels(pixels) => pixels,
AbsoluteLength::Rems(rems) => rems.to_pixels(rem_size),
}
}
@@ -3184,10 +3184,10 @@ impl AbsoluteLength {
/// # Returns
///
/// Returns the `AbsoluteLength` as `Pixels`.
- pub fn to_rems(&self, rem_size: Pixels) -> Rems {
+ pub fn to_rems(self, rem_size: Pixels) -> Rems {
match self {
AbsoluteLength::Pixels(pixels) => Rems(pixels.0 / rem_size.0),
- AbsoluteLength::Rems(rems) => *rems,
+ AbsoluteLength::Rems(rems) => rems,
}
}
}
@@ -3315,12 +3315,12 @@ impl DefiniteLength {
/// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels(32.0));
/// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels(50.0));
/// ```
- pub fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels {
+ pub fn to_pixels(self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels {
match self {
DefiniteLength::Absolute(size) => size.to_pixels(rem_size),
DefiniteLength::Fraction(fraction) => match base_size {
- AbsoluteLength::Pixels(px) => px * *fraction,
- AbsoluteLength::Rems(rems) => rems * rem_size * *fraction,
+ AbsoluteLength::Pixels(px) => px * fraction,
+ AbsoluteLength::Rems(rems) => rems * rem_size * fraction,
},
}
}
@@ -24,7 +24,7 @@
//! - State management and communication with [`Entity`]'s. Whenever you need to store application state
//! that communicates between different parts of your application, you'll want to use GPUI's
//! entities. Entities are owned by GPUI and are only accessible through an owned smart pointer
-//! similar to an [`std::rc::Rc`]. See the [`app::context`] module for more information.
+//! similar to an [`std::rc::Rc`]. See [`app::Context`] for more information.
//!
//! - High level, declarative UI with views. All UI in GPUI starts with a view. A view is simply
//! a [`Entity`] that can be rendered, by implementing the [`Render`] trait. At the start of each frame, GPUI
@@ -37,7 +37,7 @@
//! provide a nice wrapper around an imperative API that provides as much flexibility and control as
//! you need. Elements have total control over how they and their child elements are rendered and
//! can be used for making efficient views into large lists, implement custom layouting for a code editor,
-//! and anything else you can think of. See the [`element`] module for more information.
+//! and anything else you can think of. See the [`elements`] module for more information.
//!
//! Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services.
//! This context is your main interface to GPUI, and is used extensively throughout the framework.
@@ -51,9 +51,9 @@
//! Use this for implementing keyboard shortcuts, such as cmd-q (See `action` module for more information).
//! - Platform services, such as `quit the app` or `open a URL` are available as methods on the [`app::App`].
//! - An async executor that is integrated with the platform's event loop. See the [`executor`] module for more information.,
-//! - The [`gpui::test`](test) macro provides a convenient way to write tests for your GPUI applications. Tests also have their
-//! own kind of context, a [`TestAppContext`] which provides ways of simulating common platform input. See [`app::test_context`]
-//! and [`test`] modules for more details.
+//! - The [`gpui::test`](macro@test) macro provides a convenient way to write tests for your GPUI applications. Tests also have their
+//! own kind of context, a [`TestAppContext`] which provides ways of simulating common platform input. See [`TestAppContext`]
+//! and [`mod@test`] modules for more details.
//!
//! Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop
//! a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples,
@@ -117,7 +117,7 @@ pub mod private {
mod seal {
/// A mechanism for restricting implementations of a trait to only those in GPUI.
- /// See: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/
+ /// See: <https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/>
pub trait Sealed {}
}
@@ -157,7 +157,7 @@ pub use taffy::{AvailableSpace, LayoutId};
#[cfg(any(test, feature = "test-support"))]
pub use test::*;
pub use text_system::*;
-pub use util::arc_cow::ArcCow;
+pub use util::{FutureExt, Timeout, arc_cow::ArcCow};
pub use view::*;
pub use window::*;
@@ -172,6 +172,10 @@ pub trait AppContext {
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,
@@ -348,7 +352,7 @@ impl<T> Flatten<T> for Result<T> {
}
/// Information about the GPU GPUI is running on.
-#[derive(Default, Debug)]
+#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct GpuSpecs {
/// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU.
pub is_software_emulated: bool,
@@ -72,7 +72,7 @@ pub trait EntityInputHandler: 'static + Sized {
) -> Option<usize>;
}
-/// The canonical implementation of [`PlatformInputHandler`]. Call [`Window::handle_input`]
+/// The canonical implementation of [`crate::PlatformInputHandler`]. Call [`Window::handle_input`]
/// with an instance during your element's paint.
pub struct ElementInputHandler<V> {
view: Entity<V>,
@@ -164,7 +164,7 @@ mod conditional {
if let Some(render_inspector) = cx
.inspector_element_registry
.renderers_by_type_id
- .remove(&type_id)
+ .remove(type_id)
{
let mut element = (render_inspector)(
active_element.id.clone(),
@@ -408,7 +408,7 @@ impl DispatchTree {
keymap
.bindings_for_action(action)
.filter(|binding| {
- Self::binding_matches_predicate_and_not_shadowed(&keymap, &binding, context_stack)
+ Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack)
})
.cloned()
.collect()
@@ -426,7 +426,7 @@ impl DispatchTree {
.bindings_for_action(action)
.rev()
.find(|binding| {
- Self::binding_matches_predicate_and_not_shadowed(&keymap, &binding, context_stack)
+ Self::binding_matches_predicate_and_not_shadowed(&keymap, binding, context_stack)
})
.cloned()
}
@@ -458,7 +458,7 @@ impl DispatchTree {
.keymap
.borrow()
.bindings_for_input(input, &context_stack);
- return (bindings, partial, context_stack);
+ (bindings, partial, context_stack)
}
/// dispatch_key processes the keystroke
@@ -552,7 +552,7 @@ impl DispatchTree {
let mut current_node_id = Some(target);
while let Some(node_id) = current_node_id {
dispatch_path.push(node_id);
- current_node_id = self.nodes[node_id.0].parent;
+ current_node_id = self.nodes.get(node_id.0).and_then(|node| node.parent);
}
dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node.
dispatch_path
@@ -639,10 +639,7 @@ mod tests {
}
fn partial_eq(&self, action: &dyn Action) -> bool {
- action
- .as_any()
- .downcast_ref::<Self>()
- .map_or(false, |a| self == a)
+ action.as_any().downcast_ref::<Self>() == Some(self)
}
fn boxed_clone(&self) -> std::boxed::Box<dyn Action> {
@@ -4,7 +4,7 @@ mod context;
pub use binding::*;
pub use context::*;
-use crate::{Action, Keystroke, is_no_action};
+use crate::{Action, AsKeystroke, Keystroke, is_no_action};
use collections::{HashMap, HashSet};
use smallvec::SmallVec;
use std::any::TypeId;
@@ -141,14 +141,14 @@ impl Keymap {
/// only.
pub fn bindings_for_input(
&self,
- input: &[Keystroke],
+ input: &[impl AsKeystroke],
context_stack: &[KeyContext],
) -> (SmallVec<[KeyBinding; 1]>, bool) {
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
let mut pending_bindings = SmallVec::<[(BindingIndex, &KeyBinding); 1]>::new();
for (ix, binding) in self.bindings().enumerate().rev() {
- let Some(depth) = self.binding_enabled(binding, &context_stack) else {
+ let Some(depth) = self.binding_enabled(binding, context_stack) else {
continue;
};
let Some(pending) = binding.match_keystrokes(input) else {
@@ -192,7 +192,6 @@ impl Keymap {
(bindings, !pending.is_empty())
}
-
/// Check if the given binding is enabled, given a certain key context.
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
@@ -264,7 +263,7 @@ mod tests {
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
let (result, pending) = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-a").unwrap()],
@@ -290,7 +289,7 @@ mod tests {
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
// binding is only enabled in a specific context
assert!(
@@ -344,7 +343,7 @@ mod tests {
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
let space = || Keystroke::parse("space").unwrap();
let w = || Keystroke::parse("w").unwrap();
@@ -364,29 +363,29 @@ mod tests {
// Ensure `space` results in pending input on the workspace, but not editor
let space_workspace = keymap.bindings_for_input(&[space()], &workspace_context());
assert!(space_workspace.0.is_empty());
- assert_eq!(space_workspace.1, true);
+ assert!(space_workspace.1);
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
- assert_eq!(space_editor.1, false);
+ assert!(!space_editor.1);
// Ensure `space w` results in pending input on the workspace, but not editor
let space_w_workspace = keymap.bindings_for_input(&space_w, &workspace_context());
assert!(space_w_workspace.0.is_empty());
- assert_eq!(space_w_workspace.1, true);
+ assert!(space_w_workspace.1);
let space_w_editor = keymap.bindings_for_input(&space_w, &editor_workspace_context());
assert!(space_w_editor.0.is_empty());
- assert_eq!(space_w_editor.1, false);
+ assert!(!space_w_editor.1);
// Ensure `space w w` results in the binding in the workspace, but not in the editor
let space_w_w_workspace = keymap.bindings_for_input(&space_w_w, &workspace_context());
assert!(!space_w_w_workspace.0.is_empty());
- assert_eq!(space_w_w_workspace.1, false);
+ assert!(!space_w_w_workspace.1);
let space_w_w_editor = keymap.bindings_for_input(&space_w_w, &editor_workspace_context());
assert!(space_w_w_editor.0.is_empty());
- assert_eq!(space_w_w_editor.1, false);
+ assert!(!space_w_w_editor.1);
// Now test what happens if we have another binding defined AFTER the NoAction
// that should result in pending
@@ -396,11 +395,11 @@ mod tests {
KeyBinding::new("space w x", ActionAlpha {}, Some("editor")),
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
- assert_eq!(space_editor.1, true);
+ assert!(space_editor.1);
// Now test what happens if we have another binding defined BEFORE the NoAction
// that should result in pending
@@ -410,11 +409,11 @@ mod tests {
KeyBinding::new("space w w", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
- assert_eq!(space_editor.1, true);
+ assert!(space_editor.1);
// Now test what happens if we have another binding defined at a higher context
// that should result in pending
@@ -424,11 +423,11 @@ mod tests {
KeyBinding::new("space w w", NoAction {}, Some("editor")),
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
let space_editor = keymap.bindings_for_input(&[space()], &editor_workspace_context());
assert!(space_editor.0.is_empty());
- assert_eq!(space_editor.1, true);
+ assert!(space_editor.1);
}
#[test]
@@ -439,7 +438,7 @@ mod tests {
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
// Ensure `space` results in pending input on the workspace, but not editor
let (result, pending) = keymap.bindings_for_input(
@@ -447,7 +446,7 @@ mod tests {
&[KeyContext::parse("editor").unwrap()],
);
assert!(result.is_empty());
- assert_eq!(pending, true);
+ assert!(pending);
let bindings = [
KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")),
@@ -455,7 +454,7 @@ mod tests {
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
// Ensure `space` results in pending input on the workspace, but not editor
let (result, pending) = keymap.bindings_for_input(
@@ -463,7 +462,7 @@ mod tests {
&[KeyContext::parse("editor").unwrap()],
);
assert_eq!(result.len(), 1);
- assert_eq!(pending, false);
+ assert!(!pending);
}
#[test]
@@ -474,7 +473,7 @@ mod tests {
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
// Ensure `space` results in pending input on the workspace, but not editor
let (result, pending) = keymap.bindings_for_input(
@@ -482,7 +481,7 @@ mod tests {
&[KeyContext::parse("editor").unwrap()],
);
assert!(result.is_empty());
- assert_eq!(pending, false);
+ assert!(!pending);
}
#[test]
@@ -494,7 +493,7 @@ mod tests {
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
// Ensure `space` results in pending input on the workspace, but not editor
let (result, pending) = keymap.bindings_for_input(
@@ -505,7 +504,7 @@ mod tests {
],
);
assert_eq!(result.len(), 1);
- assert_eq!(pending, false);
+ assert!(!pending);
}
#[test]
@@ -516,7 +515,7 @@ mod tests {
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
// Ensure `space` results in pending input on the workspace, but not editor
let (result, pending) = keymap.bindings_for_input(
@@ -527,7 +526,7 @@ mod tests {
],
);
assert_eq!(result.len(), 0);
- assert_eq!(pending, false);
+ assert!(!pending);
}
#[test]
@@ -537,7 +536,7 @@ mod tests {
KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")),
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
let matched = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
@@ -560,7 +559,7 @@ mod tests {
KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")),
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
let matched = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
@@ -579,7 +578,7 @@ mod tests {
KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")),
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
let matched = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
@@ -602,7 +601,7 @@ mod tests {
KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")),
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
let matched = keymap.bindings_for_input(
&[Keystroke::parse("ctrl-x")].map(Result::unwrap),
@@ -629,7 +628,7 @@ mod tests {
];
let mut keymap = Keymap::default();
- keymap.add_bindings(bindings.clone());
+ keymap.add_bindings(bindings);
assert_bindings(&keymap, &ActionAlpha {}, &["ctrl-a"]);
assert_bindings(&keymap, &ActionBeta {}, &[]);
@@ -639,7 +638,7 @@ mod tests {
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
let actual = keymap
.bindings_for_action(action)
- .map(|binding| binding.keystrokes[0].unparse())
+ .map(|binding| binding.keystrokes[0].inner().unparse())
.collect::<Vec<_>>();
assert_eq!(actual, expected, "{:?}", action);
}
@@ -1,14 +1,15 @@
use std::rc::Rc;
-use collections::HashMap;
-
-use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
+use crate::{
+ Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
+ KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
+};
use smallvec::SmallVec;
/// A keybinding and its associated metadata, from the keymap.
pub struct KeyBinding {
pub(crate) action: Box<dyn Action>,
- pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
+ pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
pub(crate) meta: Option<KeyBindingMetaIndex>,
/// The json input string used when building the keybinding, if any
@@ -30,12 +31,17 @@ impl Clone for KeyBinding {
impl KeyBinding {
/// Construct a new keybinding from the given data. Panics on parse error.
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
- let context_predicate = if let Some(context) = context {
- Some(KeyBindingContextPredicate::parse(context).unwrap().into())
- } else {
- None
- };
- Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
+ let context_predicate =
+ context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
+ Self::load(
+ keystrokes,
+ Box::new(action),
+ context_predicate,
+ false,
+ None,
+ &DummyKeyboardMapper,
+ )
+ .unwrap()
}
/// Load a keybinding from the given raw data.
@@ -43,24 +49,22 @@ impl KeyBinding {
keystrokes: &str,
action: Box<dyn Action>,
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
- key_equivalents: Option<&HashMap<char, char>>,
+ use_key_equivalents: bool,
action_input: Option<SharedString>,
+ keyboard_mapper: &dyn PlatformKeyboardMapper,
) -> std::result::Result<Self, InvalidKeystrokeError> {
- let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
+ let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
.split_whitespace()
- .map(Keystroke::parse)
+ .map(|source| {
+ let keystroke = Keystroke::parse(source)?;
+ Ok(KeybindingKeystroke::new_with_mapper(
+ keystroke,
+ use_key_equivalents,
+ keyboard_mapper,
+ ))
+ })
.collect::<std::result::Result<_, _>>()?;
- if let Some(equivalents) = key_equivalents {
- for keystroke in keystrokes.iter_mut() {
- if keystroke.key.chars().count() == 1 {
- if let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) {
- keystroke.key = key.to_string();
- }
- }
- }
- }
-
Ok(Self {
keystrokes,
action,
@@ -82,13 +86,13 @@ impl KeyBinding {
}
/// Check if the given keystrokes match this binding.
- pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
+ pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option<bool> {
if self.keystrokes.len() < typed.len() {
return None;
}
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
- if !typed.should_match(target) {
+ if !typed.as_keystroke().should_match(target) {
return None;
}
}
@@ -97,7 +101,7 @@ impl KeyBinding {
}
/// Get the keystrokes associated with this binding
- pub fn keystrokes(&self) -> &[Keystroke] {
+ pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
self.keystrokes.as_slice()
}
@@ -287,7 +287,7 @@ impl KeyBindingContextPredicate {
return false;
}
}
- return true;
+ true
}
// Workspace > Pane > Editor
//
@@ -305,7 +305,7 @@ impl KeyBindingContextPredicate {
return true;
}
}
- return false;
+ false
}
Self::And(left, right) => {
left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts)
@@ -668,11 +668,7 @@ mod tests {
let contexts = vec![other_context.clone(), child_context.clone()];
assert!(!predicate.eval(&contexts));
- let contexts = vec![
- parent_context.clone(),
- other_context.clone(),
- child_context.clone(),
- ];
+ let contexts = vec![parent_context.clone(), other_context, child_context.clone()];
assert!(predicate.eval(&contexts));
assert!(!predicate.eval(&[]));
@@ -681,7 +677,7 @@ mod tests {
let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap();
assert!(!zany_predicate.eval(slice::from_ref(&child_context)));
- assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()]));
+ assert!(zany_predicate.eval(&[child_context.clone(), child_context]));
}
#[test]
@@ -718,7 +714,7 @@ mod tests {
let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap();
assert!(!not_descendant.eval(slice::from_ref(&parent_context)));
assert!(!not_descendant.eval(slice::from_ref(&child_context)));
- assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()]));
+ assert!(!not_descendant.eval(&[parent_context, child_context]));
let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap();
assert!(double_not.eval(slice::from_ref(&editor_context)));
@@ -278,7 +278,7 @@ impl PathBuilder {
options: &StrokeOptions,
) -> Result<Path<Pixels>, Error> {
let path = if let Some(dash_array) = dash_array {
- let measurements = lyon::algorithms::measure::PathMeasurements::from_path(&path, 0.01);
+ let measurements = lyon::algorithms::measure::PathMeasurements::from_path(path, 0.01);
let mut sampler = measurements
.create_sampler(path, lyon::algorithms::measure::SampleType::Normalized);
let mut builder = lyon::path::Path::builder();
@@ -318,7 +318,7 @@ impl PathBuilder {
Ok(Self::build_path(buf))
}
- /// Builds a [`Path`] from a [`lyon::VertexBuffers`].
+ /// Builds a [`Path`] from a [`lyon::tessellation::VertexBuffers`].
pub fn build_path(buf: VertexBuffers<lyon::math::Point, u16>) -> Path<Pixels> {
if buf.vertices.is_empty() {
return Path::new(Point::default());
@@ -39,8 +39,8 @@ use crate::{
Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput,
- Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
- ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, Task, TaskLabel, Window,
+ Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph,
+ ShapedRun, SharedString, Size, SvgRenderer, SvgSize, SystemWindowTab, Task, TaskLabel, Window,
WindowControlArea, hash, point, px, size,
};
use anyhow::Result;
@@ -220,14 +220,17 @@ pub(crate) trait Platform: 'static {
&self,
options: PathPromptOptions,
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
- fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>;
+ fn prompt_for_new_path(
+ &self,
+ directory: &Path,
+ suggested_name: Option<&str>,
+ ) -> oneshot::Receiver<Result<Option<PathBuf>>>;
fn can_select_mixed_files_and_dirs(&self) -> bool;
fn reveal_path(&self, path: &Path);
fn open_with_system(&self, path: &Path);
fn on_quit(&self, callback: Box<dyn FnMut()>);
fn on_reopen(&self, callback: Box<dyn FnMut()>);
- fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
@@ -247,7 +250,6 @@ pub(crate) trait Platform: 'static {
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
- fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
fn compositor_name(&self) -> &'static str {
""
@@ -268,6 +270,10 @@ pub(crate) trait Platform: 'static {
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
+
+ fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
+ fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
+ fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
}
/// A handle to a platform's display, e.g. a monitor or laptop screen.
@@ -496,9 +502,27 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
// macOS specific methods
+ fn get_title(&self) -> String {
+ String::new()
+ }
+ fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
+ None
+ }
+ fn tab_bar_visible(&self) -> bool {
+ false
+ }
fn set_edited(&mut self, _edited: bool) {}
fn show_character_palette(&self) {}
fn titlebar_double_click(&self) {}
+ fn on_move_tab_to_new_window(&self, _callback: Box<dyn FnMut()>) {}
+ fn on_merge_all_windows(&self, _callback: Box<dyn FnMut()>) {}
+ fn on_select_previous_tab(&self, _callback: Box<dyn FnMut()>) {}
+ fn on_select_next_tab(&self, _callback: Box<dyn FnMut()>) {}
+ fn on_toggle_tab_bar(&self, _callback: Box<dyn FnMut()>) {}
+ fn merge_all_windows(&self) {}
+ fn move_tab_to_new_window(&self) {}
+ fn toggle_window_tab_overview(&self) {}
+ fn set_tabbing_identifier(&self, _identifier: Option<String>) {}
#[cfg(target_os = "windows")]
fn get_raw_handle(&self) -> windows::HWND;
@@ -524,7 +548,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn set_client_inset(&self, _inset: Pixels) {}
fn gpu_specs(&self) -> Option<GpuSpecs>;
- fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>);
+ fn update_ime_position(&self, _bounds: Bounds<Pixels>);
#[cfg(any(test, feature = "test-support"))]
fn as_test(&mut self) -> Option<&mut TestWindow> {
@@ -588,7 +612,7 @@ impl PlatformTextSystem for NoopTextSystem {
}
fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
- return Ok(FontId(1));
+ Ok(FontId(1))
}
fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
@@ -669,7 +693,7 @@ impl PlatformTextSystem for NoopTextSystem {
}
}
let mut runs = Vec::default();
- if glyphs.len() > 0 {
+ if !glyphs.is_empty() {
runs.push(ShapedRun {
font_id: FontId(0),
glyphs,
@@ -1085,6 +1109,12 @@ pub struct WindowOptions {
/// Whether the window should be movable by the user
pub is_movable: bool,
+ /// Whether the window should be resizable by the user
+ pub is_resizable: bool,
+
+ /// Whether the window should be minimized by the user
+ pub is_minimizable: bool,
+
/// The display to create the window on, if this is None,
/// the window will be created on the main display
pub display_id: Option<DisplayId>,
@@ -1101,6 +1131,9 @@ pub struct WindowOptions {
/// Whether to use client or server side decorations. Wayland only
/// Note that this may be ignored.
pub window_decorations: Option<WindowDecorations>,
+
+ /// Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together.
+ pub tabbing_identifier: Option<String>,
}
/// The variables that can be configured when creating a new window
@@ -1127,6 +1160,14 @@ pub(crate) struct WindowParams {
#[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
pub is_movable: bool,
+ /// Whether the window should be resizable by the user
+ #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
+ pub is_resizable: bool,
+
+ /// Whether the window should be minimized by the user
+ #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))]
+ pub is_minimizable: bool,
+
#[cfg_attr(
any(target_os = "linux", target_os = "freebsd", target_os = "windows"),
allow(dead_code)
@@ -1140,6 +1181,8 @@ pub(crate) struct WindowParams {
pub display_id: Option<DisplayId>,
pub window_min_size: Option<Size<Pixels>>,
+ #[cfg(target_os = "macos")]
+ pub tabbing_identifier: Option<String>,
}
/// Represents the status of how a window should be opened.
@@ -1185,11 +1228,14 @@ impl Default for WindowOptions {
show: true,
kind: WindowKind::Normal,
is_movable: true,
+ is_resizable: true,
+ is_minimizable: true,
display_id: None,
window_background: WindowBackgroundAppearance::default(),
app_id: None,
window_min_size: None,
window_decorations: None,
+ tabbing_identifier: None,
}
}
}
@@ -1274,7 +1320,7 @@ pub enum WindowBackgroundAppearance {
}
/// The options that can be configured for a file dialog prompt
-#[derive(Copy, Clone, Debug)]
+#[derive(Clone, Debug)]
pub struct PathPromptOptions {
/// Should the prompt allow files to be selected?
pub files: bool,
@@ -1282,6 +1328,8 @@ pub struct PathPromptOptions {
pub directories: bool,
/// Should the prompt allow multiple files to be selected?
pub multiple: bool,
+ /// The prompt to show to a user when selecting a path
+ pub prompt: Option<SharedString>,
}
/// What kind of prompt styling to show
@@ -1502,7 +1550,7 @@ impl ClipboardItem {
for entry in self.entries.iter() {
if let ClipboardEntry::String(ClipboardString { text, metadata: _ }) = entry {
- answer.push_str(&text);
+ answer.push_str(text);
any_entries = true;
}
}
@@ -49,7 +49,7 @@ fn parse_pci_id(id: &str) -> anyhow::Result<u32> {
"Expected a 4 digit PCI ID in hexadecimal format"
);
- return u32::from_str_radix(id, 16).context("parsing PCI ID as hex");
+ u32::from_str_radix(id, 16).context("parsing PCI ID as hex")
}
#[cfg(test)]
@@ -371,7 +371,7 @@ impl BladeRenderer {
.or_else(|| {
[4, 2, 1]
.into_iter()
- .find(|count| context.gpu.supports_texture_sample_count(*count))
+ .find(|&n| (context.gpu.capabilities().sample_count_mask & n) != 0)
})
.unwrap_or(1);
let pipelines = BladePipelines::new(&context.gpu, surface.info(), path_sample_count);
@@ -434,24 +434,24 @@ impl BladeRenderer {
}
fn wait_for_gpu(&mut self) {
- if let Some(last_sp) = self.last_sync_point.take() {
- if !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {
- log::error!("GPU hung");
- #[cfg(target_os = "linux")]
- if self.gpu.device_information().driver_name == "radv" {
- log::error!(
- "there's a known bug with amdgpu/radv, try setting ZED_PATH_SAMPLE_COUNT=0 as a workaround"
- );
- log::error!(
- "if that helps you're running into https://github.com/zed-industries/zed/issues/26143"
- );
- }
+ if let Some(last_sp) = self.last_sync_point.take()
+ && !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS)
+ {
+ log::error!("GPU hung");
+ #[cfg(target_os = "linux")]
+ if self.gpu.device_information().driver_name == "radv" {
log::error!(
- "your device information is: {:?}",
- self.gpu.device_information()
+ "there's a known bug with amdgpu/radv, try setting ZED_PATH_SAMPLE_COUNT=0 as a workaround"
+ );
+ log::error!(
+ "if that helps you're running into https://github.com/zed-industries/zed/issues/26143"
);
- while !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {}
}
+ log::error!(
+ "your device information is: {:?}",
+ self.gpu.device_information()
+ );
+ while !self.gpu.wait_for(&last_sp, MAX_FRAME_TIME_MS) {}
}
}
@@ -1,3 +1,7 @@
+use collections::HashMap;
+
+use crate::{KeybindingKeystroke, Keystroke};
+
/// A trait for platform-specific keyboard layouts
pub trait PlatformKeyboardLayout {
/// Get the keyboard layout ID, which should be unique to the layout
@@ -5,3 +9,33 @@ pub trait PlatformKeyboardLayout {
/// Get the keyboard layout display name
fn name(&self) -> &str;
}
+
+/// A trait for platform-specific keyboard mappings
+pub trait PlatformKeyboardMapper {
+ /// Map a key equivalent to its platform-specific representation
+ fn map_key_equivalent(
+ &self,
+ keystroke: Keystroke,
+ use_key_equivalents: bool,
+ ) -> KeybindingKeystroke;
+ /// Get the key equivalents for the current keyboard layout,
+ /// only used on macOS
+ fn get_key_equivalents(&self) -> Option<&HashMap<char, char>>;
+}
+
+/// A dummy implementation of the platform keyboard mapper
+pub struct DummyKeyboardMapper;
+
+impl PlatformKeyboardMapper for DummyKeyboardMapper {
+ fn map_key_equivalent(
+ &self,
+ keystroke: Keystroke,
+ _use_key_equivalents: bool,
+ ) -> KeybindingKeystroke {
+ KeybindingKeystroke::from_keystroke(keystroke)
+ }
+
+ fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
+ None
+ }
+}
@@ -5,6 +5,14 @@ use std::{
fmt::{Display, Write},
};
+use crate::PlatformKeyboardMapper;
+
+/// This is a helper trait so that we can simplify the implementation of some functions
+pub trait AsKeystroke {
+ /// Returns the GPUI representation of the keystroke.
+ fn as_keystroke(&self) -> &Keystroke;
+}
+
/// A keystroke and associated metadata generated by the platform
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
pub struct Keystroke {
@@ -24,6 +32,19 @@ pub struct Keystroke {
pub key_char: Option<String>,
}
+/// Represents a keystroke that can be used in keybindings and displayed to the user.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct KeybindingKeystroke {
+ /// The GPUI representation of the keystroke.
+ inner: Keystroke,
+ /// The modifiers to display.
+ #[cfg(target_os = "windows")]
+ display_modifiers: Modifiers,
+ /// The key to display.
+ #[cfg(target_os = "windows")]
+ display_key: String,
+}
+
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
/// markdown to display it.
#[derive(Debug)]
@@ -58,7 +79,7 @@ impl Keystroke {
///
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
/// both possibilities for self against the target.
- pub fn should_match(&self, target: &Keystroke) -> bool {
+ pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
#[cfg(not(target_os = "windows"))]
if let Some(key_char) = self
.key_char
@@ -71,7 +92,7 @@ impl Keystroke {
..Default::default()
};
- if &target.key == key_char && target.modifiers == ime_modifiers {
+ if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
return true;
}
}
@@ -83,12 +104,12 @@ impl Keystroke {
.filter(|key_char| key_char != &&self.key)
{
// On Windows, if key_char is set, then the typed keystroke produced the key_char
- if &target.key == key_char && target.modifiers == Modifiers::none() {
+ if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
return true;
}
}
- target.modifiers == self.modifiers && target.key == self.key
+ target.inner.modifiers == self.modifiers && target.inner.key == self.key
}
/// key syntax is:
@@ -200,31 +221,7 @@ impl Keystroke {
/// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String {
- let mut str = String::new();
- if self.modifiers.function {
- str.push_str("fn-");
- }
- if self.modifiers.control {
- str.push_str("ctrl-");
- }
- if self.modifiers.alt {
- str.push_str("alt-");
- }
- if self.modifiers.platform {
- #[cfg(target_os = "macos")]
- str.push_str("cmd-");
-
- #[cfg(any(target_os = "linux", target_os = "freebsd"))]
- str.push_str("super-");
-
- #[cfg(target_os = "windows")]
- str.push_str("win-");
- }
- if self.modifiers.shift {
- str.push_str("shift-");
- }
- str.push_str(&self.key);
- str
+ unparse(&self.modifiers, &self.key)
}
/// Returns true if this keystroke left
@@ -266,6 +263,117 @@ impl Keystroke {
}
}
+impl KeybindingKeystroke {
+ #[cfg(target_os = "windows")]
+ pub(crate) fn new(inner: Keystroke, display_modifiers: Modifiers, display_key: String) -> Self {
+ KeybindingKeystroke {
+ inner,
+ display_modifiers,
+ display_key,
+ }
+ }
+
+ /// Create a new keybinding keystroke from the given keystroke using the given keyboard mapper.
+ pub fn new_with_mapper(
+ inner: Keystroke,
+ use_key_equivalents: bool,
+ keyboard_mapper: &dyn PlatformKeyboardMapper,
+ ) -> Self {
+ keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
+ }
+
+ /// Create a new keybinding keystroke from the given keystroke, without any platform-specific mapping.
+ pub fn from_keystroke(keystroke: Keystroke) -> Self {
+ #[cfg(target_os = "windows")]
+ {
+ let key = keystroke.key.clone();
+ let modifiers = keystroke.modifiers;
+ KeybindingKeystroke {
+ inner: keystroke,
+ display_modifiers: modifiers,
+ display_key: key,
+ }
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ KeybindingKeystroke { inner: keystroke }
+ }
+ }
+
+ /// Returns the GPUI representation of the keystroke.
+ pub fn inner(&self) -> &Keystroke {
+ &self.inner
+ }
+
+ /// Returns the modifiers.
+ ///
+ /// Platform-specific behavior:
+ /// - On macOS and Linux, this modifiers is the same as `inner.modifiers`, which is the GPUI representation of the keystroke.
+ /// - On Windows, this modifiers is the display modifiers, for example, a `ctrl-@` keystroke will have `inner.modifiers` as
+ /// `Modifiers::control()` and `display_modifiers` as `Modifiers::control_shift()`.
+ pub fn modifiers(&self) -> &Modifiers {
+ #[cfg(target_os = "windows")]
+ {
+ &self.display_modifiers
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ &self.inner.modifiers
+ }
+ }
+
+ /// Returns the key.
+ ///
+ /// Platform-specific behavior:
+ /// - On macOS and Linux, this key is the same as `inner.key`, which is the GPUI representation of the keystroke.
+ /// - On Windows, this key is the display key, for example, a `ctrl-@` keystroke will have `inner.key` as `@` and `display_key` as `2`.
+ pub fn key(&self) -> &str {
+ #[cfg(target_os = "windows")]
+ {
+ &self.display_key
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ &self.inner.key
+ }
+ }
+
+ /// Sets the modifiers. On Windows this modifies both `inner.modifiers` and `display_modifiers`.
+ pub fn set_modifiers(&mut self, modifiers: Modifiers) {
+ self.inner.modifiers = modifiers;
+ #[cfg(target_os = "windows")]
+ {
+ self.display_modifiers = modifiers;
+ }
+ }
+
+ /// Sets the key. On Windows this modifies both `inner.key` and `display_key`.
+ pub fn set_key(&mut self, key: String) {
+ #[cfg(target_os = "windows")]
+ {
+ self.display_key = key.clone();
+ }
+ self.inner.key = key;
+ }
+
+ /// Produces a representation of this key that Parse can understand.
+ pub fn unparse(&self) -> String {
+ #[cfg(target_os = "windows")]
+ {
+ unparse(&self.display_modifiers, &self.display_key)
+ }
+ #[cfg(not(target_os = "windows"))]
+ {
+ unparse(&self.inner.modifiers, &self.inner.key)
+ }
+ }
+
+ /// Removes the key_char
+ pub fn remove_key_char(&mut self) {
+ self.inner.key_char = None;
+ }
+}
+
fn is_printable_key(key: &str) -> bool {
!matches!(
key,
@@ -322,65 +430,15 @@ fn is_printable_key(key: &str) -> bool {
impl std::fmt::Display for Keystroke {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- if self.modifiers.control {
- #[cfg(target_os = "macos")]
- f.write_char('^')?;
-
- #[cfg(not(target_os = "macos"))]
- write!(f, "ctrl-")?;
- }
- if self.modifiers.alt {
- #[cfg(target_os = "macos")]
- f.write_char('⌥')?;
-
- #[cfg(not(target_os = "macos"))]
- write!(f, "alt-")?;
- }
- if self.modifiers.platform {
- #[cfg(target_os = "macos")]
- f.write_char('⌘')?;
-
- #[cfg(any(target_os = "linux", target_os = "freebsd"))]
- f.write_char('❖')?;
-
- #[cfg(target_os = "windows")]
- f.write_char('⊞')?;
- }
- if self.modifiers.shift {
- #[cfg(target_os = "macos")]
- f.write_char('⇧')?;
+ display_modifiers(&self.modifiers, f)?;
+ display_key(&self.key, f)
+ }
+}
- #[cfg(not(target_os = "macos"))]
- write!(f, "shift-")?;
- }
- let key = match self.key.as_str() {
- #[cfg(target_os = "macos")]
- "backspace" => '⌫',
- #[cfg(target_os = "macos")]
- "up" => '↑',
- #[cfg(target_os = "macos")]
- "down" => '↓',
- #[cfg(target_os = "macos")]
- "left" => '←',
- #[cfg(target_os = "macos")]
- "right" => '→',
- #[cfg(target_os = "macos")]
- "tab" => '⇥',
- #[cfg(target_os = "macos")]
- "escape" => '⎋',
- #[cfg(target_os = "macos")]
- "shift" => '⇧',
- #[cfg(target_os = "macos")]
- "control" => '⌃',
- #[cfg(target_os = "macos")]
- "alt" => '⌥',
- #[cfg(target_os = "macos")]
- "platform" => '⌘',
-
- key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
- key => return f.write_str(key),
- };
- f.write_char(key)
+impl std::fmt::Display for KeybindingKeystroke {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ display_modifiers(self.modifiers(), f)?;
+ display_key(self.key(), f)
}
}
@@ -600,3 +658,110 @@ pub struct Capslock {
#[serde(default)]
pub on: bool,
}
+
+impl AsKeystroke for Keystroke {
+ fn as_keystroke(&self) -> &Keystroke {
+ self
+ }
+}
+
+impl AsKeystroke for KeybindingKeystroke {
+ fn as_keystroke(&self) -> &Keystroke {
+ &self.inner
+ }
+}
+
+fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ if modifiers.control {
+ #[cfg(target_os = "macos")]
+ f.write_char('^')?;
+
+ #[cfg(not(target_os = "macos"))]
+ write!(f, "ctrl-")?;
+ }
+ if modifiers.alt {
+ #[cfg(target_os = "macos")]
+ f.write_char('⌥')?;
+
+ #[cfg(not(target_os = "macos"))]
+ write!(f, "alt-")?;
+ }
+ if modifiers.platform {
+ #[cfg(target_os = "macos")]
+ f.write_char('⌘')?;
+
+ #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+ f.write_char('❖')?;
+
+ #[cfg(target_os = "windows")]
+ f.write_char('⊞')?;
+ }
+ if modifiers.shift {
+ #[cfg(target_os = "macos")]
+ f.write_char('⇧')?;
+
+ #[cfg(not(target_os = "macos"))]
+ write!(f, "shift-")?;
+ }
+ Ok(())
+}
+
+fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let key = match key {
+ #[cfg(target_os = "macos")]
+ "backspace" => '⌫',
+ #[cfg(target_os = "macos")]
+ "up" => '↑',
+ #[cfg(target_os = "macos")]
+ "down" => '↓',
+ #[cfg(target_os = "macos")]
+ "left" => '←',
+ #[cfg(target_os = "macos")]
+ "right" => '→',
+ #[cfg(target_os = "macos")]
+ "tab" => '⇥',
+ #[cfg(target_os = "macos")]
+ "escape" => '⎋',
+ #[cfg(target_os = "macos")]
+ "shift" => '⇧',
+ #[cfg(target_os = "macos")]
+ "control" => '⌃',
+ #[cfg(target_os = "macos")]
+ "alt" => '⌥',
+ #[cfg(target_os = "macos")]
+ "platform" => '⌘',
+
+ key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
+ key => return f.write_str(key),
+ };
+ f.write_char(key)
+}
+
+#[inline]
+fn unparse(modifiers: &Modifiers, key: &str) -> String {
+ let mut result = String::new();
+ if modifiers.function {
+ result.push_str("fn-");
+ }
+ if modifiers.control {
+ result.push_str("ctrl-");
+ }
+ if modifiers.alt {
+ result.push_str("alt-");
+ }
+ if modifiers.platform {
+ #[cfg(target_os = "macos")]
+ result.push_str("cmd-");
+
+ #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+ result.push_str("super-");
+
+ #[cfg(target_os = "windows")]
+ result.push_str("win-");
+ }
+ if modifiers.shift {
+ result.push_str("shift-");
+ }
+ result.push_str(&key);
+ result
+}
@@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
- Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
- Point, Result, Task, WindowAppearance, WindowParams, px,
+ Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
+ PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
};
#[cfg(any(feature = "wayland", feature = "x11"))]
@@ -108,13 +108,13 @@ impl LinuxCommon {
let callbacks = PlatformHandlers::default();
- let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone()));
+ let dispatcher = Arc::new(LinuxDispatcher::new(main_sender));
let background_executor = BackgroundExecutor::new(dispatcher.clone());
let common = LinuxCommon {
background_executor,
- foreground_executor: ForegroundExecutor::new(dispatcher.clone()),
+ foreground_executor: ForegroundExecutor::new(dispatcher),
text_system,
appearance: WindowAppearance::Light,
auto_hide_scrollbars: false,
@@ -144,6 +144,10 @@ impl<P: LinuxClient + 'static> Platform for P {
self.keyboard_layout()
}
+ fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
+ Rc::new(crate::DummyKeyboardMapper)
+ }
+
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
}
@@ -294,6 +298,7 @@ impl<P: LinuxClient + 'static> Platform for P {
let request = match ashpd::desktop::file_chooser::OpenFileRequest::default()
.modal(true)
.title(title)
+ .accept_label(options.prompt.as_ref().map(crate::SharedString::as_str))
.multiple(options.multiple)
.directory(options.directories)
.send()
@@ -327,26 +332,35 @@ impl<P: LinuxClient + 'static> Platform for P {
done_rx
}
- fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
+ fn prompt_for_new_path(
+ &self,
+ directory: &Path,
+ suggested_name: Option<&str>,
+ ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
let (done_tx, done_rx) = oneshot::channel();
#[cfg(not(any(feature = "wayland", feature = "x11")))]
- let _ = (done_tx.send(Ok(None)), directory);
+ let _ = (done_tx.send(Ok(None)), directory, suggested_name);
#[cfg(any(feature = "wayland", feature = "x11"))]
self.foreground_executor()
.spawn({
let directory = directory.to_owned();
+ let suggested_name = suggested_name.map(|s| s.to_owned());
async move {
- let request = match ashpd::desktop::file_chooser::SaveFileRequest::default()
- .modal(true)
- .title("Save File")
- .current_folder(directory)
- .expect("pathbuf should not be nul terminated")
- .send()
- .await
- {
+ let mut request_builder =
+ ashpd::desktop::file_chooser::SaveFileRequest::default()
+ .modal(true)
+ .title("Save File")
+ .current_folder(directory)
+ .expect("pathbuf should not be nul terminated");
+
+ if let Some(suggested_name) = suggested_name {
+ request_builder = request_builder.current_name(suggested_name.as_str());
+ }
+
+ let request = match request_builder.send().await {
Ok(request) => request,
Err(err) => {
let result = match err {
@@ -431,7 +445,7 @@ impl<P: LinuxClient + 'static> Platform for P {
fn app_path(&self) -> Result<PathBuf> {
// get the path of the executable of the current process
let app_path = env::current_exe()?;
- return Ok(app_path);
+ Ok(app_path)
}
fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
@@ -632,7 +646,7 @@ pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::S
let mut state: Option<xkb::compose::State> = None;
for locale in locales {
if let Ok(table) =
- xkb::compose::Table::new_from_locale(&cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
+ xkb::compose::Table::new_from_locale(cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
{
state = Some(xkb::compose::State::new(
&table,
@@ -657,7 +671,7 @@ pub(super) const DEFAULT_CURSOR_ICON_NAME: &str = "left_ptr";
impl CursorStyle {
#[cfg(any(feature = "wayland", feature = "x11"))]
- pub(super) fn to_icon_names(&self) -> &'static [&'static str] {
+ pub(super) fn to_icon_names(self) -> &'static [&'static str] {
// Based on cursor names from chromium:
// https://github.com/chromium/chromium/blob/d3069cf9c973dc3627fa75f64085c6a86c8f41bf/ui/base/cursor/cursor_factory.cc#L113
match self {
@@ -834,6 +848,7 @@ impl crate::Keystroke {
Keysym::Down => "down".to_owned(),
Keysym::Home => "home".to_owned(),
Keysym::End => "end".to_owned(),
+ Keysym::Insert => "insert".to_owned(),
_ => {
let name = xkb::keysym_get_name(key_sym).to_lowercase();
@@ -980,21 +995,18 @@ mod tests {
#[test]
fn test_is_within_click_distance() {
let zero = Point::new(px(0.0), px(0.0));
- assert_eq!(
- is_within_click_distance(zero, Point::new(px(5.0), px(5.0))),
- true
- );
- assert_eq!(
- is_within_click_distance(zero, Point::new(px(-4.9), px(5.0))),
- true
- );
- assert_eq!(
- is_within_click_distance(Point::new(px(3.0), px(2.0)), Point::new(px(-2.0), px(-2.0))),
- true
- );
- assert_eq!(
- is_within_click_distance(zero, Point::new(px(5.0), px(5.1))),
- false
- );
+ assert!(is_within_click_distance(zero, Point::new(px(5.0), px(5.0))));
+ assert!(is_within_click_distance(
+ zero,
+ Point::new(px(-4.9), px(5.0))
+ ));
+ assert!(is_within_click_distance(
+ Point::new(px(3.0), px(2.0)),
+ Point::new(px(-2.0), px(-2.0))
+ ));
+ assert!(!is_within_click_distance(
+ zero,
+ Point::new(px(5.0), px(5.1))
+ ),);
}
}
@@ -12,7 +12,7 @@ use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::
use crate::CursorStyle;
impl CursorStyle {
- pub(super) fn to_shape(&self) -> Shape {
+ pub(super) fn to_shape(self) -> Shape {
match self {
CursorStyle::Arrow => Shape::Default,
CursorStyle::IBeam => Shape::Text,
@@ -75,8 +75,8 @@ use crate::{
FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon,
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay,
- PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScrollDelta,
- ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
+ PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScrollDelta, ScrollWheelEvent,
+ Size, TouchPhase, WindowParams, point, px, size,
};
use crate::{
SharedString,
@@ -323,7 +323,7 @@ impl WaylandClientStatePtr {
}
}
- pub fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
+ pub fn update_ime_position(&self, bounds: Bounds<Pixels>) {
let client = self.get_client();
let mut state = client.borrow_mut();
if state.composing || state.text_input.is_none() || state.pre_edit_text.is_some() {
@@ -359,13 +359,13 @@ impl WaylandClientStatePtr {
}
changed
};
- if changed {
- if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() {
- drop(state);
- callback();
- state = client.borrow_mut();
- state.common.callbacks.keyboard_layout_change = Some(callback);
- }
+
+ if changed && let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take()
+ {
+ drop(state);
+ callback();
+ state = client.borrow_mut();
+ state.common.callbacks.keyboard_layout_change = Some(callback);
}
}
@@ -373,15 +373,15 @@ impl WaylandClientStatePtr {
let mut client = self.get_client();
let mut state = client.borrow_mut();
let closed_window = state.windows.remove(surface_id).unwrap();
- if let Some(window) = state.mouse_focused_window.take() {
- if !window.ptr_eq(&closed_window) {
- state.mouse_focused_window = Some(window);
- }
+ if let Some(window) = state.mouse_focused_window.take()
+ && !window.ptr_eq(&closed_window)
+ {
+ state.mouse_focused_window = Some(window);
}
- if let Some(window) = state.keyboard_focused_window.take() {
- if !window.ptr_eq(&closed_window) {
- state.keyboard_focused_window = Some(window);
- }
+ if let Some(window) = state.keyboard_focused_window.take()
+ && !window.ptr_eq(&closed_window)
+ {
+ state.keyboard_focused_window = Some(window);
}
if state.windows.is_empty() {
state.common.signal.stop();
@@ -528,7 +528,7 @@ impl WaylandClient {
client.common.appearance = appearance;
- for (_, window) in &mut client.windows {
+ for window in client.windows.values_mut() {
window.set_appearance(appearance);
}
}
@@ -710,9 +710,7 @@ impl LinuxClient for WaylandClient {
fn set_cursor_style(&self, style: CursorStyle) {
let mut state = self.0.borrow_mut();
- let need_update = state
- .cursor_style
- .map_or(true, |current_style| current_style != style);
+ let need_update = state.cursor_style != Some(style);
if need_update {
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
@@ -951,11 +949,8 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
};
drop(state);
- match event {
- wl_callback::Event::Done { .. } => {
- window.frame();
- }
- _ => {}
+ if let wl_callback::Event::Done { .. } = event {
+ window.frame();
}
}
}
@@ -1145,7 +1140,7 @@ impl Dispatch<wl_seat::WlSeat, ()> for WaylandClientStatePtr {
.globals
.text_input_manager
.as_ref()
- .map(|text_input_manager| text_input_manager.get_text_input(&seat, qh, ()));
+ .map(|text_input_manager| text_input_manager.get_text_input(seat, qh, ()));
if let Some(wl_keyboard) = &state.wl_keyboard {
wl_keyboard.release();
@@ -1285,7 +1280,6 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
let Some(focused_window) = focused_window else {
return;
};
- let focused_window = focused_window.clone();
let keymap_state = state.keymap_state.as_ref().unwrap();
let keycode = Keycode::from(key + MIN_KEYCODE);
@@ -1294,7 +1288,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
match key_state {
wl_keyboard::KeyState::Pressed if !keysym.is_modifier_key() => {
let mut keystroke =
- Keystroke::from_xkb(&keymap_state, state.modifiers, keycode);
+ Keystroke::from_xkb(keymap_state, state.modifiers, keycode);
if let Some(mut compose) = state.compose_state.take() {
compose.feed(keysym);
match compose.status() {
@@ -1538,12 +1532,9 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
cursor_shape_device.set_shape(serial, style.to_shape());
} else {
let scale = window.primary_output_scale();
- state.cursor.set_icon(
- &wl_pointer,
- serial,
- style.to_icon_names(),
- scale,
- );
+ state
+ .cursor
+ .set_icon(wl_pointer, serial, style.to_icon_names(), scale);
}
}
drop(state);
@@ -1580,7 +1571,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
if state
.keyboard_focused_window
.as_ref()
- .map_or(false, |keyboard_window| window.ptr_eq(&keyboard_window))
+ .is_some_and(|keyboard_window| window.ptr_eq(keyboard_window))
{
state.enter_token = None;
}
@@ -1787,17 +1778,17 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
drop(state);
window.handle_input(input);
}
- } else if let Some(discrete) = discrete {
- if let Some(window) = state.mouse_focused_window.clone() {
- let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
- position: state.mouse_location.unwrap(),
- delta: ScrollDelta::Lines(discrete),
- modifiers: state.modifiers,
- touch_phase: TouchPhase::Moved,
- });
- drop(state);
- window.handle_input(input);
- }
+ } else if let Some(discrete) = discrete
+ && let Some(window) = state.mouse_focused_window.clone()
+ {
+ let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
+ position: state.mouse_location.unwrap(),
+ delta: ScrollDelta::Lines(discrete),
+ modifiers: state.modifiers,
+ touch_phase: TouchPhase::Moved,
+ });
+ drop(state);
+ window.handle_input(input);
}
}
}
@@ -2019,25 +2010,22 @@ impl Dispatch<wl_data_offer::WlDataOffer, ()> for WaylandClientStatePtr {
let client = this.get_client();
let mut state = client.borrow_mut();
- match event {
- wl_data_offer::Event::Offer { mime_type } => {
- // Drag and drop
- if mime_type == FILE_LIST_MIME_TYPE {
- let serial = state.serial_tracker.get(SerialKind::DataDevice);
- let mime_type = mime_type.clone();
- data_offer.accept(serial, Some(mime_type));
- }
+ if let wl_data_offer::Event::Offer { mime_type } = event {
+ // Drag and drop
+ if mime_type == FILE_LIST_MIME_TYPE {
+ let serial = state.serial_tracker.get(SerialKind::DataDevice);
+ let mime_type = mime_type.clone();
+ data_offer.accept(serial, Some(mime_type));
+ }
- // Clipboard
- if let Some(offer) = state
- .data_offers
- .iter_mut()
- .find(|wrapper| wrapper.inner.id() == data_offer.id())
- {
- offer.add_mime_type(mime_type);
- }
+ // Clipboard
+ if let Some(offer) = state
+ .data_offers
+ .iter_mut()
+ .find(|wrapper| wrapper.inner.id() == data_offer.id())
+ {
+ offer.add_mime_type(mime_type);
}
- _ => {}
}
}
}
@@ -2118,13 +2106,10 @@ impl Dispatch<zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1, ()>
let client = this.get_client();
let mut state = client.borrow_mut();
- match event {
- zwp_primary_selection_offer_v1::Event::Offer { mime_type } => {
- if let Some(offer) = state.primary_data_offer.as_mut() {
- offer.add_mime_type(mime_type);
- }
- }
- _ => {}
+ if let zwp_primary_selection_offer_v1::Event::Offer { mime_type } = event
+ && let Some(offer) = state.primary_data_offer.as_mut()
+ {
+ offer.add_mime_type(mime_type);
}
}
}
@@ -45,10 +45,11 @@ impl Cursor {
}
fn set_theme_internal(&mut self, theme_name: Option<String>) {
- if let Some(loaded_theme) = self.loaded_theme.as_ref() {
- if loaded_theme.name == theme_name && loaded_theme.scaled_size == self.scaled_size {
- return;
- }
+ if let Some(loaded_theme) = self.loaded_theme.as_ref()
+ && loaded_theme.name == theme_name
+ && loaded_theme.scaled_size == self.scaled_size
+ {
+ return;
}
let result = if let Some(theme_name) = theme_name.as_ref() {
CursorTheme::load_from_name(
@@ -66,7 +67,7 @@ impl Cursor {
{
self.loaded_theme = Some(LoadedTheme {
theme,
- name: theme_name.map(|name| name.to_string()),
+ name: theme_name,
scaled_size: self.scaled_size,
});
}
@@ -144,7 +145,7 @@ impl Cursor {
hot_y as i32 / scale,
);
- self.surface.attach(Some(&buffer), 0, 0);
+ self.surface.attach(Some(buffer), 0, 0);
self.surface.damage(0, 0, width as i32, height as i32);
self.surface.commit();
}
@@ -25,9 +25,8 @@ use crate::scene::Scene;
use crate::{
AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
- ResizeEdge, ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
- WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations,
- WindowParams, px, size,
+ ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
+ WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, px, size,
};
use crate::{
Capslock,
@@ -355,85 +354,82 @@ impl WaylandWindowStatePtr {
}
pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) {
- match event {
- xdg_surface::Event::Configure { serial } => {
- {
- let mut state = self.state.borrow_mut();
- if let Some(window_controls) = state.in_progress_window_controls.take() {
- state.window_controls = window_controls;
-
- drop(state);
- let mut callbacks = self.callbacks.borrow_mut();
- if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
- appearance_changed();
- }
+ if let xdg_surface::Event::Configure { serial } = event {
+ {
+ let mut state = self.state.borrow_mut();
+ if let Some(window_controls) = state.in_progress_window_controls.take() {
+ state.window_controls = window_controls;
+
+ drop(state);
+ let mut callbacks = self.callbacks.borrow_mut();
+ if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
+ appearance_changed();
}
}
- {
- let mut state = self.state.borrow_mut();
-
- if let Some(mut configure) = state.in_progress_configure.take() {
- let got_unmaximized = state.maximized && !configure.maximized;
- state.fullscreen = configure.fullscreen;
- state.maximized = configure.maximized;
- state.tiling = configure.tiling;
- // Limit interactive resizes to once per vblank
- if configure.resizing && state.resize_throttle {
- return;
- } else if configure.resizing {
- state.resize_throttle = true;
- }
- if !configure.fullscreen && !configure.maximized {
- configure.size = if got_unmaximized {
- Some(state.window_bounds.size)
- } else {
- compute_outer_size(state.inset(), configure.size, state.tiling)
- };
- if let Some(size) = configure.size {
- state.window_bounds = Bounds {
- origin: Point::default(),
- size,
- };
- }
- }
- drop(state);
+ }
+ {
+ let mut state = self.state.borrow_mut();
+
+ if let Some(mut configure) = state.in_progress_configure.take() {
+ let got_unmaximized = state.maximized && !configure.maximized;
+ state.fullscreen = configure.fullscreen;
+ state.maximized = configure.maximized;
+ state.tiling = configure.tiling;
+ // Limit interactive resizes to once per vblank
+ if configure.resizing && state.resize_throttle {
+ return;
+ } else if configure.resizing {
+ state.resize_throttle = true;
+ }
+ if !configure.fullscreen && !configure.maximized {
+ configure.size = if got_unmaximized {
+ Some(state.window_bounds.size)
+ } else {
+ compute_outer_size(state.inset(), configure.size, state.tiling)
+ };
if let Some(size) = configure.size {
- self.resize(size);
+ state.window_bounds = Bounds {
+ origin: Point::default(),
+ size,
+ };
}
}
- }
- let mut state = self.state.borrow_mut();
- state.xdg_surface.ack_configure(serial);
-
- let window_geometry = inset_by_tiling(
- state.bounds.map_origin(|_| px(0.0)),
- state.inset(),
- state.tiling,
- )
- .map(|v| v.0 as i32)
- .map_size(|v| if v <= 0 { 1 } else { v });
-
- state.xdg_surface.set_window_geometry(
- window_geometry.origin.x,
- window_geometry.origin.y,
- window_geometry.size.width,
- window_geometry.size.height,
- );
-
- let request_frame_callback = !state.acknowledged_first_configure;
- if request_frame_callback {
- state.acknowledged_first_configure = true;
drop(state);
- self.frame();
+ if let Some(size) = configure.size {
+ self.resize(size);
+ }
}
}
- _ => {}
+ let mut state = self.state.borrow_mut();
+ state.xdg_surface.ack_configure(serial);
+
+ let window_geometry = inset_by_tiling(
+ state.bounds.map_origin(|_| px(0.0)),
+ state.inset(),
+ state.tiling,
+ )
+ .map(|v| v.0 as i32)
+ .map_size(|v| if v <= 0 { 1 } else { v });
+
+ state.xdg_surface.set_window_geometry(
+ window_geometry.origin.x,
+ window_geometry.origin.y,
+ window_geometry.size.width,
+ window_geometry.size.height,
+ );
+
+ let request_frame_callback = !state.acknowledged_first_configure;
+ if request_frame_callback {
+ state.acknowledged_first_configure = true;
+ drop(state);
+ self.frame();
+ }
}
}
pub fn handle_toplevel_decoration_event(&self, event: zxdg_toplevel_decoration_v1::Event) {
- match event {
- zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode {
+ if let zxdg_toplevel_decoration_v1::Event::Configure { mode } = event {
+ match mode {
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => {
self.state.borrow_mut().decorations = WindowDecorations::Server;
if let Some(mut appearance_changed) =
@@ -457,17 +453,13 @@ impl WaylandWindowStatePtr {
WEnum::Unknown(v) => {
log::warn!("Unknown decoration mode: {}", v);
}
- },
- _ => {}
+ }
}
}
pub fn handle_fractional_scale_event(&self, event: wp_fractional_scale_v1::Event) {
- match event {
- wp_fractional_scale_v1::Event::PreferredScale { scale } => {
- self.rescale(scale as f32 / 120.0);
- }
- _ => {}
+ if let wp_fractional_scale_v1::Event::PreferredScale { scale } = event {
+ self.rescale(scale as f32 / 120.0);
}
}
@@ -669,8 +661,8 @@ impl WaylandWindowStatePtr {
pub fn set_size_and_scale(&self, size: Option<Size<Pixels>>, scale: Option<f32>) {
let (size, scale) = {
let mut state = self.state.borrow_mut();
- if size.map_or(true, |size| size == state.bounds.size)
- && scale.map_or(true, |scale| scale == state.scale)
+ if size.is_none_or(|size| size == state.bounds.size)
+ && scale.is_none_or(|scale| scale == state.scale)
{
return;
}
@@ -713,21 +705,20 @@ impl WaylandWindowStatePtr {
}
pub fn handle_input(&self, input: PlatformInput) {
- if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
- if !fun(input.clone()).propagate {
- return;
- }
+ if let Some(ref mut fun) = self.callbacks.borrow_mut().input
+ && !fun(input.clone()).propagate
+ {
+ return;
}
- if let PlatformInput::KeyDown(event) = input {
- if event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) {
- if let Some(key_char) = &event.keystroke.key_char {
- let mut state = self.state.borrow_mut();
- if let Some(mut input_handler) = state.input_handler.take() {
- drop(state);
- input_handler.replace_text_in_range(None, key_char);
- self.state.borrow_mut().input_handler = Some(input_handler);
- }
- }
+ if let PlatformInput::KeyDown(event) = input
+ && event.keystroke.modifiers.is_subset_of(&Modifiers::shift())
+ && let Some(key_char) = &event.keystroke.key_char
+ {
+ let mut state = self.state.borrow_mut();
+ if let Some(mut input_handler) = state.input_handler.take() {
+ drop(state);
+ input_handler.replace_text_in_range(None, key_char);
+ self.state.borrow_mut().input_handler = Some(input_handler);
}
}
}
@@ -1086,7 +1077,7 @@ impl PlatformWindow for WaylandWindow {
}
}
- fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
+ fn update_ime_position(&self, bounds: Bounds<Pixels>) {
let state = self.borrow();
state.client.update_ime_position(bounds);
}
@@ -1147,7 +1138,7 @@ fn update_window(mut state: RefMut<WaylandWindowState>) {
}
impl WindowDecorations {
- fn to_xdg(&self) -> zxdg_toplevel_decoration_v1::Mode {
+ fn to_xdg(self) -> zxdg_toplevel_decoration_v1::Mode {
match self {
WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide,
WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide,
@@ -1156,7 +1147,7 @@ impl WindowDecorations {
}
impl ResizeEdge {
- fn to_xdg(&self) -> xdg_toplevel::ResizeEdge {
+ fn to_xdg(self) -> xdg_toplevel::ResizeEdge {
match self {
ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top,
ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight,
@@ -62,8 +62,7 @@ use crate::{
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, Platform,
PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, RequestFrameOptions,
- ScaledPixels, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
- modifiers_from_xinput_info, point, px,
+ ScrollDelta, Size, TouchPhase, WindowParams, X11Window, modifiers_from_xinput_info, point, px,
};
/// Value for DeviceId parameters which selects all devices.
@@ -232,15 +231,12 @@ impl X11ClientStatePtr {
};
let mut state = client.0.borrow_mut();
- if let Some(window_ref) = state.windows.remove(&x_window) {
- match window_ref.refresh_state {
- Some(RefreshState::PeriodicRefresh {
- event_loop_token, ..
- }) => {
- state.loop_handle.remove(event_loop_token);
- }
- _ => {}
- }
+ if let Some(window_ref) = state.windows.remove(&x_window)
+ && let Some(RefreshState::PeriodicRefresh {
+ event_loop_token, ..
+ }) = window_ref.refresh_state
+ {
+ state.loop_handle.remove(event_loop_token);
}
if state.mouse_focused_window == Some(x_window) {
state.mouse_focused_window = None;
@@ -255,7 +251,7 @@ impl X11ClientStatePtr {
}
}
- pub fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
+ pub fn update_ime_position(&self, bounds: Bounds<Pixels>) {
let Some(client) = self.get_client() else {
return;
};
@@ -273,6 +269,7 @@ impl X11ClientStatePtr {
state.ximc = Some(ximc);
return;
};
+ let scaled_bounds = bounds.scale(state.scale_factor);
let ic_attributes = ximc
.build_ic_attributes()
.push(
@@ -285,8 +282,8 @@ impl X11ClientStatePtr {
b.push(
xim::AttributeName::SpotLocation,
xim::Point {
- x: u32::from(bounds.origin.x + bounds.size.width) as i16,
- y: u32::from(bounds.origin.y + bounds.size.height) as i16,
+ x: u32::from(scaled_bounds.origin.x + scaled_bounds.size.width) as i16,
+ y: u32::from(scaled_bounds.origin.y + scaled_bounds.size.height) as i16,
},
);
})
@@ -459,7 +456,7 @@ impl X11Client {
move |event, _, client| match event {
XDPEvent::WindowAppearance(appearance) => {
client.with_common(|common| common.appearance = appearance);
- for (_, window) in &mut client.0.borrow_mut().windows {
+ for window in client.0.borrow_mut().windows.values_mut() {
window.window.set_appearance(appearance);
}
}
@@ -565,10 +562,10 @@ impl X11Client {
events.push(last_keymap_change_event);
}
- if let Some(last_press) = last_key_press.as_ref() {
- if last_press.detail == key_press.detail {
- continue;
- }
+ if let Some(last_press) = last_key_press.as_ref()
+ && last_press.detail == key_press.detail
+ {
+ continue;
}
if let Some(Event::KeyRelease(key_release)) =
@@ -642,13 +639,7 @@ impl X11Client {
let xim_connected = xim_handler.connected;
drop(state);
- let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
- Ok(handled) => handled,
- Err(err) => {
- log::error!("XIMClientError: {}", err);
- false
- }
- };
+ let xim_filtered = ximc.filter_event(&event, &mut xim_handler);
let xim_callback_event = xim_handler.last_callback_event.take();
let mut state = self.0.borrow_mut();
@@ -659,14 +650,28 @@ impl X11Client {
self.handle_xim_callback_event(event);
}
- if xim_filtered {
- continue;
- }
-
- if xim_connected {
- self.xim_handle_event(event);
- } else {
- self.handle_event(event);
+ match xim_filtered {
+ Ok(handled) => {
+ if handled {
+ continue;
+ }
+ if xim_connected {
+ self.xim_handle_event(event);
+ } else {
+ self.handle_event(event);
+ }
+ }
+ Err(err) => {
+ // this might happen when xim server crashes on one of the events
+ // we do lose 1-2 keys when crash happens since there is no reliable way to get that info
+ // luckily, x11 sends us window not found error when xim server crashes upon further key press
+ // hence we fall back to handle_event
+ log::error!("XIMClientError: {}", err);
+ let mut state = self.0.borrow_mut();
+ state.take_xim();
+ drop(state);
+ self.handle_event(event);
+ }
}
}
}
@@ -698,14 +703,14 @@ impl X11Client {
state.xim_handler = Some(xim_handler);
return;
};
- if let Some(area) = window.get_ime_area() {
+ if let Some(scaled_area) = window.get_ime_area() {
ic_attributes =
ic_attributes.nested_list(xim::AttributeName::PreeditAttributes, |b| {
b.push(
xim::AttributeName::SpotLocation,
xim::Point {
- x: u32::from(area.origin.x + area.size.width) as i16,
- y: u32::from(area.origin.y + area.size.height) as i16,
+ x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16,
+ y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16,
},
);
});
@@ -868,22 +873,19 @@ impl X11Client {
let Some(reply) = reply else {
return Some(());
};
- match str::from_utf8(&reply.value) {
- Ok(file_list) => {
- let paths: SmallVec<[_; 2]> = file_list
- .lines()
- .filter_map(|path| Url::parse(path).log_err())
- .filter_map(|url| url.to_file_path().log_err())
- .collect();
- let input = PlatformInput::FileDrop(FileDropEvent::Entered {
- position: state.xdnd_state.position,
- paths: crate::ExternalPaths(paths),
- });
- drop(state);
- window.handle_input(input);
- self.0.borrow_mut().xdnd_state.retrieved = true;
- }
- Err(_) => {}
+ if let Ok(file_list) = str::from_utf8(&reply.value) {
+ let paths: SmallVec<[_; 2]> = file_list
+ .lines()
+ .filter_map(|path| Url::parse(path).log_err())
+ .filter_map(|url| url.to_file_path().log_err())
+ .collect();
+ let input = PlatformInput::FileDrop(FileDropEvent::Entered {
+ position: state.xdnd_state.position,
+ paths: crate::ExternalPaths(paths),
+ });
+ drop(state);
+ window.handle_input(input);
+ self.0.borrow_mut().xdnd_state.retrieved = true;
}
}
Event::ConfigureNotify(event) => {
@@ -1204,7 +1206,7 @@ impl X11Client {
state = self.0.borrow_mut();
if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) {
- let scroll_delta = get_scroll_delta_and_update_state(&mut pointer, &event);
+ let scroll_delta = get_scroll_delta_and_update_state(pointer, &event);
drop(state);
if let Some(scroll_delta) = scroll_delta {
window.handle_input(PlatformInput::ScrollWheel(make_scroll_wheel_event(
@@ -1263,7 +1265,7 @@ impl X11Client {
Event::XinputDeviceChanged(event) => {
let mut state = self.0.borrow_mut();
if let Some(mut pointer) = state.pointer_device_states.get_mut(&event.sourceid) {
- reset_pointer_device_scroll_positions(&mut pointer);
+ reset_pointer_device_scroll_positions(pointer);
}
}
_ => {}
@@ -1327,7 +1329,7 @@ impl X11Client {
state.composing = false;
drop(state);
if let Some(mut keystroke) = keystroke {
- keystroke.key_char = Some(text.clone());
+ keystroke.key_char = Some(text);
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
keystroke,
is_held: false,
@@ -1349,7 +1351,7 @@ impl X11Client {
drop(state);
window.handle_ime_preedit(text);
- if let Some(area) = window.get_ime_area() {
+ if let Some(scaled_area) = window.get_ime_area() {
let ic_attributes = ximc
.build_ic_attributes()
.push(
@@ -1362,8 +1364,8 @@ impl X11Client {
b.push(
xim::AttributeName::SpotLocation,
xim::Point {
- x: u32::from(area.origin.x + area.size.width) as i16,
- y: u32::from(area.origin.y + area.size.height) as i16,
+ x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16,
+ y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16,
},
);
})
@@ -1578,11 +1580,11 @@ impl LinuxClient for X11Client {
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
let state = self.0.borrow_mut();
- return state
+ state
.clipboard
.get_any(clipboard::ClipboardKind::Primary)
.context("X11: Failed to read from clipboard (primary)")
- .log_with_level(log::Level::Debug);
+ .log_with_level(log::Level::Debug)
}
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
@@ -1595,11 +1597,11 @@ impl LinuxClient for X11Client {
{
return state.clipboard_item.clone();
}
- return state
+ state
.clipboard
.get_any(clipboard::ClipboardKind::Clipboard)
.context("X11: Failed to read from clipboard (clipboard)")
- .log_with_level(log::Level::Debug);
+ .log_with_level(log::Level::Debug)
}
fn run(&self) {
@@ -2002,12 +2004,12 @@ fn check_gtk_frame_extents_supported(
}
fn xdnd_is_atom_supported(atom: u32, atoms: &XcbAtoms) -> bool {
- return atom == atoms.TEXT
+ atom == atoms.TEXT
|| atom == atoms.STRING
|| atom == atoms.UTF8_STRING
|| atom == atoms.TEXT_PLAIN
|| atom == atoms.TEXT_PLAIN_UTF8
- || atom == atoms.TextUriList;
+ || atom == atoms.TextUriList
}
fn xdnd_get_supported_atom(
@@ -2027,16 +2029,15 @@ fn xdnd_get_supported_atom(
),
)
.log_with_level(Level::Warn)
+ && let Some(atoms) = reply.value32()
{
- if let Some(atoms) = reply.value32() {
- for atom in atoms {
- if xdnd_is_atom_supported(atom, &supported_atoms) {
- return atom;
- }
+ for atom in atoms {
+ if xdnd_is_atom_supported(atom, supported_atoms) {
+ return atom;
}
}
}
- return 0;
+ 0
}
fn xdnd_send_finished(
@@ -2107,7 +2108,7 @@ fn current_pointer_device_states(
.classes
.iter()
.filter_map(|class| class.data.as_scroll())
- .map(|class| *class)
+ .copied()
.rev()
.collect::<Vec<_>>();
let old_state = scroll_values_to_preserve.get(&info.deviceid);
@@ -2137,7 +2138,7 @@ fn current_pointer_device_states(
if pointer_device_states.is_empty() {
log::error!("Found no xinput mouse pointers.");
}
- return Some(pointer_device_states);
+ Some(pointer_device_states)
}
/// Returns true if the device is a pointer device. Does not include pointer device groups.
@@ -2403,11 +2404,13 @@ fn legacy_get_randr_scale_factor(connection: &XCBConnection, root: u32) -> Optio
let mut crtc_infos: HashMap<randr::Crtc, randr::GetCrtcInfoReply> = HashMap::default();
let mut valid_outputs: HashSet<randr::Output> = HashSet::new();
for (crtc, cookie) in crtc_cookies {
- if let Ok(reply) = cookie.reply() {
- if reply.width > 0 && reply.height > 0 && !reply.outputs.is_empty() {
- crtc_infos.insert(crtc, reply.clone());
- valid_outputs.extend(&reply.outputs);
- }
+ if let Ok(reply) = cookie.reply()
+ && reply.width > 0
+ && reply.height > 0
+ && !reply.outputs.is_empty()
+ {
+ crtc_infos.insert(crtc, reply.clone());
+ valid_outputs.extend(&reply.outputs);
}
}
@@ -1078,11 +1078,11 @@ impl Clipboard {
} else {
String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)?
};
- return Ok(ClipboardItem::new_string(text));
+ Ok(ClipboardItem::new_string(text))
}
pub fn is_owner(&self, selection: ClipboardKind) -> bool {
- return self.inner.is_owner(selection).unwrap_or(false);
+ self.inner.is_owner(selection).unwrap_or(false)
}
}
@@ -1120,25 +1120,25 @@ impl Drop for Clipboard {
log::error!("Failed to flush the clipboard window. Error: {}", e);
return;
}
- if let Some(global_cb) = global_cb {
- if let Err(e) = global_cb.server_handle.join() {
- // Let's try extracting the error message
- let message;
- if let Some(msg) = e.downcast_ref::<&'static str>() {
- message = Some((*msg).to_string());
- } else if let Some(msg) = e.downcast_ref::<String>() {
- message = Some(msg.clone());
- } else {
- message = None;
- }
- if let Some(message) = message {
- log::error!(
- "The clipboard server thread panicked. Panic message: '{}'",
- message,
- );
- } else {
- log::error!("The clipboard server thread panicked.");
- }
+ if let Some(global_cb) = global_cb
+ && let Err(e) = global_cb.server_handle.join()
+ {
+ // Let's try extracting the error message
+ let message;
+ if let Some(msg) = e.downcast_ref::<&'static str>() {
+ message = Some((*msg).to_string());
+ } else if let Some(msg) = e.downcast_ref::<String>() {
+ message = Some(msg.clone());
+ } else {
+ message = None;
+ }
+ if let Some(message) = message {
+ log::error!(
+ "The clipboard server thread panicked. Panic message: '{}'",
+ message,
+ );
+ } else {
+ log::error!("The clipboard server thread panicked.");
}
}
}
@@ -73,8 +73,8 @@ pub(crate) fn get_valuator_axis_index(
// valuator present in this event's axisvalues. Axisvalues is ordered from
// lowest valuator number to highest, so counting bits before the 1 bit for
// this valuator yields the index in axisvalues.
- if bit_is_set_in_vec(&valuator_mask, valuator_number) {
- Some(popcount_upto_bit_index(&valuator_mask, valuator_number) as usize)
+ if bit_is_set_in_vec(valuator_mask, valuator_number) {
+ Some(popcount_upto_bit_index(valuator_mask, valuator_number) as usize)
} else {
None
}
@@ -104,7 +104,7 @@ fn bit_is_set_in_vec(bit_vec: &Vec<u32>, bit_index: u16) -> bool {
let array_index = bit_index as usize / 32;
bit_vec
.get(array_index)
- .map_or(false, |bits| bit_is_set(*bits, bit_index % 32))
+ .is_some_and(|bits| bit_is_set(*bits, bit_index % 32))
}
fn bit_is_set(bits: u32, bit_index: u16) -> bool {
@@ -95,7 +95,7 @@ fn query_render_extent(
}
impl ResizeEdge {
- fn to_moveresize(&self) -> u32 {
+ fn to_moveresize(self) -> u32 {
match self {
ResizeEdge::TopLeft => 0,
ResizeEdge::Top => 1,
@@ -397,7 +397,7 @@ impl X11WindowState {
.display_id
.map_or(x_main_screen_index, |did| did.0 as usize);
- let visual_set = find_visuals(&xcb, x_screen_index);
+ let visual_set = find_visuals(xcb, x_screen_index);
let visual = match visual_set.transparent {
Some(visual) => visual,
@@ -515,19 +515,19 @@ impl X11WindowState {
xcb.configure_window(x_window, &xproto::ConfigureWindowAux::new().x(x).y(y)),
)?;
}
- if let Some(titlebar) = params.titlebar {
- if let Some(title) = titlebar.title {
- check_reply(
- || "X11 ChangeProperty8 on window title failed.",
- xcb.change_property8(
- xproto::PropMode::REPLACE,
- x_window,
- xproto::AtomEnum::WM_NAME,
- xproto::AtomEnum::STRING,
- title.as_bytes(),
- ),
- )?;
- }
+ if let Some(titlebar) = params.titlebar
+ && let Some(title) = titlebar.title
+ {
+ check_reply(
+ || "X11 ChangeProperty8 on window title failed.",
+ xcb.change_property8(
+ xproto::PropMode::REPLACE,
+ x_window,
+ xproto::AtomEnum::WM_NAME,
+ xproto::AtomEnum::STRING,
+ title.as_bytes(),
+ ),
+ )?;
}
if params.kind == WindowKind::PopUp {
check_reply(
@@ -604,7 +604,7 @@ impl X11WindowState {
),
)?;
- xcb_flush(&xcb);
+ xcb_flush(xcb);
let renderer = {
let raw_window = RawWindow {
@@ -664,7 +664,7 @@ impl X11WindowState {
|| "X11 DestroyWindow failed while cleaning it up after setup failure.",
xcb.destroy_window(x_window),
)?;
- xcb_flush(&xcb);
+ xcb_flush(xcb);
}
setup_result
@@ -956,10 +956,10 @@ impl X11WindowStatePtr {
}
pub fn handle_input(&self, input: PlatformInput) {
- if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
- if !fun(input.clone()).propagate {
- return;
- }
+ if let Some(ref mut fun) = self.callbacks.borrow_mut().input
+ && !fun(input.clone()).propagate
+ {
+ return;
}
if let PlatformInput::KeyDown(event) = input {
// only allow shift modifier when inserting text
@@ -1019,8 +1019,9 @@ impl X11WindowStatePtr {
}
}
- pub fn get_ime_area(&self) -> Option<Bounds<Pixels>> {
+ pub fn get_ime_area(&self) -> Option<Bounds<ScaledPixels>> {
let mut state = self.state.borrow_mut();
+ let scale_factor = state.scale_factor;
let mut bounds: Option<Bounds<Pixels>> = None;
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -1030,7 +1031,7 @@ impl X11WindowStatePtr {
let mut state = self.state.borrow_mut();
state.input_handler = Some(input_handler);
};
- bounds
+ bounds.map(|b| b.scale(scale_factor))
}
pub fn set_bounds(&self, bounds: Bounds<i32>) -> anyhow::Result<()> {
@@ -1068,15 +1069,14 @@ impl X11WindowStatePtr {
}
let mut callbacks = self.callbacks.borrow_mut();
- if let Some((content_size, scale_factor)) = resize_args {
- if let Some(ref mut fun) = callbacks.resize {
- fun(content_size, scale_factor)
- }
+ if let Some((content_size, scale_factor)) = resize_args
+ && let Some(ref mut fun) = callbacks.resize
+ {
+ fun(content_size, scale_factor)
}
- if !is_resize {
- if let Some(ref mut fun) = callbacks.moved {
- fun();
- }
+
+ if !is_resize && let Some(ref mut fun) = callbacks.moved {
+ fun();
}
Ok(())
@@ -1619,7 +1619,7 @@ impl PlatformWindow for X11Window {
}
}
- fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
+ fn update_ime_position(&self, bounds: Bounds<Pixels>) {
let mut state = self.0.state.borrow_mut();
let client = state.client.clone();
drop(state);
@@ -311,9 +311,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask)
- && first_char.map_or(true, |ch| {
- !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)
- });
+ && first_char
+ .is_none_or(|ch| !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch));
#[allow(non_upper_case_globals)]
let key = match first_char {
@@ -427,7 +426,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
key_char = Some(chars_for_modified_key(native_event.keyCode(), mods));
}
- let mut key = if shift
+ if shift
&& chars_ignoring_modifiers
.chars()
.all(|c| c.is_ascii_lowercase())
@@ -438,9 +437,7 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
chars_with_shift
} else {
chars_ignoring_modifiers
- };
-
- key
+ }
}
};
@@ -1,8 +1,9 @@
+use collections::HashMap;
use std::ffi::{CStr, c_void};
use objc::{msg_send, runtime::Object, sel, sel_impl};
-use crate::PlatformKeyboardLayout;
+use crate::{KeybindingKeystroke, Keystroke, PlatformKeyboardLayout, PlatformKeyboardMapper};
use super::{
TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID,
@@ -14,6 +15,10 @@ pub(crate) struct MacKeyboardLayout {
name: String,
}
+pub(crate) struct MacKeyboardMapper {
+ key_equivalents: Option<HashMap<char, char>>,
+}
+
impl PlatformKeyboardLayout for MacKeyboardLayout {
fn id(&self) -> &str {
&self.id
@@ -24,6 +29,27 @@ impl PlatformKeyboardLayout for MacKeyboardLayout {
}
}
+impl PlatformKeyboardMapper for MacKeyboardMapper {
+ fn map_key_equivalent(
+ &self,
+ mut keystroke: Keystroke,
+ use_key_equivalents: bool,
+ ) -> KeybindingKeystroke {
+ if use_key_equivalents && let Some(key_equivalents) = &self.key_equivalents {
+ if keystroke.key.chars().count() == 1
+ && let Some(key) = key_equivalents.get(&keystroke.key.chars().next().unwrap())
+ {
+ keystroke.key = key.to_string();
+ }
+ }
+ KeybindingKeystroke::from_keystroke(keystroke)
+ }
+
+ fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
+ self.key_equivalents.as_ref()
+ }
+}
+
impl MacKeyboardLayout {
pub(crate) fn new() -> Self {
unsafe {
@@ -47,3 +73,1428 @@ impl MacKeyboardLayout {
}
}
}
+
+impl MacKeyboardMapper {
+ pub(crate) fn new(layout_id: &str) -> Self {
+ let key_equivalents = get_key_equivalents(layout_id);
+
+ Self { key_equivalents }
+ }
+}
+
+// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range
+// without using option. This means that some of our built in keyboard shortcuts do not work
+// for those users.
+//
+// The way macOS solves this problem is to move shortcuts around so that they are all reachable,
+// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct
+//
+// For example, cmd-> is the "switch window" shortcut because the > key is right above tab.
+// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves
+// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position
+// as cmd-> on a QWERTY layout.
+//
+// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö
+// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard
+// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the
+// specific key moves)
+//
+// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every
+// possible key combination, and inspecting the UI to see what it rendered. So that's what we did...
+//
+// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the
+// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with:
+// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add'
+// From there I used multi-cursor to produce this match statement.
+fn get_key_equivalents(layout_id: &str) -> Option<HashMap<char, char>> {
+ let mappings: &[(char, char)] = match layout_id {
+ "com.apple.keylayout.ABC-AZERTY" => &[
+ ('!', '1'),
+ ('"', '%'),
+ ('#', '3'),
+ ('$', '4'),
+ ('%', '5'),
+ ('&', '7'),
+ ('(', '9'),
+ (')', '0'),
+ ('*', '8'),
+ ('.', ';'),
+ ('/', ':'),
+ ('0', 'à'),
+ ('1', '&'),
+ ('2', 'é'),
+ ('3', '"'),
+ ('4', '\''),
+ ('5', '('),
+ ('6', '§'),
+ ('7', 'è'),
+ ('8', '!'),
+ ('9', 'ç'),
+ (':', '°'),
+ (';', ')'),
+ ('<', '.'),
+ ('>', '/'),
+ ('@', '2'),
+ ('[', '^'),
+ ('\'', 'ù'),
+ ('\\', '`'),
+ (']', '$'),
+ ('^', '6'),
+ ('`', '<'),
+ ('{', '¨'),
+ ('|', '£'),
+ ('}', '*'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.ABC-QWERTZ" => &[
+ ('"', '`'),
+ ('#', '§'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', 'ß'),
+ (':', 'Ü'),
+ (';', 'ü'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ö'),
+ ('\'', '´'),
+ ('\\', '#'),
+ (']', 'ä'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ö'),
+ ('|', '\''),
+ ('}', 'Ä'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Albanian" => &[
+ ('"', '\''),
+ (':', 'Ç'),
+ (';', 'ç'),
+ ('<', ';'),
+ ('>', ':'),
+ ('@', '"'),
+ ('\'', '@'),
+ ('\\', 'ë'),
+ ('`', '<'),
+ ('|', 'Ë'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Austrian" => &[
+ ('"', '`'),
+ ('#', '§'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', 'ß'),
+ (':', 'Ü'),
+ (';', 'ü'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ö'),
+ ('\'', '´'),
+ ('\\', '#'),
+ (']', 'ä'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ö'),
+ ('|', '\''),
+ ('}', 'Ä'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Azeri" => &[
+ ('"', 'Ə'),
+ (',', 'ç'),
+ ('.', 'ş'),
+ ('/', '.'),
+ (':', 'I'),
+ (';', 'ı'),
+ ('<', 'Ç'),
+ ('>', 'Ş'),
+ ('?', ','),
+ ('W', 'Ü'),
+ ('[', 'ö'),
+ ('\'', 'ə'),
+ (']', 'ğ'),
+ ('w', 'ü'),
+ ('{', 'Ö'),
+ ('|', '/'),
+ ('}', 'Ğ'),
+ ],
+ "com.apple.keylayout.Belgian" => &[
+ ('!', '1'),
+ ('"', '%'),
+ ('#', '3'),
+ ('$', '4'),
+ ('%', '5'),
+ ('&', '7'),
+ ('(', '9'),
+ (')', '0'),
+ ('*', '8'),
+ ('.', ';'),
+ ('/', ':'),
+ ('0', 'à'),
+ ('1', '&'),
+ ('2', 'é'),
+ ('3', '"'),
+ ('4', '\''),
+ ('5', '('),
+ ('6', '§'),
+ ('7', 'è'),
+ ('8', '!'),
+ ('9', 'ç'),
+ (':', '°'),
+ (';', ')'),
+ ('<', '.'),
+ ('>', '/'),
+ ('@', '2'),
+ ('[', '^'),
+ ('\'', 'ù'),
+ ('\\', '`'),
+ (']', '$'),
+ ('^', '6'),
+ ('`', '<'),
+ ('{', '¨'),
+ ('|', '£'),
+ ('}', '*'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Brazilian-ABNT2" => &[
+ ('"', '`'),
+ ('/', 'ç'),
+ ('?', 'Ç'),
+ ('\'', '´'),
+ ('\\', '~'),
+ ('^', '¨'),
+ ('`', '\''),
+ ('|', '^'),
+ ('~', '"'),
+ ],
+ "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')],
+ "com.apple.keylayout.British" => &[('#', '£')],
+ "com.apple.keylayout.Canadian-CSA" => &[
+ ('"', 'È'),
+ ('/', 'é'),
+ ('<', '\''),
+ ('>', '"'),
+ ('?', 'É'),
+ ('[', '^'),
+ ('\'', 'è'),
+ ('\\', 'à'),
+ (']', 'ç'),
+ ('`', 'ù'),
+ ('{', '¨'),
+ ('|', 'À'),
+ ('}', 'Ç'),
+ ('~', 'Ù'),
+ ],
+ "com.apple.keylayout.Croatian" => &[
+ ('"', 'Ć'),
+ ('&', '\''),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ (':', 'Č'),
+ (';', 'č'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'š'),
+ ('\'', 'ć'),
+ ('\\', 'ž'),
+ (']', 'đ'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Š'),
+ ('|', 'Ž'),
+ ('}', 'Đ'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Croatian-PC" => &[
+ ('"', 'Ć'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '\''),
+ (':', 'Č'),
+ (';', 'č'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'š'),
+ ('\'', 'ć'),
+ ('\\', 'ž'),
+ (']', 'đ'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Š'),
+ ('|', 'Ž'),
+ ('}', 'Đ'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Czech" => &[
+ ('!', '1'),
+ ('"', '!'),
+ ('#', '3'),
+ ('$', '4'),
+ ('%', '5'),
+ ('&', '7'),
+ ('(', '9'),
+ (')', '0'),
+ ('*', '8'),
+ ('+', '%'),
+ ('/', '\''),
+ ('0', 'é'),
+ ('1', '+'),
+ ('2', 'ě'),
+ ('3', 'š'),
+ ('4', 'č'),
+ ('5', 'ř'),
+ ('6', 'ž'),
+ ('7', 'ý'),
+ ('8', 'á'),
+ ('9', 'í'),
+ (':', '"'),
+ (';', 'ů'),
+ ('<', '?'),
+ ('>', ':'),
+ ('?', 'ˇ'),
+ ('@', '2'),
+ ('[', 'ú'),
+ ('\'', '§'),
+ (']', ')'),
+ ('^', '6'),
+ ('`', '¨'),
+ ('{', 'Ú'),
+ ('}', '('),
+ ('~', '`'),
+ ],
+ "com.apple.keylayout.Czech-QWERTY" => &[
+ ('!', '1'),
+ ('"', '!'),
+ ('#', '3'),
+ ('$', '4'),
+ ('%', '5'),
+ ('&', '7'),
+ ('(', '9'),
+ (')', '0'),
+ ('*', '8'),
+ ('+', '%'),
+ ('/', '\''),
+ ('0', 'é'),
+ ('1', '+'),
+ ('2', 'ě'),
+ ('3', 'š'),
+ ('4', 'č'),
+ ('5', 'ř'),
+ ('6', 'ž'),
+ ('7', 'ý'),
+ ('8', 'á'),
+ ('9', 'í'),
+ (':', '"'),
+ (';', 'ů'),
+ ('<', '?'),
+ ('>', ':'),
+ ('?', 'ˇ'),
+ ('@', '2'),
+ ('[', 'ú'),
+ ('\'', '§'),
+ (']', ')'),
+ ('^', '6'),
+ ('`', '¨'),
+ ('{', 'Ú'),
+ ('}', '('),
+ ('~', '`'),
+ ],
+ "com.apple.keylayout.Danish" => &[
+ ('"', '^'),
+ ('$', '€'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'æ'),
+ ('\'', '¨'),
+ ('\\', '\''),
+ (']', 'ø'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Æ'),
+ ('|', '*'),
+ ('}', 'Ø'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Faroese" => &[
+ ('"', 'Ø'),
+ ('$', '€'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Æ'),
+ (';', 'æ'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'å'),
+ ('\'', 'ø'),
+ ('\\', '\''),
+ (']', 'ð'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Å'),
+ ('|', '*'),
+ ('}', 'Ð'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Finnish" => &[
+ ('"', '^'),
+ ('$', '€'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ö'),
+ ('\'', '¨'),
+ ('\\', '\''),
+ (']', 'ä'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ö'),
+ ('|', '*'),
+ ('}', 'Ä'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.FinnishExtended" => &[
+ ('"', 'ˆ'),
+ ('$', '€'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ö'),
+ ('\'', '¨'),
+ ('\\', '\''),
+ (']', 'ä'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ö'),
+ ('|', '*'),
+ ('}', 'Ä'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.FinnishSami-PC" => &[
+ ('"', 'ˆ'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ö'),
+ ('\'', '¨'),
+ ('\\', '@'),
+ (']', 'ä'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ö'),
+ ('|', '*'),
+ ('}', 'Ä'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.French" => &[
+ ('!', '1'),
+ ('"', '%'),
+ ('#', '3'),
+ ('$', '4'),
+ ('%', '5'),
+ ('&', '7'),
+ ('(', '9'),
+ (')', '0'),
+ ('*', '8'),
+ ('.', ';'),
+ ('/', ':'),
+ ('0', 'à'),
+ ('1', '&'),
+ ('2', 'é'),
+ ('3', '"'),
+ ('4', '\''),
+ ('5', '('),
+ ('6', '§'),
+ ('7', 'è'),
+ ('8', '!'),
+ ('9', 'ç'),
+ (':', '°'),
+ (';', ')'),
+ ('<', '.'),
+ ('>', '/'),
+ ('@', '2'),
+ ('[', '^'),
+ ('\'', 'ù'),
+ ('\\', '`'),
+ (']', '$'),
+ ('^', '6'),
+ ('`', '<'),
+ ('{', '¨'),
+ ('|', '£'),
+ ('}', '*'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.French-PC" => &[
+ ('!', '1'),
+ ('"', '%'),
+ ('#', '3'),
+ ('$', '4'),
+ ('%', '5'),
+ ('&', '7'),
+ ('(', '9'),
+ (')', '0'),
+ ('*', '8'),
+ ('-', ')'),
+ ('.', ';'),
+ ('/', ':'),
+ ('0', 'à'),
+ ('1', '&'),
+ ('2', 'é'),
+ ('3', '"'),
+ ('4', '\''),
+ ('5', '('),
+ ('6', '-'),
+ ('7', 'è'),
+ ('8', '_'),
+ ('9', 'ç'),
+ (':', '§'),
+ (';', '!'),
+ ('<', '.'),
+ ('>', '/'),
+ ('@', '2'),
+ ('[', '^'),
+ ('\'', 'ù'),
+ ('\\', '*'),
+ (']', '$'),
+ ('^', '6'),
+ ('_', '°'),
+ ('`', '<'),
+ ('{', '¨'),
+ ('|', 'μ'),
+ ('}', '£'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.French-numerical" => &[
+ ('!', '1'),
+ ('"', '%'),
+ ('#', '3'),
+ ('$', '4'),
+ ('%', '5'),
+ ('&', '7'),
+ ('(', '9'),
+ (')', '0'),
+ ('*', '8'),
+ ('.', ';'),
+ ('/', ':'),
+ ('0', 'à'),
+ ('1', '&'),
+ ('2', 'é'),
+ ('3', '"'),
+ ('4', '\''),
+ ('5', '('),
+ ('6', '§'),
+ ('7', 'è'),
+ ('8', '!'),
+ ('9', 'ç'),
+ (':', '°'),
+ (';', ')'),
+ ('<', '.'),
+ ('>', '/'),
+ ('@', '2'),
+ ('[', '^'),
+ ('\'', 'ù'),
+ ('\\', '`'),
+ (']', '$'),
+ ('^', '6'),
+ ('`', '<'),
+ ('{', '¨'),
+ ('|', '£'),
+ ('}', '*'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.German" => &[
+ ('"', '`'),
+ ('#', '§'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', 'ß'),
+ (':', 'Ü'),
+ (';', 'ü'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ö'),
+ ('\'', '´'),
+ ('\\', '#'),
+ (']', 'ä'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ö'),
+ ('|', '\''),
+ ('}', 'Ä'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.German-DIN-2137" => &[
+ ('"', '`'),
+ ('#', '§'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', 'ß'),
+ (':', 'Ü'),
+ (';', 'ü'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ö'),
+ ('\'', '´'),
+ ('\\', '#'),
+ (']', 'ä'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ö'),
+ ('|', '\''),
+ ('}', 'Ä'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')],
+ "com.apple.keylayout.Hungarian" => &[
+ ('!', '\''),
+ ('"', 'Á'),
+ ('#', '+'),
+ ('$', '!'),
+ ('&', '='),
+ ('(', ')'),
+ (')', 'Ö'),
+ ('*', '('),
+ ('+', 'Ó'),
+ ('/', 'ü'),
+ ('0', 'ö'),
+ (':', 'É'),
+ (';', 'é'),
+ ('<', 'Ü'),
+ ('=', 'ó'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ő'),
+ ('\'', 'á'),
+ ('\\', 'ű'),
+ (']', 'ú'),
+ ('^', '/'),
+ ('`', 'í'),
+ ('{', 'Ő'),
+ ('|', 'Ű'),
+ ('}', 'Ú'),
+ ('~', 'Í'),
+ ],
+ "com.apple.keylayout.Hungarian-QWERTY" => &[
+ ('!', '\''),
+ ('"', 'Á'),
+ ('#', '+'),
+ ('$', '!'),
+ ('&', '='),
+ ('(', ')'),
+ (')', 'Ö'),
+ ('*', '('),
+ ('+', 'Ó'),
+ ('/', 'ü'),
+ ('0', 'ö'),
+ (':', 'É'),
+ (';', 'é'),
+ ('<', 'Ü'),
+ ('=', 'ó'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ő'),
+ ('\'', 'á'),
+ ('\\', 'ű'),
+ (']', 'ú'),
+ ('^', '/'),
+ ('`', 'í'),
+ ('{', 'Ő'),
+ ('|', 'Ű'),
+ ('}', 'Ú'),
+ ('~', 'Í'),
+ ],
+ "com.apple.keylayout.Icelandic" => &[
+ ('"', 'Ö'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '\''),
+ (':', 'Ð'),
+ (';', 'ð'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'æ'),
+ ('\'', 'ö'),
+ ('\\', 'þ'),
+ (']', '´'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Æ'),
+ ('|', 'Þ'),
+ ('}', '´'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Irish" => &[('#', '£')],
+ "com.apple.keylayout.IrishExtended" => &[('#', '£')],
+ "com.apple.keylayout.Italian" => &[
+ ('!', '1'),
+ ('"', '%'),
+ ('#', '3'),
+ ('$', '4'),
+ ('%', '5'),
+ ('&', '7'),
+ ('(', '9'),
+ (')', '0'),
+ ('*', '8'),
+ (',', ';'),
+ ('.', ':'),
+ ('/', ','),
+ ('0', 'é'),
+ ('1', '&'),
+ ('2', '"'),
+ ('3', '\''),
+ ('4', '('),
+ ('5', 'ç'),
+ ('6', 'è'),
+ ('7', ')'),
+ ('8', '£'),
+ ('9', 'à'),
+ (':', '!'),
+ (';', 'ò'),
+ ('<', '.'),
+ ('>', '/'),
+ ('@', '2'),
+ ('[', 'ì'),
+ ('\'', 'ù'),
+ ('\\', '§'),
+ (']', '$'),
+ ('^', '6'),
+ ('`', '<'),
+ ('{', '^'),
+ ('|', '°'),
+ ('}', '*'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Italian-Pro" => &[
+ ('"', '^'),
+ ('#', '£'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '\''),
+ (':', 'é'),
+ (';', 'è'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ò'),
+ ('\'', 'ì'),
+ ('\\', 'ù'),
+ (']', 'à'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'ç'),
+ ('|', '§'),
+ ('}', '°'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.LatinAmerican" => &[
+ ('"', '¨'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '\''),
+ (':', 'Ñ'),
+ (';', 'ñ'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', '{'),
+ ('\'', '´'),
+ ('\\', '¿'),
+ (']', '}'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', '['),
+ ('|', '¡'),
+ ('}', ']'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Lithuanian" => &[
+ ('!', 'Ą'),
+ ('#', 'Ę'),
+ ('$', 'Ė'),
+ ('%', 'Į'),
+ ('&', 'Ų'),
+ ('*', 'Ū'),
+ ('+', 'Ž'),
+ ('1', 'ą'),
+ ('2', 'č'),
+ ('3', 'ę'),
+ ('4', 'ė'),
+ ('5', 'į'),
+ ('6', 'š'),
+ ('7', 'ų'),
+ ('8', 'ū'),
+ ('=', 'ž'),
+ ('@', 'Č'),
+ ('^', 'Š'),
+ ],
+ "com.apple.keylayout.Maltese" => &[
+ ('#', '£'),
+ ('[', 'ġ'),
+ (']', 'ħ'),
+ ('`', 'ż'),
+ ('{', 'Ġ'),
+ ('}', 'Ħ'),
+ ('~', 'Ż'),
+ ],
+ "com.apple.keylayout.NorthernSami" => &[
+ ('"', 'Ŋ'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('Q', 'Á'),
+ ('W', 'Š'),
+ ('X', 'Č'),
+ ('[', 'ø'),
+ ('\'', 'ŋ'),
+ ('\\', 'đ'),
+ (']', 'æ'),
+ ('^', '&'),
+ ('`', 'ž'),
+ ('q', 'á'),
+ ('w', 'š'),
+ ('x', 'č'),
+ ('{', 'Ø'),
+ ('|', 'Đ'),
+ ('}', 'Æ'),
+ ('~', 'Ž'),
+ ],
+ "com.apple.keylayout.Norwegian" => &[
+ ('"', '^'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ø'),
+ ('\'', '¨'),
+ ('\\', '@'),
+ (']', 'æ'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ø'),
+ ('|', '*'),
+ ('}', 'Æ'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.NorwegianExtended" => &[
+ ('"', 'ˆ'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ø'),
+ ('\\', '@'),
+ (']', 'æ'),
+ ('`', '<'),
+ ('}', 'Æ'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.NorwegianSami-PC" => &[
+ ('"', 'ˆ'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ø'),
+ ('\'', '¨'),
+ ('\\', '@'),
+ (']', 'æ'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ø'),
+ ('|', '*'),
+ ('}', 'Æ'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Polish" => &[
+ ('!', '§'),
+ ('"', 'ę'),
+ ('#', '!'),
+ ('$', '?'),
+ ('%', '+'),
+ ('&', ':'),
+ ('(', '/'),
+ (')', '"'),
+ ('*', '_'),
+ ('+', ']'),
+ (',', '.'),
+ ('.', ','),
+ ('/', 'ż'),
+ (':', 'Ł'),
+ (';', 'ł'),
+ ('<', 'ś'),
+ ('=', '['),
+ ('>', 'ń'),
+ ('?', 'Ż'),
+ ('@', '%'),
+ ('[', 'ó'),
+ ('\'', 'ą'),
+ ('\\', ';'),
+ (']', '('),
+ ('^', '='),
+ ('_', 'ć'),
+ ('`', '<'),
+ ('{', 'ź'),
+ ('|', '$'),
+ ('}', ')'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Portuguese" => &[
+ ('"', '`'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '\''),
+ (':', 'ª'),
+ (';', 'º'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ç'),
+ ('\'', '´'),
+ (']', '~'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ç'),
+ ('}', '^'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Sami-PC" => &[
+ ('"', 'Ŋ'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('Q', 'Á'),
+ ('W', 'Š'),
+ ('X', 'Č'),
+ ('[', 'ø'),
+ ('\'', 'ŋ'),
+ ('\\', 'đ'),
+ (']', 'æ'),
+ ('^', '&'),
+ ('`', 'ž'),
+ ('q', 'á'),
+ ('w', 'š'),
+ ('x', 'č'),
+ ('{', 'Ø'),
+ ('|', 'Đ'),
+ ('}', 'Æ'),
+ ('~', 'Ž'),
+ ],
+ "com.apple.keylayout.Serbian-Latin" => &[
+ ('"', 'Ć'),
+ ('&', '\''),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ (':', 'Č'),
+ (';', 'č'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'š'),
+ ('\'', 'ć'),
+ ('\\', 'ž'),
+ (']', 'đ'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Š'),
+ ('|', 'Ž'),
+ ('}', 'Đ'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Slovak" => &[
+ ('!', '1'),
+ ('"', '!'),
+ ('#', '3'),
+ ('$', '4'),
+ ('%', '5'),
+ ('&', '7'),
+ ('(', '9'),
+ (')', '0'),
+ ('*', '8'),
+ ('+', '%'),
+ ('/', '\''),
+ ('0', 'é'),
+ ('1', '+'),
+ ('2', 'ľ'),
+ ('3', 'š'),
+ ('4', 'č'),
+ ('5', 'ť'),
+ ('6', 'ž'),
+ ('7', 'ý'),
+ ('8', 'á'),
+ ('9', 'í'),
+ (':', '"'),
+ (';', 'ô'),
+ ('<', '?'),
+ ('>', ':'),
+ ('?', 'ˇ'),
+ ('@', '2'),
+ ('[', 'ú'),
+ ('\'', '§'),
+ (']', 'ä'),
+ ('^', '6'),
+ ('`', 'ň'),
+ ('{', 'Ú'),
+ ('}', 'Ä'),
+ ('~', 'Ň'),
+ ],
+ "com.apple.keylayout.Slovak-QWERTY" => &[
+ ('!', '1'),
+ ('"', '!'),
+ ('#', '3'),
+ ('$', '4'),
+ ('%', '5'),
+ ('&', '7'),
+ ('(', '9'),
+ (')', '0'),
+ ('*', '8'),
+ ('+', '%'),
+ ('/', '\''),
+ ('0', 'é'),
+ ('1', '+'),
+ ('2', 'ľ'),
+ ('3', 'š'),
+ ('4', 'č'),
+ ('5', 'ť'),
+ ('6', 'ž'),
+ ('7', 'ý'),
+ ('8', 'á'),
+ ('9', 'í'),
+ (':', '"'),
+ (';', 'ô'),
+ ('<', '?'),
+ ('>', ':'),
+ ('?', 'ˇ'),
+ ('@', '2'),
+ ('[', 'ú'),
+ ('\'', '§'),
+ (']', 'ä'),
+ ('^', '6'),
+ ('`', 'ň'),
+ ('{', 'Ú'),
+ ('}', 'Ä'),
+ ('~', 'Ň'),
+ ],
+ "com.apple.keylayout.Slovenian" => &[
+ ('"', 'Ć'),
+ ('&', '\''),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ (':', 'Č'),
+ (';', 'č'),
+ ('<', ';'),
+ ('=', '*'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'š'),
+ ('\'', 'ć'),
+ ('\\', 'ž'),
+ (']', 'đ'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Š'),
+ ('|', 'Ž'),
+ ('}', 'Đ'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Spanish" => &[
+ ('!', '¡'),
+ ('"', '¨'),
+ ('.', 'ç'),
+ ('/', '.'),
+ (':', 'º'),
+ (';', '´'),
+ ('<', '¿'),
+ ('>', 'Ç'),
+ ('@', '!'),
+ ('[', 'ñ'),
+ ('\'', '`'),
+ ('\\', '\''),
+ (']', ';'),
+ ('^', '/'),
+ ('`', '<'),
+ ('{', 'Ñ'),
+ ('|', '"'),
+ ('}', ':'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Spanish-ISO" => &[
+ ('"', '¨'),
+ ('#', '·'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('.', 'ç'),
+ ('/', '.'),
+ (':', 'º'),
+ (';', '´'),
+ ('<', '¿'),
+ ('>', 'Ç'),
+ ('@', '"'),
+ ('[', 'ñ'),
+ ('\'', '`'),
+ ('\\', '\''),
+ (']', ';'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ñ'),
+ ('|', '"'),
+ ('}', '`'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Swedish" => &[
+ ('"', '^'),
+ ('$', '€'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ö'),
+ ('\'', '¨'),
+ ('\\', '\''),
+ (']', 'ä'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ö'),
+ ('|', '*'),
+ ('}', 'Ä'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Swedish-Pro" => &[
+ ('"', '^'),
+ ('$', '€'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ö'),
+ ('\'', '¨'),
+ ('\\', '\''),
+ (']', 'ä'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ö'),
+ ('|', '*'),
+ ('}', 'Ä'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.SwedishSami-PC" => &[
+ ('"', 'ˆ'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('/', '´'),
+ (':', 'Å'),
+ (';', 'å'),
+ ('<', ';'),
+ ('=', '`'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ö'),
+ ('\'', '¨'),
+ ('\\', '@'),
+ (']', 'ä'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ö'),
+ ('|', '*'),
+ ('}', 'Ä'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.SwissFrench" => &[
+ ('!', '+'),
+ ('"', '`'),
+ ('#', '*'),
+ ('$', 'ç'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('+', '!'),
+ ('/', '\''),
+ (':', 'ü'),
+ (';', 'è'),
+ ('<', ';'),
+ ('=', '¨'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'é'),
+ ('\'', '^'),
+ ('\\', '$'),
+ (']', 'à'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'ö'),
+ ('|', '£'),
+ ('}', 'ä'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.SwissGerman" => &[
+ ('!', '+'),
+ ('"', '`'),
+ ('#', '*'),
+ ('$', 'ç'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('+', '!'),
+ ('/', '\''),
+ (':', 'è'),
+ (';', 'ü'),
+ ('<', ';'),
+ ('=', '¨'),
+ ('>', ':'),
+ ('@', '"'),
+ ('[', 'ö'),
+ ('\'', '^'),
+ ('\\', '$'),
+ (']', 'ä'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'é'),
+ ('|', '£'),
+ ('}', 'à'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Turkish" => &[
+ ('"', '-'),
+ ('#', '"'),
+ ('$', '\''),
+ ('%', '('),
+ ('&', ')'),
+ ('(', '%'),
+ (')', ':'),
+ ('*', '_'),
+ (',', 'ö'),
+ ('-', 'ş'),
+ ('.', 'ç'),
+ ('/', '.'),
+ (':', '$'),
+ ('<', 'Ö'),
+ ('>', 'Ç'),
+ ('@', '*'),
+ ('[', 'ğ'),
+ ('\'', ','),
+ ('\\', 'ü'),
+ (']', 'ı'),
+ ('^', '/'),
+ ('_', 'Ş'),
+ ('`', '<'),
+ ('{', 'Ğ'),
+ ('|', 'Ü'),
+ ('}', 'I'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Turkish-QWERTY-PC" => &[
+ ('"', 'I'),
+ ('#', '^'),
+ ('$', '+'),
+ ('&', '/'),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ ('+', ':'),
+ (',', 'ö'),
+ ('.', 'ç'),
+ ('/', '*'),
+ (':', 'Ş'),
+ (';', 'ş'),
+ ('<', 'Ö'),
+ ('=', '.'),
+ ('>', 'Ç'),
+ ('@', '\''),
+ ('[', 'ğ'),
+ ('\'', 'ı'),
+ ('\\', ','),
+ (']', 'ü'),
+ ('^', '&'),
+ ('`', '<'),
+ ('{', 'Ğ'),
+ ('|', ';'),
+ ('}', 'Ü'),
+ ('~', '>'),
+ ],
+ "com.apple.keylayout.Turkish-Standard" => &[
+ ('"', 'Ş'),
+ ('#', '^'),
+ ('&', '\''),
+ ('(', ')'),
+ (')', '='),
+ ('*', '('),
+ (',', '.'),
+ ('.', ','),
+ (':', 'Ç'),
+ (';', 'ç'),
+ ('<', ':'),
+ ('=', '*'),
+ ('>', ';'),
+ ('@', '"'),
+ ('[', 'ğ'),
+ ('\'', 'ş'),
+ ('\\', 'ü'),
+ (']', 'ı'),
+ ('^', '&'),
+ ('`', 'ö'),
+ ('{', 'Ğ'),
+ ('|', 'Ü'),
+ ('}', 'I'),
+ ('~', 'Ö'),
+ ],
+ "com.apple.keylayout.Turkmen" => &[
+ ('C', 'Ç'),
+ ('Q', 'Ä'),
+ ('V', 'Ý'),
+ ('X', 'Ü'),
+ ('[', 'ň'),
+ ('\\', 'ş'),
+ (']', 'ö'),
+ ('^', '№'),
+ ('`', 'ž'),
+ ('c', 'ç'),
+ ('q', 'ä'),
+ ('v', 'ý'),
+ ('x', 'ü'),
+ ('{', 'Ň'),
+ ('|', 'Ş'),
+ ('}', 'Ö'),
+ ('~', 'Ž'),
+ ],
+ "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')],
+ "com.apple.keylayout.Welsh" => &[('#', '£')],
+
+ _ => return None,
+ };
+
+ Some(HashMap::from_iter(mappings.iter().cloned()))
+}
@@ -314,6 +314,15 @@ impl MetalRenderer {
}
fn update_path_intermediate_textures(&mut self, size: Size<DevicePixels>) {
+ // We are uncertain when this happens, but sometimes size can be 0 here. Most likely before
+ // the layout pass on window creation. Zero-sized texture creation causes SIGABRT.
+ // https://github.com/zed-industries/zed/issues/36229
+ if size.width.0 <= 0 || size.height.0 <= 0 {
+ self.path_intermediate_texture = None;
+ self.path_intermediate_msaa_texture = None;
+ return;
+ }
+
let texture_descriptor = metal::TextureDescriptor::new();
texture_descriptor.set_width(size.width.0 as u64);
texture_descriptor.set_height(size.height.0 as u64);
@@ -323,7 +332,7 @@ impl MetalRenderer {
self.path_intermediate_texture = Some(self.device.new_texture(&texture_descriptor));
if self.path_sample_count > 1 {
- let mut msaa_descriptor = texture_descriptor.clone();
+ let mut msaa_descriptor = texture_descriptor;
msaa_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample);
msaa_descriptor.set_storage_mode(metal::MTLStorageMode::Private);
msaa_descriptor.set_sample_count(self.path_sample_count as _);
@@ -436,14 +445,14 @@ impl MetalRenderer {
instance_buffer,
&mut instance_offset,
viewport_size,
- &command_encoder,
+ command_encoder,
),
PrimitiveBatch::Quads(quads) => self.draw_quads(
quads,
instance_buffer,
&mut instance_offset,
viewport_size,
- &command_encoder,
+ command_encoder,
),
PrimitiveBatch::Paths(paths) => {
command_encoder.end_encoding();
@@ -471,7 +480,7 @@ impl MetalRenderer {
instance_buffer,
&mut instance_offset,
viewport_size,
- &command_encoder,
+ command_encoder,
)
} else {
false
@@ -482,7 +491,7 @@ impl MetalRenderer {
instance_buffer,
&mut instance_offset,
viewport_size,
- &command_encoder,
+ command_encoder,
),
PrimitiveBatch::MonochromeSprites {
texture_id,
@@ -493,7 +502,7 @@ impl MetalRenderer {
instance_buffer,
&mut instance_offset,
viewport_size,
- &command_encoder,
+ command_encoder,
),
PrimitiveBatch::PolychromeSprites {
texture_id,
@@ -504,14 +513,14 @@ impl MetalRenderer {
instance_buffer,
&mut instance_offset,
viewport_size,
- &command_encoder,
+ command_encoder,
),
PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(
surfaces,
instance_buffer,
&mut instance_offset,
viewport_size,
- &command_encoder,
+ command_encoder,
),
};
if !ok {
@@ -754,7 +763,7 @@ impl MetalRenderer {
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
- let Some(ref first_path) = paths.first() else {
+ let Some(first_path) = paths.first() else {
return true;
};
@@ -35,14 +35,14 @@ pub fn apply_features_and_fallbacks(
unsafe {
let mut keys = vec![kCTFontFeatureSettingsAttribute];
let mut values = vec![generate_feature_array(features)];
- if let Some(fallbacks) = fallbacks {
- if !fallbacks.fallback_list().is_empty() {
- keys.push(kCTFontCascadeListAttribute);
- values.push(generate_fallback_array(
- fallbacks,
- font.native_font().as_concrete_TypeRef(),
- ));
- }
+ if let Some(fallbacks) = fallbacks
+ && !fallbacks.fallback_list().is_empty()
+ {
+ keys.push(kCTFontCascadeListAttribute);
+ values.push(generate_fallback_array(
+ fallbacks,
+ font.native_font().as_concrete_TypeRef(),
+ ));
}
let attrs = CFDictionaryCreate(
kCFAllocatorDefault,
@@ -1,5 +1,5 @@
use super::{
- BoolExt, MacKeyboardLayout,
+ BoolExt, MacKeyboardLayout, MacKeyboardMapper,
attributed_string::{NSAttributedString, NSMutableAttributedString},
events::key_to_native,
renderer,
@@ -8,8 +8,9 @@ use crate::{
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
- PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
- SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
+ PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
+ PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams,
+ hash,
};
use anyhow::{Context as _, anyhow};
use block::ConcreteBlock;
@@ -171,6 +172,7 @@ pub(crate) struct MacPlatformState {
finish_launching: Option<Box<dyn FnOnce()>>,
dock_menu: Option<id>,
menus: Option<Vec<OwnedMenu>>,
+ keyboard_mapper: Rc<MacKeyboardMapper>,
}
impl Default for MacPlatform {
@@ -189,6 +191,9 @@ impl MacPlatform {
#[cfg(not(feature = "font-kit"))]
let text_system = Arc::new(crate::NoopTextSystem::new());
+ let keyboard_layout = MacKeyboardLayout::new();
+ let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
+
Self(Mutex::new(MacPlatformState {
headless,
text_system,
@@ -209,6 +214,7 @@ impl MacPlatform {
dock_menu: None,
on_keyboard_layout_change: None,
menus: None,
+ keyboard_mapper,
}))
}
@@ -348,19 +354,19 @@ impl MacPlatform {
let mut mask = NSEventModifierFlags::empty();
for (modifier, flag) in &[
(
- keystroke.modifiers.platform,
+ keystroke.modifiers().platform,
NSEventModifierFlags::NSCommandKeyMask,
),
(
- keystroke.modifiers.control,
+ keystroke.modifiers().control,
NSEventModifierFlags::NSControlKeyMask,
),
(
- keystroke.modifiers.alt,
+ keystroke.modifiers().alt,
NSEventModifierFlags::NSAlternateKeyMask,
),
(
- keystroke.modifiers.shift,
+ keystroke.modifiers().shift,
NSEventModifierFlags::NSShiftKeyMask,
),
] {
@@ -371,9 +377,9 @@ impl MacPlatform {
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
- ns_string(&name),
+ ns_string(name),
selector,
- ns_string(key_to_native(&keystroke.key).as_ref()),
+ ns_string(key_to_native(keystroke.key()).as_ref()),
)
.autorelease();
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
@@ -383,7 +389,7 @@ impl MacPlatform {
} else {
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
- ns_string(&name),
+ ns_string(name),
selector,
ns_string(""),
)
@@ -392,7 +398,7 @@ impl MacPlatform {
} else {
item = NSMenuItem::alloc(nil)
.initWithTitle_action_keyEquivalent_(
- ns_string(&name),
+ ns_string(name),
selector,
ns_string(""),
)
@@ -412,7 +418,7 @@ impl MacPlatform {
submenu.addItem_(Self::create_menu_item(item, delegate, actions, keymap));
}
item.setSubmenu_(submenu);
- item.setTitle_(ns_string(&name));
+ item.setTitle_(ns_string(name));
item
}
MenuItem::SystemMenu(OsMenu { name, menu_type }) => {
@@ -420,7 +426,7 @@ impl MacPlatform {
let submenu = NSMenu::new(nil).autorelease();
submenu.setDelegate_(delegate);
item.setSubmenu_(submenu);
- item.setTitle_(ns_string(&name));
+ item.setTitle_(ns_string(name));
match menu_type {
SystemMenuType::Services => {
@@ -705,6 +711,7 @@ impl Platform for MacPlatform {
panel.setCanChooseDirectories_(options.directories.to_objc());
panel.setCanChooseFiles_(options.files.to_objc());
panel.setAllowsMultipleSelection_(options.multiple.to_objc());
+
panel.setCanCreateDirectories(true.to_objc());
panel.setResolvesAliases_(false.to_objc());
let done_tx = Cell::new(Some(done_tx));
@@ -714,10 +721,10 @@ impl Platform for MacPlatform {
let urls = panel.URLs();
for i in 0..urls.count() {
let url = urls.objectAtIndex(i);
- if url.isFileURL() == YES {
- if let Ok(path) = ns_url_to_path(url) {
- result.push(path)
- }
+ if url.isFileURL() == YES
+ && let Ok(path) = ns_url_to_path(url)
+ {
+ result.push(path)
}
}
Some(result)
@@ -730,6 +737,11 @@ impl Platform for MacPlatform {
}
});
let block = block.copy();
+
+ if let Some(prompt) = options.prompt {
+ let _: () = msg_send![panel, setPrompt: ns_string(&prompt)];
+ }
+
let _: () = msg_send![panel, beginWithCompletionHandler: block];
}
})
@@ -737,8 +749,13 @@ impl Platform for MacPlatform {
done_rx
}
- fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
+ fn prompt_for_new_path(
+ &self,
+ directory: &Path,
+ suggested_name: Option<&str>,
+ ) -> oneshot::Receiver<Result<Option<PathBuf>>> {
let directory = directory.to_owned();
+ let suggested_name = suggested_name.map(|s| s.to_owned());
let (done_tx, done_rx) = oneshot::channel();
self.foreground_executor()
.spawn(async move {
@@ -748,6 +765,11 @@ impl Platform for MacPlatform {
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
panel.setDirectoryURL(url);
+ if let Some(suggested_name) = suggested_name {
+ let name_string = ns_string(&suggested_name);
+ let _: () = msg_send![panel, setNameFieldStringValue: name_string];
+ }
+
let done_tx = Cell::new(Some(done_tx));
let block = ConcreteBlock::new(move |response: NSModalResponse| {
let mut result = None;
@@ -770,17 +792,18 @@ impl Platform for MacPlatform {
// This is conditional on OS version because I'd like to get rid of it, so that
// you can manually create a file called `a.sql.s`. That said it seems better
// to break that use-case than breaking `a.sql`.
- if chunks.len() == 3 && chunks[1].starts_with(chunks[2]) {
- if Self::os_version() >= SemanticVersion::new(15, 0, 0) {
- let new_filename = OsStr::from_bytes(
- &filename.as_bytes()
- [..chunks[0].len() + 1 + chunks[1].len()],
- )
- .to_owned();
- result.set_file_name(&new_filename);
- }
+ if chunks.len() == 3
+ && chunks[1].starts_with(chunks[2])
+ && Self::os_version() >= SemanticVersion::new(15, 0, 0)
+ {
+ let new_filename = OsStr::from_bytes(
+ &filename.as_bytes()
+ [..chunks[0].len() + 1 + chunks[1].len()],
+ )
+ .to_owned();
+ result.set_file_name(&new_filename);
}
- return result;
+ result
})
}
}
@@ -865,6 +888,10 @@ impl Platform for MacPlatform {
Box::new(MacKeyboardLayout::new())
}
+ fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
+ self.0.lock().keyboard_mapper.clone()
+ }
+
fn app_path(&self) -> Result<PathBuf> {
unsafe {
let bundle: id = NSBundle::mainBundle();
@@ -1376,6 +1403,8 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
let platform = unsafe { get_mac_platform(this) };
let mut lock = platform.0.lock();
+ let keyboard_layout = MacKeyboardLayout::new();
+ lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
drop(lock);
callback();
@@ -16,7 +16,7 @@ use core_foundation::{
use core_graphics::{
base::{CGGlyph, kCGImageAlphaPremultipliedLast},
color_space::CGColorSpace,
- context::CGContext,
+ context::{CGContext, CGTextDrawingMode},
display::CGPoint,
};
use core_text::{
@@ -319,7 +319,7 @@ impl MacTextSystemState {
fn is_emoji(&self, font_id: FontId) -> bool {
self.postscript_names_by_font_id
.get(&font_id)
- .map_or(false, |postscript_name| {
+ .is_some_and(|postscript_name| {
postscript_name == "AppleColorEmoji" || postscript_name == ".AppleColorEmojiUI"
})
}
@@ -396,6 +396,12 @@ impl MacTextSystemState {
let subpixel_shift = params
.subpixel_variant
.map(|v| v as f32 / SUBPIXEL_VARIANTS as f32);
+ cx.set_allows_font_smoothing(true);
+ cx.set_should_smooth_fonts(true);
+ cx.set_text_drawing_mode(CGTextDrawingMode::CGTextFill);
+ cx.set_gray_fill_color(0.0, 1.0);
+ cx.set_allows_antialiasing(true);
+ cx.set_should_antialias(true);
cx.set_allows_font_subpixel_positioning(true);
cx.set_should_subpixel_position_fonts(true);
cx.set_allows_font_subpixel_quantization(false);
@@ -4,8 +4,9 @@ use crate::{
ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay,
PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
- ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
- WindowControlArea, WindowKind, WindowParams, platform::PlatformInputHandler, point, px, size,
+ SharedString, Size, SystemWindowTab, Timer, WindowAppearance, WindowBackgroundAppearance,
+ WindowBounds, WindowControlArea, WindowKind, WindowParams, dispatch_get_main_queue,
+ dispatch_sys::dispatch_async_f, platform::PlatformInputHandler, point, px, size,
};
use block::ConcreteBlock;
use cocoa::{
@@ -24,6 +25,7 @@ use cocoa::{
NSUserDefaults,
},
};
+
use core_graphics::display::{CGDirectDisplayID, CGPoint, CGRect};
use ctor::ctor;
use futures::channel::oneshot;
@@ -82,6 +84,12 @@ type NSDragOperation = NSUInteger;
const NSDragOperationNone: NSDragOperation = 0;
#[allow(non_upper_case_globals)]
const NSDragOperationCopy: NSDragOperation = 1;
+#[derive(PartialEq)]
+pub enum UserTabbingPreference {
+ Never,
+ Always,
+ InFullScreen,
+}
#[link(name = "CoreGraphics", kind = "framework")]
unsafe extern "C" {
@@ -343,6 +351,36 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
conclude_drag_operation as extern "C" fn(&Object, Sel, id),
);
+ decl.add_method(
+ sel!(addTitlebarAccessoryViewController:),
+ add_titlebar_accessory_view_controller as extern "C" fn(&Object, Sel, id),
+ );
+
+ decl.add_method(
+ sel!(moveTabToNewWindow:),
+ move_tab_to_new_window as extern "C" fn(&Object, Sel, id),
+ );
+
+ decl.add_method(
+ sel!(mergeAllWindows:),
+ merge_all_windows as extern "C" fn(&Object, Sel, id),
+ );
+
+ decl.add_method(
+ sel!(selectNextTab:),
+ select_next_tab as extern "C" fn(&Object, Sel, id),
+ );
+
+ decl.add_method(
+ sel!(selectPreviousTab:),
+ select_previous_tab as extern "C" fn(&Object, Sel, id),
+ );
+
+ decl.add_method(
+ sel!(toggleTabBar:),
+ toggle_tab_bar as extern "C" fn(&Object, Sel, id),
+ );
+
decl.register()
}
}
@@ -375,6 +413,11 @@ struct MacWindowState {
// Whether the next left-mouse click is also the focusing click.
first_mouse: bool,
fullscreen_restore_bounds: Bounds<Pixels>,
+ move_tab_to_new_window_callback: Option<Box<dyn FnMut()>>,
+ merge_all_windows_callback: Option<Box<dyn FnMut()>>,
+ select_next_tab_callback: Option<Box<dyn FnMut()>>,
+ select_previous_tab_callback: Option<Box<dyn FnMut()>>,
+ toggle_tab_bar_callback: Option<Box<dyn FnMut()>>,
}
impl MacWindowState {
@@ -530,10 +573,13 @@ impl MacWindow {
titlebar,
kind,
is_movable,
+ is_resizable,
+ is_minimizable,
focus,
show,
display_id,
window_min_size,
+ tabbing_identifier,
}: WindowParams,
executor: ForegroundExecutor,
renderer_context: renderer::Context,
@@ -541,14 +587,25 @@ impl MacWindow {
unsafe {
let pool = NSAutoreleasePool::new(nil);
- let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
+ let allows_automatic_window_tabbing = tabbing_identifier.is_some();
+ if allows_automatic_window_tabbing {
+ let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES];
+ } else {
+ let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
+ }
let mut style_mask;
if let Some(titlebar) = titlebar.as_ref() {
- style_mask = NSWindowStyleMask::NSClosableWindowMask
- | NSWindowStyleMask::NSMiniaturizableWindowMask
- | NSWindowStyleMask::NSResizableWindowMask
- | NSWindowStyleMask::NSTitledWindowMask;
+ style_mask =
+ NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSTitledWindowMask;
+
+ if is_resizable {
+ style_mask |= NSWindowStyleMask::NSResizableWindowMask;
+ }
+
+ if is_minimizable {
+ style_mask |= NSWindowStyleMask::NSMiniaturizableWindowMask;
+ }
if titlebar.appears_transparent {
style_mask |= NSWindowStyleMask::NSFullSizeContentViewWindowMask;
@@ -653,13 +710,18 @@ impl MacWindow {
.and_then(|titlebar| titlebar.traffic_light_position),
transparent_titlebar: titlebar
.as_ref()
- .map_or(true, |titlebar| titlebar.appears_transparent),
+ .is_none_or(|titlebar| titlebar.appears_transparent),
previous_modifiers_changed_event: None,
keystroke_for_do_command: None,
do_command_handled: None,
external_files_dragged: false,
first_mouse: false,
fullscreen_restore_bounds: Bounds::default(),
+ move_tab_to_new_window_callback: None,
+ merge_all_windows_callback: None,
+ select_next_tab_callback: None,
+ select_previous_tab_callback: None,
+ toggle_tab_bar_callback: None,
})));
(*native_window).set_ivar(
@@ -688,7 +750,7 @@ impl MacWindow {
});
}
- if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) {
+ if titlebar.is_none_or(|titlebar| titlebar.appears_transparent) {
native_window.setTitlebarAppearsTransparent_(YES);
native_window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
}
@@ -714,6 +776,13 @@ impl MacWindow {
WindowKind::Normal => {
native_window.setLevel_(NSNormalWindowLevel);
native_window.setAcceptsMouseMovedEvents_(YES);
+
+ if let Some(tabbing_identifier) = tabbing_identifier {
+ let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str());
+ let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id];
+ } else {
+ let _: () = msg_send![native_window, setTabbingIdentifier:nil];
+ }
}
WindowKind::PopUp => {
// Use a tracking area to allow receiving MouseMoved events even when
@@ -742,6 +811,38 @@ impl MacWindow {
}
}
+ let app = NSApplication::sharedApplication(nil);
+ let main_window: id = msg_send![app, mainWindow];
+ if allows_automatic_window_tabbing
+ && !main_window.is_null()
+ && main_window != native_window
+ {
+ let main_window_is_fullscreen = main_window
+ .styleMask()
+ .contains(NSWindowStyleMask::NSFullScreenWindowMask);
+ let user_tabbing_preference = Self::get_user_tabbing_preference()
+ .unwrap_or(UserTabbingPreference::InFullScreen);
+ let should_add_as_tab = user_tabbing_preference == UserTabbingPreference::Always
+ || user_tabbing_preference == UserTabbingPreference::InFullScreen
+ && main_window_is_fullscreen;
+
+ if should_add_as_tab {
+ let main_window_can_tab: BOOL =
+ msg_send![main_window, respondsToSelector: sel!(addTabbedWindow:ordered:)];
+ let main_window_visible: BOOL = msg_send![main_window, isVisible];
+
+ if main_window_can_tab == YES && main_window_visible == YES {
+ let _: () = msg_send![main_window, addTabbedWindow: native_window ordered: NSWindowOrderingMode::NSWindowAbove];
+
+ // Ensure the window is visible immediately after adding the tab, since the tab bar is updated with a new entry at this point.
+ // Note: Calling orderFront here can break fullscreen mode (makes fullscreen windows exit fullscreen), so only do this if the main window is not fullscreen.
+ if !main_window_is_fullscreen {
+ let _: () = msg_send![native_window, orderFront: nil];
+ }
+ }
+ }
+ }
+
if focus && show {
native_window.makeKeyAndOrderFront_(nil);
} else if show {
@@ -796,6 +897,33 @@ impl MacWindow {
window_handles
}
}
+
+ pub fn get_user_tabbing_preference() -> Option<UserTabbingPreference> {
+ unsafe {
+ let defaults: id = NSUserDefaults::standardUserDefaults();
+ let domain = NSString::alloc(nil).init_str("NSGlobalDomain");
+ let key = NSString::alloc(nil).init_str("AppleWindowTabbingMode");
+
+ let dict: id = msg_send![defaults, persistentDomainForName: domain];
+ let value: id = if !dict.is_null() {
+ msg_send![dict, objectForKey: key]
+ } else {
+ nil
+ };
+
+ let value_str = if !value.is_null() {
+ CStr::from_ptr(NSString::UTF8String(value)).to_string_lossy()
+ } else {
+ "".into()
+ };
+
+ match value_str.as_ref() {
+ "manual" => Some(UserTabbingPreference::Never),
+ "always" => Some(UserTabbingPreference::Always),
+ _ => Some(UserTabbingPreference::InFullScreen),
+ }
+ }
+ }
}
impl Drop for MacWindow {
@@ -851,6 +979,65 @@ impl PlatformWindow for MacWindow {
.detach();
}
+ fn merge_all_windows(&self) {
+ let native_window = self.0.lock().native_window;
+ unsafe extern "C" fn merge_windows_async(context: *mut std::ffi::c_void) {
+ let native_window = context as id;
+ let _: () = msg_send![native_window, mergeAllWindows:nil];
+ }
+
+ unsafe {
+ dispatch_async_f(
+ dispatch_get_main_queue(),
+ native_window as *mut std::ffi::c_void,
+ Some(merge_windows_async),
+ );
+ }
+ }
+
+ fn move_tab_to_new_window(&self) {
+ let native_window = self.0.lock().native_window;
+ unsafe extern "C" fn move_tab_async(context: *mut std::ffi::c_void) {
+ let native_window = context as id;
+ let _: () = msg_send![native_window, moveTabToNewWindow:nil];
+ let _: () = msg_send![native_window, makeKeyAndOrderFront: nil];
+ }
+
+ unsafe {
+ dispatch_async_f(
+ dispatch_get_main_queue(),
+ native_window as *mut std::ffi::c_void,
+ Some(move_tab_async),
+ );
+ }
+ }
+
+ fn toggle_window_tab_overview(&self) {
+ let native_window = self.0.lock().native_window;
+ unsafe {
+ let _: () = msg_send![native_window, toggleTabOverview:nil];
+ }
+ }
+
+ fn set_tabbing_identifier(&self, tabbing_identifier: Option<String>) {
+ let native_window = self.0.lock().native_window;
+ unsafe {
+ let allows_automatic_window_tabbing = tabbing_identifier.is_some();
+ if allows_automatic_window_tabbing {
+ let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: YES];
+ } else {
+ let () = msg_send![class!(NSWindow), setAllowsAutomaticWindowTabbing: NO];
+ }
+
+ if let Some(tabbing_identifier) = tabbing_identifier {
+ let tabbing_id = NSString::alloc(nil).init_str(tabbing_identifier.as_str());
+ let _: () = msg_send![native_window, setTabbingIdentifier: tabbing_id];
+ } else {
+ let _: () = msg_send![native_window, setTabbingIdentifier:nil];
+ }
+ }
+ }
+
fn scale_factor(&self) -> f32 {
self.0.as_ref().lock().scale_factor()
}
@@ -1051,6 +1238,17 @@ impl PlatformWindow for MacWindow {
}
}
+ fn get_title(&self) -> String {
+ unsafe {
+ let title: id = msg_send![self.0.lock().native_window, title];
+ if title.is_null() {
+ "".to_string()
+ } else {
+ title.to_str().to_string()
+ }
+ }
+ }
+
fn set_app_id(&mut self, _app_id: &str) {}
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
@@ -1090,7 +1288,7 @@ impl PlatformWindow for MacWindow {
NSView::removeFromSuperview(blur_view);
this.blurred_view = None;
}
- } else if this.blurred_view == None {
+ } else if this.blurred_view.is_none() {
let content_view = this.native_window.contentView();
let frame = NSView::bounds(content_view);
let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc];
@@ -1212,6 +1410,62 @@ impl PlatformWindow for MacWindow {
self.0.lock().appearance_changed_callback = Some(callback);
}
+ fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
+ unsafe {
+ let windows: id = msg_send![self.0.lock().native_window, tabbedWindows];
+ if windows.is_null() {
+ return None;
+ }
+
+ let count: NSUInteger = msg_send![windows, count];
+ let mut result = Vec::new();
+ for i in 0..count {
+ let window: id = msg_send![windows, objectAtIndex:i];
+ if msg_send![window, isKindOfClass: WINDOW_CLASS] {
+ let handle = get_window_state(&*window).lock().handle;
+ let title: id = msg_send![window, title];
+ let title = SharedString::from(title.to_str().to_string());
+
+ result.push(SystemWindowTab::new(title, handle));
+ }
+ }
+
+ Some(result)
+ }
+ }
+
+ fn tab_bar_visible(&self) -> bool {
+ unsafe {
+ let tab_group: id = msg_send![self.0.lock().native_window, tabGroup];
+ if tab_group.is_null() {
+ false
+ } else {
+ let tab_bar_visible: BOOL = msg_send![tab_group, isTabBarVisible];
+ tab_bar_visible == YES
+ }
+ }
+ }
+
+ fn on_move_tab_to_new_window(&self, callback: Box<dyn FnMut()>) {
+ self.0.as_ref().lock().move_tab_to_new_window_callback = Some(callback);
+ }
+
+ fn on_merge_all_windows(&self, callback: Box<dyn FnMut()>) {
+ self.0.as_ref().lock().merge_all_windows_callback = Some(callback);
+ }
+
+ fn on_select_next_tab(&self, callback: Box<dyn FnMut()>) {
+ self.0.as_ref().lock().select_next_tab_callback = Some(callback);
+ }
+
+ fn on_select_previous_tab(&self, callback: Box<dyn FnMut()>) {
+ self.0.as_ref().lock().select_previous_tab_callback = Some(callback);
+ }
+
+ fn on_toggle_tab_bar(&self, callback: Box<dyn FnMut()>) {
+ self.0.as_ref().lock().toggle_tab_bar_callback = Some(callback);
+ }
+
fn draw(&self, scene: &crate::Scene) {
let mut this = self.0.lock();
this.renderer.draw(scene);
@@ -1225,7 +1479,7 @@ impl PlatformWindow for MacWindow {
None
}
- fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {
+ fn update_ime_position(&self, _bounds: Bounds<Pixels>) {
let executor = self.0.lock().executor.clone();
executor
.spawn(async move {
@@ -1478,18 +1732,18 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
return YES;
}
- if key_down_event.is_held {
- if let Some(key_char) = key_down_event.keystroke.key_char.as_ref() {
- let handled = with_input_handler(&this, |input_handler| {
- if !input_handler.apple_press_and_hold_enabled() {
- input_handler.replace_text_in_range(None, &key_char);
- return YES;
- }
- NO
- });
- if handled == Some(YES) {
+ if key_down_event.is_held
+ && let Some(key_char) = key_down_event.keystroke.key_char.as_ref()
+ {
+ let handled = with_input_handler(this, |input_handler| {
+ if !input_handler.apple_press_and_hold_enabled() {
+ input_handler.replace_text_in_range(None, key_char);
return YES;
}
+ NO
+ });
+ if handled == Some(YES) {
+ return YES;
}
}
@@ -1624,10 +1878,10 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
modifiers: prev_modifiers,
capslock: prev_capslock,
})) = &lock.previous_modifiers_changed_event
+ && prev_modifiers == modifiers
+ && prev_capslock == capslock
{
- if prev_modifiers == modifiers && prev_capslock == capslock {
- return;
- }
+ return;
}
lock.previous_modifiers_changed_event = Some(event.clone());
@@ -1653,6 +1907,7 @@ extern "C" fn window_did_change_occlusion_state(this: &Object, _: Sel, _: id) {
.occlusionState()
.contains(NSWindowOcclusionState::NSWindowOcclusionStateVisible)
{
+ lock.move_traffic_light();
lock.start_display_link();
} else {
lock.stop_display_link();
@@ -1714,7 +1969,7 @@ extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) {
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
- let lock = window_state.lock();
+ let mut lock = window_state.lock();
let is_active = unsafe { lock.native_window.isKeyWindow() == YES };
// When opening a pop-up while the application isn't active, Cocoa sends a spurious
@@ -1735,9 +1990,34 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id)
let executor = lock.executor.clone();
drop(lock);
+
+ // If window is becoming active, trigger immediate synchronous frame request.
+ if selector == sel!(windowDidBecomeKey:) && is_active {
+ let window_state = unsafe { get_window_state(this) };
+ let mut lock = window_state.lock();
+
+ if let Some(mut callback) = lock.request_frame_callback.take() {
+ #[cfg(not(feature = "macos-blade"))]
+ lock.renderer.set_presents_with_transaction(true);
+ lock.stop_display_link();
+ drop(lock);
+ callback(Default::default());
+
+ let mut lock = window_state.lock();
+ lock.request_frame_callback = Some(callback);
+ #[cfg(not(feature = "macos-blade"))]
+ lock.renderer.set_presents_with_transaction(false);
+ lock.start_display_link();
+ }
+ }
+
executor
.spawn(async move {
let mut lock = window_state.as_ref().lock();
+ if is_active {
+ lock.move_traffic_light();
+ }
+
if let Some(mut callback) = lock.activate_callback.take() {
drop(lock);
callback(is_active);
@@ -1949,7 +2229,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
let text = text.to_str();
let replacement_range = replacement_range.to_range();
with_input_handler(this, |input_handler| {
- input_handler.replace_text_in_range(replacement_range, &text)
+ input_handler.replace_text_in_range(replacement_range, text)
});
}
}
@@ -1973,7 +2253,7 @@ extern "C" fn set_marked_text(
let replacement_range = replacement_range.to_range();
let text = text.to_str();
with_input_handler(this, |input_handler| {
- input_handler.replace_and_mark_text_in_range(replacement_range, &text, selected_range)
+ input_handler.replace_and_mark_text_in_range(replacement_range, text, selected_range)
});
}
}
@@ -1995,10 +2275,10 @@ extern "C" fn attributed_substring_for_proposed_range(
let mut adjusted: Option<Range<usize>> = None;
let selected_text = input_handler.text_for_range(range.clone(), &mut adjusted)?;
- if let Some(adjusted) = adjusted {
- if adjusted != range {
- unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) };
- }
+ if let Some(adjusted) = adjusted
+ && adjusted != range
+ {
+ unsafe { (actual_range as *mut NSRange).write(NSRange::from(adjusted)) };
}
unsafe {
let string: id = msg_send![class!(NSAttributedString), alloc];
@@ -2063,8 +2343,8 @@ fn screen_point_to_gpui_point(this: &Object, position: NSPoint) -> Point<Pixels>
let frame = get_frame(this);
let window_x = position.x - frame.origin.x;
let window_y = frame.size.height - (position.y - frame.origin.y);
- let position = point(px(window_x as f32), px(window_y as f32));
- position
+
+ point(px(window_x as f32), px(window_y as f32))
}
extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDragOperation {
@@ -2073,11 +2353,10 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr
let paths = external_paths_from_event(dragging_info);
if let Some(event) =
paths.map(|paths| PlatformInput::FileDrop(FileDropEvent::Entered { position, paths }))
+ && send_new_event(&window_state, event)
{
- if send_new_event(&window_state, event) {
- window_state.lock().external_files_dragged = true;
- return NSDragOperationCopy;
- }
+ window_state.lock().external_files_dragged = true;
+ return NSDragOperationCopy;
}
NSDragOperationNone
}
@@ -2274,3 +2553,80 @@ unsafe fn remove_layer_background(layer: id) {
}
}
}
+
+extern "C" fn add_titlebar_accessory_view_controller(this: &Object, _: Sel, view_controller: id) {
+ unsafe {
+ let _: () = msg_send![super(this, class!(NSWindow)), addTitlebarAccessoryViewController: view_controller];
+
+ // Hide the native tab bar and set its height to 0, since we render our own.
+ let accessory_view: id = msg_send![view_controller, view];
+ let _: () = msg_send![accessory_view, setHidden: YES];
+ let mut frame: NSRect = msg_send![accessory_view, frame];
+ frame.size.height = 0.0;
+ let _: () = msg_send![accessory_view, setFrame: frame];
+ }
+}
+
+extern "C" fn move_tab_to_new_window(this: &Object, _: Sel, _: id) {
+ unsafe {
+ let _: () = msg_send![super(this, class!(NSWindow)), moveTabToNewWindow:nil];
+
+ let window_state = get_window_state(this);
+ let mut lock = window_state.as_ref().lock();
+ if let Some(mut callback) = lock.move_tab_to_new_window_callback.take() {
+ drop(lock);
+ callback();
+ window_state.lock().move_tab_to_new_window_callback = Some(callback);
+ }
+ }
+}
+
+extern "C" fn merge_all_windows(this: &Object, _: Sel, _: id) {
+ unsafe {
+ let _: () = msg_send![super(this, class!(NSWindow)), mergeAllWindows:nil];
+
+ let window_state = get_window_state(this);
+ let mut lock = window_state.as_ref().lock();
+ if let Some(mut callback) = lock.merge_all_windows_callback.take() {
+ drop(lock);
+ callback();
+ window_state.lock().merge_all_windows_callback = Some(callback);
+ }
+ }
+}
+
+extern "C" fn select_next_tab(this: &Object, _sel: Sel, _id: id) {
+ let window_state = unsafe { get_window_state(this) };
+ let mut lock = window_state.as_ref().lock();
+ if let Some(mut callback) = lock.select_next_tab_callback.take() {
+ drop(lock);
+ callback();
+ window_state.lock().select_next_tab_callback = Some(callback);
+ }
+}
+
+extern "C" fn select_previous_tab(this: &Object, _sel: Sel, _id: id) {
+ let window_state = unsafe { get_window_state(this) };
+ let mut lock = window_state.as_ref().lock();
+ if let Some(mut callback) = lock.select_previous_tab_callback.take() {
+ drop(lock);
+ callback();
+ window_state.lock().select_previous_tab_callback = Some(callback);
+ }
+}
+
+extern "C" fn toggle_tab_bar(this: &Object, _sel: Sel, _id: id) {
+ unsafe {
+ let _: () = msg_send![super(this, class!(NSWindow)), toggleTabBar:nil];
+
+ let window_state = get_window_state(this);
+ let mut lock = window_state.as_ref().lock();
+ lock.move_traffic_light();
+
+ if let Some(mut callback) = lock.toggle_tab_bar_callback.take() {
+ drop(lock);
+ callback();
+ window_state.lock().toggle_tab_bar_callback = Some(callback);
+ }
+ }
+}
@@ -228,7 +228,7 @@ fn run_capture(
display,
size,
}));
- if let Err(_) = stream_send_result {
+ if stream_send_result.is_err() {
return;
}
while !cancel_stream.load(std::sync::atomic::Ordering::SeqCst) {
@@ -78,11 +78,11 @@ impl TestDispatcher {
let state = self.state.lock();
let next_due_time = state.delayed.first().map(|(time, _)| *time);
drop(state);
- if let Some(due_time) = next_due_time {
- if due_time <= new_now {
- self.state.lock().time = due_time;
- continue;
- }
+ if let Some(due_time) = next_due_time
+ && due_time <= new_now
+ {
+ self.state.lock().time = due_time;
+ continue;
}
break;
}
@@ -118,7 +118,7 @@ impl TestDispatcher {
}
YieldNow {
- count: self.state.lock().random.gen_range(0..10),
+ count: self.state.lock().random.random_range(0..10),
}
}
@@ -151,11 +151,11 @@ impl TestDispatcher {
if deprioritized_background_len == 0 {
return false;
}
- let ix = state.random.gen_range(0..deprioritized_background_len);
+ let ix = state.random.random_range(0..deprioritized_background_len);
main_thread = false;
runnable = state.deprioritized_background.swap_remove(ix);
} else {
- main_thread = state.random.gen_ratio(
+ main_thread = state.random.random_ratio(
foreground_len as u32,
(foreground_len + background_len) as u32,
);
@@ -170,7 +170,7 @@ impl TestDispatcher {
.pop_front()
.unwrap();
} else {
- let ix = state.random.gen_range(0..background_len);
+ let ix = state.random.random_range(0..background_len);
runnable = state.background.swap_remove(ix);
};
};
@@ -241,7 +241,7 @@ impl TestDispatcher {
pub fn gen_block_on_ticks(&self) -> usize {
let mut lock = self.state.lock();
let block_on_ticks = lock.block_on_ticks.clone();
- lock.random.gen_range(block_on_ticks)
+ lock.random.random_range(block_on_ticks)
}
}
@@ -270,9 +270,7 @@ impl PlatformDispatcher for TestDispatcher {
fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
{
let mut state = self.state.lock();
- if label.map_or(false, |label| {
- state.deprioritized_task_labels.contains(&label)
- }) {
+ if label.is_some_and(|label| state.deprioritized_task_labels.contains(&label)) {
state.deprioritized_background.push(runnable);
} else {
state.background.push(runnable);
@@ -1,8 +1,9 @@
use crate::{
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
- ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
- PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
- SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
+ DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
+ PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
+ ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
+ TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
};
use anyhow::Result;
use collections::VecDeque;
@@ -187,24 +188,24 @@ impl TestPlatform {
.push_back(TestPrompt {
msg: msg.to_string(),
detail: detail.map(|s| s.to_string()),
- answers: answers.clone(),
+ answers,
tx,
});
rx
}
pub(crate) fn set_active_window(&self, window: Option<TestWindow>) {
- let executor = self.foreground_executor().clone();
+ let executor = self.foreground_executor();
let previous_window = self.active_window.borrow_mut().take();
self.active_window.borrow_mut().clone_from(&window);
executor
.spawn(async move {
if let Some(previous_window) = previous_window {
- if let Some(window) = window.as_ref() {
- if Rc::ptr_eq(&previous_window.0, &window.0) {
- return;
- }
+ if let Some(window) = window.as_ref()
+ && Rc::ptr_eq(&previous_window.0, &window.0)
+ {
+ return;
}
previous_window.simulate_active_status_change(false);
}
@@ -237,6 +238,10 @@ impl Platform for TestPlatform {
Box::new(TestKeyboardLayout)
}
+ fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
+ Rc::new(DummyKeyboardMapper)
+ }
+
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
@@ -336,6 +341,7 @@ impl Platform for TestPlatform {
fn prompt_for_new_path(
&self,
directory: &std::path::Path,
+ _suggested_name: Option<&str>,
) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
let (tx, rx) = oneshot::channel();
self.background_executor()
@@ -1,8 +1,8 @@
use crate::{
AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult, GpuSpecs,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
- Point, PromptButton, RequestFrameOptions, ScaledPixels, Size, TestPlatform, TileId,
- WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams,
+ Point, PromptButton, RequestFrameOptions, Size, TestPlatform, TileId, WindowAppearance,
+ WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams,
};
use collections::HashMap;
use parking_lot::Mutex;
@@ -289,7 +289,7 @@ impl PlatformWindow for TestWindow {
unimplemented!()
}
- fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {}
+ fn update_ime_position(&self, _bounds: Bounds<Pixels>) {}
fn gpu_specs(&self) -> Option<GpuSpecs> {
None
@@ -2,6 +2,7 @@ mod clipboard;
mod destination_list;
mod direct_write;
mod directx_atlas;
+mod directx_devices;
mod directx_renderer;
mod dispatcher;
mod display;
@@ -18,6 +19,7 @@ pub(crate) use clipboard::*;
pub(crate) use destination_list::*;
pub(crate) use direct_write::*;
pub(crate) use directx_atlas::*;
+pub(crate) use directx_devices::*;
pub(crate) use directx_renderer::*;
pub(crate) use dispatcher::*;
pub(crate) use display::*;
@@ -0,0 +1,28 @@
+float color_brightness(float3 color) {
+ // REC. 601 luminance coefficients for perceived brightness
+ return dot(color, float3(0.30f, 0.59f, 0.11f));
+}
+
+float light_on_dark_contrast(float enhancedContrast, float3 color) {
+ float brightness = color_brightness(color);
+ float multiplier = saturate(4.0f * (0.75f - brightness));
+ return enhancedContrast * multiplier;
+}
+
+float enhance_contrast(float alpha, float k) {
+ return alpha * (k + 1.0f) / (alpha * k + 1.0f);
+}
+
+float apply_alpha_correction(float a, float b, float4 g) {
+ float brightness_adjustment = g.x * b + g.y;
+ float correction = brightness_adjustment * a + (g.z * b + g.w);
+ return a + a * (1.0f - a) * correction;
+}
+
+float apply_contrast_and_gamma_correction(float sample, float3 color, float enhanced_contrast_factor, float4 gamma_ratios) {
+ float enhanced_contrast = light_on_dark_contrast(enhanced_contrast_factor, color);
+ float brightness = color_brightness(color);
+
+ float contrasted = enhance_contrast(sample, enhanced_contrast);
+ return apply_alpha_correction(contrasted, brightness, gamma_ratios);
+}
@@ -1,3 +1,5 @@
+#include "alpha_correction.hlsl"
+
struct RasterVertexOutput {
float4 position : SV_Position;
float2 texcoord : TEXCOORD0;
@@ -23,17 +25,20 @@ struct Bounds {
int2 size;
};
-Texture2D<float4> t_layer : register(t0);
+Texture2D<float> t_layer : register(t0);
SamplerState s_layer : register(s0);
cbuffer GlyphLayerTextureParams : register(b0) {
Bounds bounds;
float4 run_color;
+ float4 gamma_ratios;
+ float grayscale_enhanced_contrast;
+ float3 _pad;
};
float4 emoji_rasterization_fragment(PixelInput input): SV_Target {
- float3 sampled = t_layer.Sample(s_layer, input.texcoord.xy).rgb;
- float alpha = (sampled.r + sampled.g + sampled.b) / 3;
-
- return float4(run_color.rgb, alpha);
+ float sample = t_layer.Sample(s_layer, input.texcoord.xy).r;
+ float alpha_corrected = apply_contrast_and_gamma_correction(sample, run_color.rgb, grayscale_enhanced_contrast, gamma_ratios);
+ float alpha = alpha_corrected * run_color.a;
+ return float4(run_color.rgb * alpha, alpha);
}
@@ -1,7 +1,7 @@
use std::{borrow::Cow, sync::Arc};
use ::util::ResultExt;
-use anyhow::Result;
+use anyhow::{Context, Result};
use collections::HashMap;
use itertools::Itertools;
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
@@ -10,12 +10,8 @@ use windows::{
Foundation::*,
Globalization::GetUserDefaultLocaleName,
Graphics::{
- Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP,
- Direct3D11::*,
- DirectWrite::*,
- Dxgi::Common::*,
- Gdi::{IsRectEmpty, LOGFONTW},
- Imaging::*,
+ Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP, Direct3D11::*, DirectWrite::*,
+ Dxgi::Common::*, Gdi::LOGFONTW,
},
System::SystemServices::LOCALE_NAME_MAX_LENGTH,
UI::WindowsAndMessaging::*,
@@ -40,12 +36,10 @@ pub(crate) struct DirectWriteTextSystem(RwLock<DirectWriteState>);
struct DirectWriteComponent {
locale: String,
factory: IDWriteFactory5,
- bitmap_factory: AgileReference<IWICImagingFactory>,
in_memory_loader: IDWriteInMemoryFontFileLoader,
builder: IDWriteFontSetBuilder1,
text_renderer: Arc<TextRendererWrapper>,
- render_params: IDWriteRenderingParams3,
gpu_state: GPUState,
}
@@ -76,11 +70,10 @@ struct FontIdentifier {
}
impl DirectWriteComponent {
- pub fn new(bitmap_factory: &IWICImagingFactory, gpu_context: &DirectXDevices) -> Result<Self> {
+ pub fn new(directx_devices: &DirectXDevices) -> Result<Self> {
// todo: ideally this would not be a large unsafe block but smaller isolated ones for easier auditing
unsafe {
let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?;
- let bitmap_factory = AgileReference::new(bitmap_factory)?;
// The `IDWriteInMemoryFontFileLoader` here is supported starting from
// Windows 10 Creators Update, which consequently requires the entire
// `DirectWriteTextSystem` to run on `win10 1703`+.
@@ -92,36 +85,14 @@ impl DirectWriteComponent {
let locale = String::from_utf16_lossy(&locale_vec);
let text_renderer = Arc::new(TextRendererWrapper::new(&locale));
- let render_params = {
- let default_params: IDWriteRenderingParams3 =
- factory.CreateRenderingParams()?.cast()?;
- let gamma = default_params.GetGamma();
- let enhanced_contrast = default_params.GetEnhancedContrast();
- let gray_contrast = default_params.GetGrayscaleEnhancedContrast();
- let cleartype_level = default_params.GetClearTypeLevel();
- let grid_fit_mode = default_params.GetGridFitMode();
-
- factory.CreateCustomRenderingParams(
- gamma,
- enhanced_contrast,
- gray_contrast,
- cleartype_level,
- DWRITE_PIXEL_GEOMETRY_RGB,
- DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
- grid_fit_mode,
- )?
- };
-
- let gpu_state = GPUState::new(gpu_context)?;
+ let gpu_state = GPUState::new(directx_devices)?;
Ok(DirectWriteComponent {
locale,
factory,
- bitmap_factory,
in_memory_loader,
builder,
text_renderer,
- render_params,
gpu_state,
})
}
@@ -129,9 +100,9 @@ impl DirectWriteComponent {
}
impl GPUState {
- fn new(gpu_context: &DirectXDevices) -> Result<Self> {
- let device = gpu_context.device.clone();
- let device_context = gpu_context.device_context.clone();
+ fn new(directx_devices: &DirectXDevices) -> Result<Self> {
+ let device = directx_devices.device.clone();
+ let device_context = directx_devices.device_context.clone();
let blend_state = {
let mut blend_state = None;
@@ -141,10 +112,10 @@ impl GPUState {
RenderTarget: [
D3D11_RENDER_TARGET_BLEND_DESC {
BlendEnable: true.into(),
- SrcBlend: D3D11_BLEND_SRC_ALPHA,
+ SrcBlend: D3D11_BLEND_ONE,
DestBlend: D3D11_BLEND_INV_SRC_ALPHA,
BlendOp: D3D11_BLEND_OP_ADD,
- SrcBlendAlpha: D3D11_BLEND_SRC_ALPHA,
+ SrcBlendAlpha: D3D11_BLEND_ONE,
DestBlendAlpha: D3D11_BLEND_INV_SRC_ALPHA,
BlendOpAlpha: D3D11_BLEND_OP_ADD,
RenderTargetWriteMask: D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8,
@@ -212,11 +183,8 @@ impl GPUState {
}
impl DirectWriteTextSystem {
- pub(crate) fn new(
- gpu_context: &DirectXDevices,
- bitmap_factory: &IWICImagingFactory,
- ) -> Result<Self> {
- let components = DirectWriteComponent::new(bitmap_factory, gpu_context)?;
+ pub(crate) fn new(directx_devices: &DirectXDevices) -> Result<Self> {
+ let components = DirectWriteComponent::new(directx_devices)?;
let system_font_collection = unsafe {
let mut result = std::mem::zeroed();
components
@@ -242,6 +210,10 @@ impl DirectWriteTextSystem {
font_id_by_identifier: HashMap::default(),
})))
}
+
+ pub(crate) fn handle_gpu_lost(&self, directx_devices: &DirectXDevices) {
+ self.0.write().handle_gpu_lost(directx_devices);
+ }
}
impl PlatformTextSystem for DirectWriteTextSystem {
@@ -762,18 +734,22 @@ impl DirectWriteState {
unsafe {
font.font_face.GetRecommendedRenderingMode(
params.font_size.0,
- // The dpi here seems that it has the same effect with `Some(&transform)`
- 1.0,
- 1.0,
+ // Using 96 as scale is applied by the transform
+ 96.0,
+ 96.0,
Some(&transform),
false,
DWRITE_OUTLINE_THRESHOLD_ANTIALIASED,
DWRITE_MEASURING_MODE_NATURAL,
- &self.components.render_params,
+ None,
&mut rendering_mode,
&mut grid_fit_mode,
)?;
}
+ let rendering_mode = match rendering_mode {
+ DWRITE_RENDERING_MODE1_OUTLINE => DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
+ m => m,
+ };
let glyph_analysis = unsafe {
self.components.factory.CreateGlyphRunAnalysis(
@@ -782,8 +758,7 @@ impl DirectWriteState {
rendering_mode,
DWRITE_MEASURING_MODE_NATURAL,
grid_fit_mode,
- // We're using cleartype not grayscale for monochrome is because it provides better quality
- DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE,
+ DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE,
baseline_origin_x,
baseline_origin_y,
)
@@ -794,10 +769,14 @@ impl DirectWriteState {
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let glyph_analysis = self.create_glyph_run_analysis(params)?;
- let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1)? };
- // Some glyphs cannot be drawn with ClearType, such as bitmap fonts. In that case
- // GetAlphaTextureBounds() supposedly returns an empty RECT, but I haven't tested that yet.
- if !unsafe { IsRectEmpty(&bounds) }.as_bool() {
+ let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1)? };
+
+ if bounds.right < bounds.left {
+ Ok(Bounds {
+ origin: point(0.into(), 0.into()),
+ size: size(0.into(), 0.into()),
+ })
+ } else {
Ok(Bounds {
origin: point(bounds.left.into(), bounds.top.into()),
size: size(
@@ -805,25 +784,6 @@ impl DirectWriteState {
(bounds.bottom - bounds.top).into(),
),
})
- } else {
- // If it's empty, retry with grayscale AA.
- let bounds =
- unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1)? };
-
- if bounds.right < bounds.left {
- Ok(Bounds {
- origin: point(0.into(), 0.into()),
- size: size(0.into(), 0.into()),
- })
- } else {
- Ok(Bounds {
- origin: point(bounds.left.into(), bounds.top.into()),
- size: size(
- (bounds.right - bounds.left).into(),
- (bounds.bottom - bounds.top).into(),
- ),
- })
- }
}
}
@@ -850,7 +810,7 @@ impl DirectWriteState {
}
let bitmap_data = if params.is_emoji {
- if let Ok(color) = self.rasterize_color(¶ms, glyph_bounds) {
+ if let Ok(color) = self.rasterize_color(params, glyph_bounds) {
color
} else {
let monochrome = self.rasterize_monochrome(params, glyph_bounds)?;
@@ -872,13 +832,12 @@ impl DirectWriteState {
glyph_bounds: Bounds<DevicePixels>,
) -> Result<Vec<u8>> {
let mut bitmap_data =
- vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize * 3];
+ vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize];
let glyph_analysis = self.create_glyph_run_analysis(params)?;
unsafe {
glyph_analysis.CreateAlphaTexture(
- // We're using cleartype not grayscale for monochrome is because it provides better quality
- DWRITE_TEXTURE_CLEARTYPE_3x1,
+ DWRITE_TEXTURE_ALIASED_1x1,
&RECT {
left: glyph_bounds.origin.x.0,
top: glyph_bounds.origin.y.0,
@@ -889,30 +848,6 @@ impl DirectWriteState {
)?;
}
- let bitmap_factory = self.components.bitmap_factory.resolve()?;
- let bitmap = unsafe {
- bitmap_factory.CreateBitmapFromMemory(
- glyph_bounds.size.width.0 as u32,
- glyph_bounds.size.height.0 as u32,
- &GUID_WICPixelFormat24bppRGB,
- glyph_bounds.size.width.0 as u32 * 3,
- &bitmap_data,
- )
- }?;
-
- let grayscale_bitmap =
- unsafe { WICConvertBitmapSource(&GUID_WICPixelFormat8bppGray, &bitmap) }?;
-
- let mut bitmap_data =
- vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize];
- unsafe {
- grayscale_bitmap.CopyPixels(
- std::ptr::null() as _,
- glyph_bounds.size.width.0 as u32,
- &mut bitmap_data,
- )
- }?;
-
Ok(bitmap_data)
}
@@ -981,25 +916,24 @@ impl DirectWriteState {
DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
DWRITE_MEASURING_MODE_NATURAL,
DWRITE_GRID_FIT_MODE_DEFAULT,
- DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE,
+ DWRITE_TEXT_ANTIALIAS_MODE_GRAYSCALE,
baseline_origin_x,
baseline_origin_y,
)
}?;
let color_bounds =
- unsafe { color_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1) }?;
+ unsafe { color_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1) }?;
let color_size = size(
color_bounds.right - color_bounds.left,
color_bounds.bottom - color_bounds.top,
);
if color_size.width > 0 && color_size.height > 0 {
- let mut alpha_data =
- vec![0u8; (color_size.width * color_size.height * 3) as usize];
+ let mut alpha_data = vec![0u8; (color_size.width * color_size.height) as usize];
unsafe {
color_analysis.CreateAlphaTexture(
- DWRITE_TEXTURE_CLEARTYPE_3x1,
+ DWRITE_TEXTURE_ALIASED_1x1,
&color_bounds,
&mut alpha_data,
)
@@ -1015,10 +949,6 @@ impl DirectWriteState {
}
};
let bounds = bounds(point(color_bounds.left, color_bounds.top), color_size);
- let alpha_data = alpha_data
- .chunks_exact(3)
- .flat_map(|chunk| [chunk[0], chunk[1], chunk[2], 255])
- .collect::<Vec<_>>();
glyph_layers.push(GlyphLayerTexture::new(
&self.components.gpu_state,
run_color,
@@ -1135,10 +1065,18 @@ impl DirectWriteState {
unsafe { device_context.PSSetSamplers(0, Some(&gpu_state.sampler)) };
unsafe { device_context.OMSetBlendState(&gpu_state.blend_state, None, 0xffffffff) };
+ let crate::FontInfo {
+ gamma_ratios,
+ grayscale_enhanced_contrast,
+ } = DirectXRenderer::get_font_info();
+
for layer in glyph_layers {
let params = GlyphLayerTextureParams {
run_color: layer.run_color,
bounds: layer.bounds,
+ gamma_ratios: *gamma_ratios,
+ grayscale_enhanced_contrast: *grayscale_enhanced_contrast,
+ _pad: [0f32; 3],
};
unsafe {
let mut dest = std::mem::zeroed();
@@ -1202,6 +1140,20 @@ impl DirectWriteState {
};
}
+ // Convert from premultiplied to straight alpha
+ for chunk in rasterized.chunks_exact_mut(4) {
+ let b = chunk[0] as f32;
+ let g = chunk[1] as f32;
+ let r = chunk[2] as f32;
+ let a = chunk[3] as f32;
+ if a > 0.0 {
+ let inv_a = 255.0 / a;
+ chunk[0] = (b * inv_a).clamp(0.0, 255.0) as u8;
+ chunk[1] = (g * inv_a).clamp(0.0, 255.0) as u8;
+ chunk[2] = (r * inv_a).clamp(0.0, 255.0) as u8;
+ }
+ }
+
Ok(rasterized)
}
@@ -1263,6 +1215,20 @@ impl DirectWriteState {
));
result
}
+
+ fn handle_gpu_lost(&mut self, directx_devices: &DirectXDevices) {
+ try_to_recover_from_device_lost(
+ || GPUState::new(directx_devices).context("Recreating GPU state for DirectWrite"),
+ |gpu_state| self.components.gpu_state = gpu_state,
+ || {
+ log::error!(
+ "Failed to recreate GPU state for DirectWrite after multiple attempts."
+ );
+ // Do something here?
+ // At this point, the device loss is considered unrecoverable.
+ },
+ );
+ }
}
impl Drop for DirectWriteState {
@@ -1298,14 +1264,14 @@ impl GlyphLayerTexture {
Height: texture_size.height as u32,
MipLevels: 1,
ArraySize: 1,
- Format: DXGI_FORMAT_R8G8B8A8_UNORM,
+ Format: DXGI_FORMAT_R8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
- CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
+ CPUAccessFlags: 0,
MiscFlags: 0,
};
@@ -1334,7 +1300,7 @@ impl GlyphLayerTexture {
0,
None,
alpha_data.as_ptr() as _,
- (texture_size.width * 4) as u32,
+ texture_size.width as u32,
0,
)
};
@@ -1352,6 +1318,9 @@ impl GlyphLayerTexture {
struct GlyphLayerTextureParams {
bounds: Bounds<i32>,
run_color: Rgba,
+ gamma_ratios: [f32; 4],
+ grayscale_enhanced_contrast: f32,
+ _pad: [f32; 3],
}
struct TextRendererWrapper(pub IDWriteTextRenderer);
@@ -1784,7 +1753,7 @@ fn apply_font_features(
}
unsafe {
- direct_write_features.AddFontFeature(make_direct_write_feature(&tag, *value))?;
+ direct_write_features.AddFontFeature(make_direct_write_feature(tag, *value))?;
}
}
unsafe {
@@ -3,9 +3,8 @@ use etagere::BucketedAtlasAllocator;
use parking_lot::Mutex;
use windows::Win32::Graphics::{
Direct3D11::{
- D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_CPU_ACCESS_WRITE, D3D11_TEXTURE2D_DESC,
- D3D11_USAGE_DEFAULT, ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView,
- ID3D11Texture2D,
+ D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT,
+ ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView, ID3D11Texture2D,
},
Dxgi::Common::*,
};
@@ -189,7 +188,7 @@ impl DirectXAtlasState {
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: bind_flag.0 as u32,
- CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
+ CPUAccessFlags: 0,
MiscFlags: 0,
};
let mut texture: Option<ID3D11Texture2D> = None;
@@ -0,0 +1,199 @@
+use anyhow::{Context, Result};
+use util::ResultExt;
+use windows::Win32::{
+ Foundation::HMODULE,
+ Graphics::{
+ Direct3D::{
+ D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL, D3D_FEATURE_LEVEL_10_1,
+ D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1,
+ },
+ Direct3D11::{
+ D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_DEBUG,
+ D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS,
+ D3D11_SDK_VERSION, D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext,
+ },
+ Dxgi::{
+ CreateDXGIFactory2, DXGI_CREATE_FACTORY_DEBUG, DXGI_CREATE_FACTORY_FLAGS,
+ DXGI_GPU_PREFERENCE_MINIMUM_POWER, IDXGIAdapter1, IDXGIFactory6,
+ },
+ },
+};
+
+pub(crate) fn try_to_recover_from_device_lost<T>(
+ mut f: impl FnMut() -> Result<T>,
+ on_success: impl FnOnce(T),
+ on_error: impl FnOnce(),
+) {
+ let result = (0..5).find_map(|i| {
+ if i > 0 {
+ // Add a small delay before retrying
+ std::thread::sleep(std::time::Duration::from_millis(100));
+ }
+ f().log_err()
+ });
+
+ if let Some(result) = result {
+ on_success(result);
+ } else {
+ on_error();
+ }
+}
+
+#[derive(Clone)]
+pub(crate) struct DirectXDevices {
+ pub(crate) adapter: IDXGIAdapter1,
+ pub(crate) dxgi_factory: IDXGIFactory6,
+ pub(crate) device: ID3D11Device,
+ pub(crate) device_context: ID3D11DeviceContext,
+}
+
+impl DirectXDevices {
+ pub(crate) fn new() -> Result<Self> {
+ let debug_layer_available = check_debug_layer_available();
+ let dxgi_factory =
+ get_dxgi_factory(debug_layer_available).context("Creating DXGI factory")?;
+ let adapter =
+ get_adapter(&dxgi_factory, debug_layer_available).context("Getting DXGI adapter")?;
+ let (device, device_context) = {
+ let mut context: Option<ID3D11DeviceContext> = None;
+ let mut feature_level = D3D_FEATURE_LEVEL::default();
+ let device = get_device(
+ &adapter,
+ Some(&mut context),
+ Some(&mut feature_level),
+ debug_layer_available,
+ )
+ .context("Creating Direct3D device")?;
+ match feature_level {
+ D3D_FEATURE_LEVEL_11_1 => {
+ log::info!("Created device with Direct3D 11.1 feature level.")
+ }
+ D3D_FEATURE_LEVEL_11_0 => {
+ log::info!("Created device with Direct3D 11.0 feature level.")
+ }
+ D3D_FEATURE_LEVEL_10_1 => {
+ log::info!("Created device with Direct3D 10.1 feature level.")
+ }
+ _ => unreachable!(),
+ }
+ (device, context.unwrap())
+ };
+
+ Ok(Self {
+ adapter,
+ dxgi_factory,
+ device,
+ device_context,
+ })
+ }
+}
+
+#[inline]
+fn check_debug_layer_available() -> bool {
+ #[cfg(debug_assertions)]
+ {
+ use windows::Win32::Graphics::Dxgi::{DXGIGetDebugInterface1, IDXGIInfoQueue};
+
+ unsafe { DXGIGetDebugInterface1::<IDXGIInfoQueue>(0) }
+ .log_err()
+ .is_some()
+ }
+ #[cfg(not(debug_assertions))]
+ {
+ false
+ }
+}
+
+#[inline]
+fn get_dxgi_factory(debug_layer_available: bool) -> Result<IDXGIFactory6> {
+ let factory_flag = if debug_layer_available {
+ DXGI_CREATE_FACTORY_DEBUG
+ } else {
+ #[cfg(debug_assertions)]
+ log::warn!(
+ "Failed to get DXGI debug interface. DirectX debugging features will be disabled."
+ );
+ DXGI_CREATE_FACTORY_FLAGS::default()
+ };
+ unsafe { Ok(CreateDXGIFactory2(factory_flag)?) }
+}
+
+#[inline]
+fn get_adapter(dxgi_factory: &IDXGIFactory6, debug_layer_available: bool) -> Result<IDXGIAdapter1> {
+ for adapter_index in 0.. {
+ let adapter: IDXGIAdapter1 = unsafe {
+ dxgi_factory
+ .EnumAdapterByGpuPreference(adapter_index, DXGI_GPU_PREFERENCE_MINIMUM_POWER)
+ }?;
+ if let Ok(desc) = unsafe { adapter.GetDesc1() } {
+ let gpu_name = String::from_utf16_lossy(&desc.Description)
+ .trim_matches(char::from(0))
+ .to_string();
+ log::info!("Using GPU: {}", gpu_name);
+ }
+ // Check to see whether the adapter supports Direct3D 11, but don't
+ // create the actual device yet.
+ if get_device(&adapter, None, None, debug_layer_available)
+ .log_err()
+ .is_some()
+ {
+ return Ok(adapter);
+ }
+ }
+
+ unreachable!()
+}
+
+#[inline]
+fn get_device(
+ adapter: &IDXGIAdapter1,
+ context: Option<*mut Option<ID3D11DeviceContext>>,
+ feature_level: Option<*mut D3D_FEATURE_LEVEL>,
+ debug_layer_available: bool,
+) -> Result<ID3D11Device> {
+ let mut device: Option<ID3D11Device> = None;
+ let device_flags = if debug_layer_available {
+ D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG
+ } else {
+ D3D11_CREATE_DEVICE_BGRA_SUPPORT
+ };
+ unsafe {
+ D3D11CreateDevice(
+ adapter,
+ D3D_DRIVER_TYPE_UNKNOWN,
+ HMODULE::default(),
+ device_flags,
+ // 4x MSAA is required for Direct3D Feature Level 10.1 or better
+ Some(&[
+ D3D_FEATURE_LEVEL_11_1,
+ D3D_FEATURE_LEVEL_11_0,
+ D3D_FEATURE_LEVEL_10_1,
+ ]),
+ D3D11_SDK_VERSION,
+ Some(&mut device),
+ feature_level,
+ context,
+ )?;
+ }
+ let device = device.unwrap();
+ let mut data = D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS::default();
+ unsafe {
+ device
+ .CheckFeatureSupport(
+ D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS,
+ &mut data as *mut _ as _,
+ std::mem::size_of::<D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS>() as u32,
+ )
+ .context("Checking GPU device feature support")?;
+ }
+ if data
+ .ComputeShaders_Plus_RawAndStructuredBuffers_Via_Shader_4_x
+ .as_bool()
+ {
+ Ok(device)
+ } else {
+ Err(anyhow::anyhow!(
+ "Required feature StructuredBuffer is not supported by GPU/driver"
+ ))
+ }
+}
@@ -1,14 +1,18 @@
-use std::{mem::ManuallyDrop, sync::Arc};
+use std::{
+ mem::ManuallyDrop,
+ sync::{Arc, OnceLock},
+};
use ::util::ResultExt;
use anyhow::{Context, Result};
use windows::{
Win32::{
- Foundation::{HMODULE, HWND},
+ Foundation::HWND,
Graphics::{
Direct3D::*,
Direct3D11::*,
DirectComposition::*,
+ DirectWrite::*,
Dxgi::{Common::*, *},
},
},
@@ -27,21 +31,27 @@ const RENDER_TARGET_FORMAT: DXGI_FORMAT = DXGI_FORMAT_B8G8R8A8_UNORM;
// This configuration is used for MSAA rendering on paths only, and it's guaranteed to be supported by DirectX 11.
const PATH_MULTISAMPLE_COUNT: u32 = 4;
+pub(crate) struct FontInfo {
+ pub gamma_ratios: [f32; 4],
+ pub grayscale_enhanced_contrast: f32,
+}
+
pub(crate) struct DirectXRenderer {
hwnd: HWND,
atlas: Arc<DirectXAtlas>,
- devices: ManuallyDrop<DirectXDevices>,
+ devices: ManuallyDrop<DirectXRendererDevices>,
resources: ManuallyDrop<DirectXResources>,
globals: DirectXGlobalElements,
pipelines: DirectXRenderPipelines,
direct_composition: Option<DirectComposition>,
+ font_info: &'static FontInfo,
}
/// Direct3D objects
#[derive(Clone)]
-pub(crate) struct DirectXDevices {
- adapter: IDXGIAdapter1,
- dxgi_factory: IDXGIFactory6,
+pub(crate) struct DirectXRendererDevices {
+ pub(crate) adapter: IDXGIAdapter1,
+ pub(crate) dxgi_factory: IDXGIFactory6,
pub(crate) device: ID3D11Device,
pub(crate) device_context: ID3D11DeviceContext,
dxgi_device: Option<IDXGIDevice>,
@@ -86,39 +96,17 @@ struct DirectComposition {
comp_visual: IDCompositionVisual,
}
-impl DirectXDevices {
- pub(crate) fn new(disable_direct_composition: bool) -> Result<ManuallyDrop<Self>> {
- let debug_layer_available = check_debug_layer_available();
- let dxgi_factory =
- get_dxgi_factory(debug_layer_available).context("Creating DXGI factory")?;
- let adapter =
- get_adapter(&dxgi_factory, debug_layer_available).context("Getting DXGI adapter")?;
- let (device, device_context) = {
- let mut device: Option<ID3D11Device> = None;
- let mut context: Option<ID3D11DeviceContext> = None;
- let mut feature_level = D3D_FEATURE_LEVEL::default();
- get_device(
- &adapter,
- Some(&mut device),
- Some(&mut context),
- Some(&mut feature_level),
- debug_layer_available,
- )
- .context("Creating Direct3D device")?;
- match feature_level {
- D3D_FEATURE_LEVEL_11_1 => {
- log::info!("Created device with Direct3D 11.1 feature level.")
- }
- D3D_FEATURE_LEVEL_11_0 => {
- log::info!("Created device with Direct3D 11.0 feature level.")
- }
- D3D_FEATURE_LEVEL_10_1 => {
- log::info!("Created device with Direct3D 10.1 feature level.")
- }
- _ => unreachable!(),
- }
- (device.unwrap(), context.unwrap())
- };
+impl DirectXRendererDevices {
+ pub(crate) fn new(
+ directx_devices: &DirectXDevices,
+ disable_direct_composition: bool,
+ ) -> Result<ManuallyDrop<Self>> {
+ let DirectXDevices {
+ adapter,
+ dxgi_factory,
+ device,
+ device_context,
+ } = directx_devices;
let dxgi_device = if disable_direct_composition {
None
} else {
@@ -126,23 +114,27 @@ impl DirectXDevices {
};
Ok(ManuallyDrop::new(Self {
- adapter,
- dxgi_factory,
+ adapter: adapter.clone(),
+ dxgi_factory: dxgi_factory.clone(),
+ device: device.clone(),
+ device_context: device_context.clone(),
dxgi_device,
- device,
- device_context,
}))
}
}
impl DirectXRenderer {
- pub(crate) fn new(hwnd: HWND, disable_direct_composition: bool) -> Result<Self> {
+ pub(crate) fn new(
+ hwnd: HWND,
+ directx_devices: &DirectXDevices,
+ disable_direct_composition: bool,
+ ) -> Result<Self> {
if disable_direct_composition {
log::info!("Direct Composition is disabled.");
}
- let devices =
- DirectXDevices::new(disable_direct_composition).context("Creating DirectX devices")?;
+ let devices = DirectXRendererDevices::new(directx_devices, disable_direct_composition)
+ .context("Creating DirectX devices")?;
let atlas = Arc::new(DirectXAtlas::new(&devices.device, &devices.device_context));
let resources = DirectXResources::new(&devices, 1, 1, hwnd, disable_direct_composition)
@@ -171,6 +163,7 @@ impl DirectXRenderer {
globals,
pipelines,
direct_composition,
+ font_info: Self::get_font_info(),
})
}
@@ -183,10 +176,12 @@ impl DirectXRenderer {
&self.devices.device_context,
self.globals.global_params_buffer[0].as_ref().unwrap(),
&[GlobalParams {
+ gamma_ratios: self.font_info.gamma_ratios,
viewport_size: [
self.resources.viewport[0].Width,
self.resources.viewport[0].Height,
],
+ grayscale_enhanced_contrast: self.font_info.grayscale_enhanced_contrast,
_pad: 0,
}],
)?;
@@ -205,28 +200,30 @@ impl DirectXRenderer {
Ok(())
}
+ #[inline]
fn present(&mut self) -> Result<()> {
- unsafe {
- let result = self.resources.swap_chain.Present(0, DXGI_PRESENT(0));
- // Presenting the swap chain can fail if the DirectX device was removed or reset.
- if result == DXGI_ERROR_DEVICE_REMOVED || result == DXGI_ERROR_DEVICE_RESET {
- let reason = self.devices.device.GetDeviceRemovedReason();
+ let result = unsafe { self.resources.swap_chain.Present(0, DXGI_PRESENT(0)) };
+ result.ok().context("Presenting swap chain failed")
+ }
+
+ pub(crate) fn handle_device_lost(&mut self, directx_devices: &DirectXDevices) {
+ try_to_recover_from_device_lost(
+ || {
+ self.handle_device_lost_impl(directx_devices)
+ .context("DirectXRenderer handling device lost")
+ },
+ |_| {},
+ || {
log::error!(
- "DirectX device removed or reset when drawing. Reason: {:?}",
- reason
+ "DirectXRenderer failed to recover from device lost after multiple attempts"
);
- self.handle_device_lost()?;
- } else {
- result.ok()?;
- }
- }
- Ok(())
+ // Do something here?
+ // At this point, the device loss is considered unrecoverable.
+ },
+ );
}
- fn handle_device_lost(&mut self) -> Result<()> {
- // Here we wait a bit to ensure the the system has time to recover from the device lost state.
- // If we don't wait, the final drawing result will be blank.
- std::thread::sleep(std::time::Duration::from_millis(300));
+ fn handle_device_lost_impl(&mut self, directx_devices: &DirectXDevices) -> Result<()> {
let disable_direct_composition = self.direct_composition.is_none();
unsafe {
@@ -249,7 +246,7 @@ impl DirectXRenderer {
ManuallyDrop::drop(&mut self.devices);
}
- let devices = DirectXDevices::new(disable_direct_composition)
+ let devices = DirectXRendererDevices::new(directx_devices, disable_direct_composition)
.context("Recreating DirectX devices")?;
let resources = DirectXResources::new(
&devices,
@@ -324,49 +321,39 @@ impl DirectXRenderer {
if self.resources.width == width && self.resources.height == height {
return Ok(());
}
+ self.resources.width = width;
+ self.resources.height = height;
+
+ // Clear the render target before resizing
+ unsafe { self.devices.device_context.OMSetRenderTargets(None, None) };
+ unsafe { ManuallyDrop::drop(&mut self.resources.render_target) };
+ drop(self.resources.render_target_view[0].take().unwrap());
+
+ // Resizing the swap chain requires a call to the underlying DXGI adapter, which can return the device removed error.
+ // The app might have moved to a monitor that's attached to a different graphics device.
+ // When a graphics device is removed or reset, the desktop resolution often changes, resulting in a window size change.
+ // But here we just return the error, because we are handling device lost scenarios elsewhere.
unsafe {
- // Clear the render target before resizing
- self.devices.device_context.OMSetRenderTargets(None, None);
- ManuallyDrop::drop(&mut self.resources.render_target);
- drop(self.resources.render_target_view[0].take().unwrap());
-
- let result = self.resources.swap_chain.ResizeBuffers(
- BUFFER_COUNT as u32,
- width,
- height,
- RENDER_TARGET_FORMAT,
- DXGI_SWAP_CHAIN_FLAG(0),
- );
- // Resizing the swap chain requires a call to the underlying DXGI adapter, which can return the device removed error.
- // The app might have moved to a monitor that's attached to a different graphics device.
- // When a graphics device is removed or reset, the desktop resolution often changes, resulting in a window size change.
- match result {
- Ok(_) => {}
- Err(e) => {
- if e.code() == DXGI_ERROR_DEVICE_REMOVED || e.code() == DXGI_ERROR_DEVICE_RESET
- {
- let reason = self.devices.device.GetDeviceRemovedReason();
- log::error!(
- "DirectX device removed or reset when resizing. Reason: {:?}",
- reason
- );
- self.resources.width = width;
- self.resources.height = height;
- self.handle_device_lost()?;
- return Ok(());
- } else {
- log::error!("Failed to resize swap chain: {:?}", e);
- return Err(e.into());
- }
- }
- }
-
self.resources
- .recreate_resources(&self.devices, width, height)?;
+ .swap_chain
+ .ResizeBuffers(
+ BUFFER_COUNT as u32,
+ width,
+ height,
+ RENDER_TARGET_FORMAT,
+ DXGI_SWAP_CHAIN_FLAG(0),
+ )
+ .context("Failed to resize swap chain")?;
+ }
+
+ self.resources
+ .recreate_resources(&self.devices, width, height)?;
+ unsafe {
self.devices
.device_context
.OMSetRenderTargets(Some(&self.resources.render_target_view), None);
}
+
Ok(())
}
@@ -617,11 +604,57 @@ impl DirectXRenderer {
driver_info: driver_version,
})
}
+
+ pub(crate) fn get_font_info() -> &'static FontInfo {
+ static CACHED_FONT_INFO: OnceLock<FontInfo> = OnceLock::new();
+ CACHED_FONT_INFO.get_or_init(|| unsafe {
+ let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED).unwrap();
+ let render_params: IDWriteRenderingParams1 =
+ factory.CreateRenderingParams().unwrap().cast().unwrap();
+ FontInfo {
+ gamma_ratios: Self::get_gamma_ratios(render_params.GetGamma()),
+ grayscale_enhanced_contrast: render_params.GetGrayscaleEnhancedContrast(),
+ }
+ })
+ }
+
+ // Gamma ratios for brightening/darkening edges for better contrast
+ // https://github.com/microsoft/terminal/blob/1283c0f5b99a2961673249fa77c6b986efb5086c/src/renderer/atlas/dwrite.cpp#L50
+ fn get_gamma_ratios(gamma: f32) -> [f32; 4] {
+ const GAMMA_INCORRECT_TARGET_RATIOS: [[f32; 4]; 13] = [
+ [0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0, 0.0000 / 4.0], // gamma = 1.0
+ [0.0166 / 4.0, -0.0807 / 4.0, 0.2227 / 4.0, -0.0751 / 4.0], // gamma = 1.1
+ [0.0350 / 4.0, -0.1760 / 4.0, 0.4325 / 4.0, -0.1370 / 4.0], // gamma = 1.2
+ [0.0543 / 4.0, -0.2821 / 4.0, 0.6302 / 4.0, -0.1876 / 4.0], // gamma = 1.3
+ [0.0739 / 4.0, -0.3963 / 4.0, 0.8167 / 4.0, -0.2287 / 4.0], // gamma = 1.4
+ [0.0933 / 4.0, -0.5161 / 4.0, 0.9926 / 4.0, -0.2616 / 4.0], // gamma = 1.5
+ [0.1121 / 4.0, -0.6395 / 4.0, 1.1588 / 4.0, -0.2877 / 4.0], // gamma = 1.6
+ [0.1300 / 4.0, -0.7649 / 4.0, 1.3159 / 4.0, -0.3080 / 4.0], // gamma = 1.7
+ [0.1469 / 4.0, -0.8911 / 4.0, 1.4644 / 4.0, -0.3234 / 4.0], // gamma = 1.8
+ [0.1627 / 4.0, -1.0170 / 4.0, 1.6051 / 4.0, -0.3347 / 4.0], // gamma = 1.9
+ [0.1773 / 4.0, -1.1420 / 4.0, 1.7385 / 4.0, -0.3426 / 4.0], // gamma = 2.0
+ [0.1908 / 4.0, -1.2652 / 4.0, 1.8650 / 4.0, -0.3476 / 4.0], // gamma = 2.1
+ [0.2031 / 4.0, -1.3864 / 4.0, 1.9851 / 4.0, -0.3501 / 4.0], // gamma = 2.2
+ ];
+
+ const NORM13: f32 = ((0x10000 as f64) / (255.0 * 255.0) * 4.0) as f32;
+ const NORM24: f32 = ((0x100 as f64) / (255.0) * 4.0) as f32;
+
+ let index = ((gamma * 10.0).round() as usize).clamp(10, 22) - 10;
+ let ratios = GAMMA_INCORRECT_TARGET_RATIOS[index];
+
+ [
+ ratios[0] * NORM13,
+ ratios[1] * NORM24,
+ ratios[2] * NORM13,
+ ratios[3] * NORM24,
+ ]
+ }
}
impl DirectXResources {
pub fn new(
- devices: &DirectXDevices,
+ devices: &DirectXRendererDevices,
width: u32,
height: u32,
hwnd: HWND,
@@ -666,7 +699,7 @@ impl DirectXResources {
#[inline]
fn recreate_resources(
&mut self,
- devices: &DirectXDevices,
+ devices: &DirectXRendererDevices,
width: u32,
height: u32,
) -> Result<()> {
@@ -686,8 +719,6 @@ impl DirectXResources {
self.path_intermediate_msaa_view = path_intermediate_msaa_view;
self.path_intermediate_srv = path_intermediate_srv;
self.viewport = viewport;
- self.width = width;
- self.height = height;
Ok(())
}
}
@@ -758,7 +789,7 @@ impl DirectXRenderPipelines {
impl DirectComposition {
pub fn new(dxgi_device: &IDXGIDevice, hwnd: HWND) -> Result<Self> {
- let comp_device = get_comp_device(&dxgi_device)?;
+ let comp_device = get_comp_device(dxgi_device)?;
let comp_target = unsafe { comp_device.CreateTargetForHwnd(hwnd, true) }?;
let comp_visual = unsafe { comp_device.CreateVisual() }?;
@@ -822,8 +853,10 @@ impl DirectXGlobalElements {
#[derive(Debug, Default)]
#[repr(C)]
struct GlobalParams {
+ gamma_ratios: [f32; 4],
viewport_size: [f32; 2],
- _pad: u64,
+ grayscale_enhanced_contrast: f32,
+ _pad: u32,
}
struct PipelineState<T> {
@@ -980,92 +1013,6 @@ impl Drop for DirectXResources {
}
}
-#[inline]
-fn check_debug_layer_available() -> bool {
- #[cfg(debug_assertions)]
- {
- unsafe { DXGIGetDebugInterface1::<IDXGIInfoQueue>(0) }
- .log_err()
- .is_some()
- }
- #[cfg(not(debug_assertions))]
- {
- false
- }
-}
-
-#[inline]
-fn get_dxgi_factory(debug_layer_available: bool) -> Result<IDXGIFactory6> {
- let factory_flag = if debug_layer_available {
- DXGI_CREATE_FACTORY_DEBUG
- } else {
- #[cfg(debug_assertions)]
- log::warn!(
- "Failed to get DXGI debug interface. DirectX debugging features will be disabled."
- );
- DXGI_CREATE_FACTORY_FLAGS::default()
- };
- unsafe { Ok(CreateDXGIFactory2(factory_flag)?) }
-}
-
-fn get_adapter(dxgi_factory: &IDXGIFactory6, debug_layer_available: bool) -> Result<IDXGIAdapter1> {
- for adapter_index in 0.. {
- let adapter: IDXGIAdapter1 = unsafe {
- dxgi_factory
- .EnumAdapterByGpuPreference(adapter_index, DXGI_GPU_PREFERENCE_MINIMUM_POWER)
- }?;
- if let Ok(desc) = unsafe { adapter.GetDesc1() } {
- let gpu_name = String::from_utf16_lossy(&desc.Description)
- .trim_matches(char::from(0))
- .to_string();
- log::info!("Using GPU: {}", gpu_name);
- }
- // Check to see whether the adapter supports Direct3D 11, but don't
- // create the actual device yet.
- if get_device(&adapter, None, None, None, debug_layer_available)
- .log_err()
- .is_some()
- {
- return Ok(adapter);
- }
- }
-
- unreachable!()
-}
-
-fn get_device(
- adapter: &IDXGIAdapter1,
- device: Option<*mut Option<ID3D11Device>>,
- context: Option<*mut Option<ID3D11DeviceContext>>,
- feature_level: Option<*mut D3D_FEATURE_LEVEL>,
- debug_layer_available: bool,
-) -> Result<()> {
- let device_flags = if debug_layer_available {
- D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG
- } else {
- D3D11_CREATE_DEVICE_BGRA_SUPPORT
- };
- unsafe {
- D3D11CreateDevice(
- adapter,
- D3D_DRIVER_TYPE_UNKNOWN,
- HMODULE::default(),
- device_flags,
- // 4x MSAA is required for Direct3D Feature Level 10.1 or better
- Some(&[
- D3D_FEATURE_LEVEL_11_1,
- D3D_FEATURE_LEVEL_11_0,
- D3D_FEATURE_LEVEL_10_1,
- ]),
- D3D11_SDK_VERSION,
- device,
- feature_level,
- context,
- )?;
- }
- Ok(())
-}
-
#[inline]
fn get_comp_device(dxgi_device: &IDXGIDevice) -> Result<IDCompositionDevice> {
Ok(unsafe { DCompositionCreateDevice(dxgi_device)? })
@@ -1130,7 +1077,7 @@ fn create_swap_chain(
#[inline]
fn create_resources(
- devices: &DirectXDevices,
+ devices: &DirectXRendererDevices,
swap_chain: &IDXGISwapChain1,
width: u32,
height: u32,
@@ -1144,7 +1091,7 @@ fn create_resources(
[D3D11_VIEWPORT; 1],
)> {
let (render_target, render_target_view) =
- create_render_target_and_its_view(&swap_chain, &devices.device)?;
+ create_render_target_and_its_view(swap_chain, &devices.device)?;
let (path_intermediate_texture, path_intermediate_srv) =
create_path_intermediate_texture(&devices.device, width, height)?;
let (path_intermediate_msaa_texture, path_intermediate_msaa_view) =
@@ -1544,6 +1491,10 @@ pub(crate) mod shader_resources {
#[cfg(debug_assertions)]
pub(super) fn build_shader_blob(entry: ShaderModule, target: ShaderTarget) -> Result<ID3DBlob> {
unsafe {
+ use windows::Win32::Graphics::{
+ Direct3D::ID3DInclude, Hlsl::D3D_COMPILE_STANDARD_FILE_INCLUDE,
+ };
+
let shader_name = if matches!(entry, ShaderModule::EmojiRasterization) {
"color_text_raster.hlsl"
} else {
@@ -1572,10 +1523,15 @@ pub(crate) mod shader_resources {
let entry_point = PCSTR::from_raw(entry.as_ptr());
let target_cstr = PCSTR::from_raw(target.as_ptr());
+ // really dirty trick because winapi bindings are unhappy otherwise
+ let include_handler = &std::mem::transmute::<usize, ID3DInclude>(
+ D3D_COMPILE_STANDARD_FILE_INCLUDE as usize,
+ );
+
let ret = D3DCompileFromFile(
&HSTRING::from(shader_path.to_str().unwrap()),
None,
- None,
+ include_handler,
entry_point,
target_cstr,
D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION,
@@ -1760,7 +1716,7 @@ mod amd {
anyhow::bail!("Failed to initialize AMD AGS, error code: {}", result);
}
- // Vulkan acctually returns this as the driver version
+ // Vulkan actually returns this as the driver version
let software_version = if !gpu_info.radeon_software_version.is_null() {
std::ffi::CStr::from_ptr(gpu_info.radeon_software_version)
.to_string_lossy()
@@ -9,41 +9,42 @@ use parking::Parker;
use parking_lot::Mutex;
use util::ResultExt;
use windows::{
- Foundation::TimeSpan,
System::Threading::{
- ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions,
- WorkItemPriority,
+ ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority,
},
Win32::{
Foundation::{LPARAM, WPARAM},
- UI::WindowsAndMessaging::PostThreadMessageW,
+ UI::WindowsAndMessaging::PostMessageW,
},
};
-use crate::{PlatformDispatcher, TaskLabel, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD};
+use crate::{
+ HWND, PlatformDispatcher, SafeHwnd, TaskLabel, WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD,
+};
pub(crate) struct WindowsDispatcher {
main_sender: Sender<Runnable>,
parker: Mutex<Parker>,
main_thread_id: ThreadId,
- main_thread_id_win32: u32,
+ platform_window_handle: SafeHwnd,
validation_number: usize,
}
impl WindowsDispatcher {
pub(crate) fn new(
main_sender: Sender<Runnable>,
- main_thread_id_win32: u32,
+ platform_window_handle: HWND,
validation_number: usize,
) -> Self {
let parker = Mutex::new(Parker::new());
let main_thread_id = current().id();
+ let platform_window_handle = platform_window_handle.into();
WindowsDispatcher {
main_sender,
parker,
main_thread_id,
- main_thread_id_win32,
+ platform_window_handle,
validation_number,
}
}
@@ -56,12 +57,7 @@ impl WindowsDispatcher {
Ok(())
})
};
- ThreadPool::RunWithPriorityAndOptionsAsync(
- &handler,
- WorkItemPriority::High,
- WorkItemOptions::TimeSliced,
- )
- .log_err();
+ ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err();
}
fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) {
@@ -72,12 +68,7 @@ impl WindowsDispatcher {
Ok(())
})
};
- let delay = TimeSpan {
- // A time period expressed in 100-nanosecond units.
- // 10,000,000 ticks per second
- Duration: (duration.as_nanos() / 100) as i64,
- };
- ThreadPoolTimer::CreateTimer(&handler, delay).log_err();
+ ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err();
}
}
@@ -96,8 +87,8 @@ impl PlatformDispatcher for WindowsDispatcher {
fn dispatch_on_main_thread(&self, runnable: Runnable) {
match self.main_sender.send(runnable) {
Ok(_) => unsafe {
- PostThreadMessageW(
- self.main_thread_id_win32,
+ PostMessageW(
+ Some(self.platform_window_handle.as_raw()),
WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD,
WPARAM(self.validation_number),
LPARAM(0),
@@ -24,6 +24,8 @@ pub(crate) const WM_GPUI_CLOSE_ONE_WINDOW: u32 = WM_USER + 2;
pub(crate) const WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD: u32 = WM_USER + 3;
pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4;
pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5;
+pub(crate) const WM_GPUI_KEYBOARD_LAYOUT_CHANGED: u32 = WM_USER + 6;
+pub(crate) const WM_GPUI_GPU_DEVICE_LOST: u32 = WM_USER + 7;
const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1;
@@ -39,7 +41,6 @@ impl WindowsWindowInner {
let handled = match msg {
WM_ACTIVATE => self.handle_activate_msg(wparam),
WM_CREATE => self.handle_create_msg(handle),
- WM_DEVICECHANGE => self.handle_device_change_msg(handle, wparam),
WM_MOVE => self.handle_move_msg(handle, lparam),
WM_SIZE => self.handle_size_msg(wparam, lparam),
WM_GETMINMAXINFO => self.handle_get_min_max_info_msg(lparam),
@@ -99,9 +100,11 @@ impl WindowsWindowInner {
WM_IME_COMPOSITION => self.handle_ime_composition(handle, lparam),
WM_SETCURSOR => self.handle_set_cursor(handle, lparam),
WM_SETTINGCHANGE => self.handle_system_settings_changed(handle, wparam, lparam),
- WM_INPUTLANGCHANGE => self.handle_input_language_changed(lparam),
+ WM_INPUTLANGCHANGE => self.handle_input_language_changed(),
+ WM_SHOWWINDOW => self.handle_window_visibility_changed(handle, wparam),
WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam),
WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true),
+ WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam),
_ => None,
};
if let Some(n) = handled {
@@ -263,8 +266,8 @@ impl WindowsWindowInner {
callback();
}
unsafe {
- PostThreadMessageW(
- self.main_thread_id_win32,
+ PostMessageW(
+ Some(self.platform_window_handle),
WM_GPUI_CLOSE_ONE_WINDOW,
WPARAM(self.validation_number),
LPARAM(handle.0 as isize),
@@ -700,29 +703,28 @@ impl WindowsWindowInner {
// Fix auto hide taskbar not showing. This solution is based on the approach
// used by Chrome. However, it may result in one row of pixels being obscured
// in our client area. But as Chrome says, "there seems to be no better solution."
- if is_maximized {
- if let Some(ref taskbar_position) = self
+ if is_maximized
+ && let Some(ref taskbar_position) = self
.state
.borrow()
.system_settings
.auto_hide_taskbar_position
- {
- // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge,
- // so the window isn't treated as a "fullscreen app", which would cause
- // the taskbar to disappear.
- match taskbar_position {
- AutoHideTaskbarPosition::Left => {
- requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX
- }
- AutoHideTaskbarPosition::Top => {
- requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX
- }
- AutoHideTaskbarPosition::Right => {
- requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX
- }
- AutoHideTaskbarPosition::Bottom => {
- requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX
- }
+ {
+ // For the auto-hide taskbar, adjust in by 1 pixel on taskbar edge,
+ // so the window isn't treated as a "fullscreen app", which would cause
+ // the taskbar to disappear.
+ match taskbar_position {
+ AutoHideTaskbarPosition::Left => {
+ requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX
+ }
+ AutoHideTaskbarPosition::Top => {
+ requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX
+ }
+ AutoHideTaskbarPosition::Right => {
+ requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX
+ }
+ AutoHideTaskbarPosition::Bottom => {
+ requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX
}
}
}
@@ -956,7 +958,7 @@ impl WindowsWindowInner {
click_count,
first_mouse: false,
});
- let result = func(input.clone());
+ let result = func(input);
let handled = !result.propagate || result.default_prevented;
self.state.borrow_mut().callbacks.input = Some(func);
@@ -1124,62 +1126,54 @@ impl WindowsWindowInner {
// lParam is a pointer to a string that indicates the area containing the system parameter
// that was changed.
let parameter = PCWSTR::from_raw(lparam.0 as _);
- if unsafe { !parameter.is_null() && !parameter.is_empty() } {
- if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() {
- log::info!("System settings changed: {}", parameter_string);
- match parameter_string.as_str() {
- "ImmersiveColorSet" => {
- let new_appearance = system_appearance()
- .context(
- "unable to get system appearance when handling ImmersiveColorSet",
- )
- .log_err()?;
- let mut lock = self.state.borrow_mut();
- if new_appearance != lock.appearance {
- lock.appearance = new_appearance;
- let mut callback = lock.callbacks.appearance_changed.take()?;
- drop(lock);
- callback();
- self.state.borrow_mut().callbacks.appearance_changed = Some(callback);
- configure_dwm_dark_mode(handle, new_appearance);
- }
- }
- _ => {}
+ if unsafe { !parameter.is_null() && !parameter.is_empty() }
+ && let Some(parameter_string) = unsafe { parameter.to_string() }.log_err()
+ {
+ log::info!("System settings changed: {}", parameter_string);
+ if parameter_string.as_str() == "ImmersiveColorSet" {
+ let new_appearance = system_appearance()
+ .context("unable to get system appearance when handling ImmersiveColorSet")
+ .log_err()?;
+ let mut lock = self.state.borrow_mut();
+ if new_appearance != lock.appearance {
+ lock.appearance = new_appearance;
+ let mut callback = lock.callbacks.appearance_changed.take()?;
+ drop(lock);
+ callback();
+ self.state.borrow_mut().callbacks.appearance_changed = Some(callback);
+ configure_dwm_dark_mode(handle, new_appearance);
}
}
}
Some(0)
}
- fn handle_input_language_changed(&self, lparam: LPARAM) -> Option<isize> {
- let thread = self.main_thread_id_win32;
- let validation = self.validation_number;
+ fn handle_input_language_changed(&self) -> Option<isize> {
unsafe {
- PostThreadMessageW(thread, WM_INPUTLANGCHANGE, WPARAM(validation), lparam).log_err();
+ PostMessageW(
+ Some(self.platform_window_handle),
+ WM_GPUI_KEYBOARD_LAYOUT_CHANGED,
+ WPARAM(self.validation_number),
+ LPARAM(0),
+ )
+ .log_err();
}
Some(0)
}
- fn handle_device_change_msg(&self, handle: HWND, wparam: WPARAM) -> Option<isize> {
- if wparam.0 == DBT_DEVNODES_CHANGED as usize {
- // The reason for sending this message is to actually trigger a redraw of the window.
- unsafe {
- PostMessageW(
- Some(handle),
- WM_GPUI_FORCE_UPDATE_WINDOW,
- WPARAM(0),
- LPARAM(0),
- )
- .log_err();
- }
- // If the GPU device is lost, this redraw will take care of recreating the device context.
- // The WM_GPUI_FORCE_UPDATE_WINDOW message will take care of redrawing the window, after
- // the device context has been recreated.
- self.draw_window(handle, true)
- } else {
- // Other device change messages are not handled.
- None
+ fn handle_window_visibility_changed(&self, handle: HWND, wparam: WPARAM) -> Option<isize> {
+ if wparam.0 == 1 {
+ self.draw_window(handle, false);
}
+ None
+ }
+
+ fn handle_device_lost(&self, lparam: LPARAM) -> Option<isize> {
+ let mut lock = self.state.borrow_mut();
+ let devices = lparam.0 as *const DirectXDevices;
+ let devices = unsafe { &*devices };
+ lock.renderer.handle_device_lost(&devices);
+ Some(0)
}
#[inline]
@@ -1464,7 +1458,7 @@ pub(crate) fn current_modifiers() -> Modifiers {
#[inline]
pub(crate) fn current_capslock() -> Capslock {
let on = unsafe { GetKeyState(VK_CAPITAL.0 as i32) & 1 } > 0;
- Capslock { on: on }
+ Capslock { on }
}
fn get_client_area_insets(
@@ -1,22 +1,31 @@
use anyhow::Result;
+use collections::HashMap;
use windows::Win32::UI::{
Input::KeyboardAndMouse::{
- GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
- VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
- VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
- VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
+ GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
+ VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
+ VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
+ VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
},
WindowsAndMessaging::KL_NAMELENGTH,
};
use windows_core::HSTRING;
-use crate::{Modifiers, PlatformKeyboardLayout};
+use crate::{
+ KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
+};
pub(crate) struct WindowsKeyboardLayout {
id: String,
name: String,
}
+pub(crate) struct WindowsKeyboardMapper {
+ key_to_vkey: HashMap<String, (u16, bool)>,
+ vkey_to_key: HashMap<u16, String>,
+ vkey_to_shifted: HashMap<u16, String>,
+}
+
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
fn id(&self) -> &str {
&self.id
@@ -27,6 +36,61 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout {
}
}
+impl PlatformKeyboardMapper for WindowsKeyboardMapper {
+ fn map_key_equivalent(
+ &self,
+ mut keystroke: Keystroke,
+ use_key_equivalents: bool,
+ ) -> KeybindingKeystroke {
+ let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
+ else {
+ return KeybindingKeystroke::from_keystroke(keystroke);
+ };
+ if shifted_key && keystroke.modifiers.shift {
+ log::warn!(
+ "Keystroke '{}' has both shift and a shifted key, this is likely a bug",
+ keystroke.key
+ );
+ }
+
+ let shift = shifted_key || keystroke.modifiers.shift;
+ keystroke.modifiers.shift = false;
+
+ let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
+ log::error!(
+ "Failed to map key equivalent '{:?}' to a valid key",
+ keystroke
+ );
+ return KeybindingKeystroke::from_keystroke(keystroke);
+ };
+
+ keystroke.key = if shift {
+ let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
+ log::error!(
+ "Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
+ keystroke,
+ vkey
+ );
+ return KeybindingKeystroke::from_keystroke(keystroke);
+ };
+ shifted_key
+ } else {
+ key.clone()
+ };
+
+ let modifiers = Modifiers {
+ shift,
+ ..keystroke.modifiers
+ };
+
+ KeybindingKeystroke::new(keystroke, modifiers, key)
+ }
+
+ fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
+ None
+ }
+}
+
impl WindowsKeyboardLayout {
pub(crate) fn new() -> Result<Self> {
let mut buffer = [0u16; KL_NAMELENGTH as usize];
@@ -48,6 +112,41 @@ impl WindowsKeyboardLayout {
}
}
+impl WindowsKeyboardMapper {
+ pub(crate) fn new() -> Self {
+ let mut key_to_vkey = HashMap::default();
+ let mut vkey_to_key = HashMap::default();
+ let mut vkey_to_shifted = HashMap::default();
+ for vkey in CANDIDATE_VKEYS {
+ if let Some(key) = get_key_from_vkey(*vkey) {
+ key_to_vkey.insert(key.clone(), (vkey.0, false));
+ vkey_to_key.insert(vkey.0, key);
+ }
+ let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
+ if scan_code == 0 {
+ continue;
+ }
+ if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
+ key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
+ vkey_to_shifted.insert(vkey.0, shifted_key);
+ }
+ }
+ Self {
+ key_to_vkey,
+ vkey_to_key,
+ vkey_to_shifted,
+ }
+ }
+
+ fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
+ if use_key_equivalents {
+ get_vkey_from_key_with_us_layout(key)
+ } else {
+ self.key_to_vkey.get(key).cloned()
+ }
+ }
+}
+
pub(crate) fn get_keystroke_key(
vkey: VIRTUAL_KEY,
scan_code: u32,
@@ -140,3 +239,134 @@ pub(crate) fn generate_key_char(
_ => None,
}
}
+
+fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
+ match key {
+ // ` => VK_OEM_3
+ "`" => Some((VK_OEM_3.0, false)),
+ "~" => Some((VK_OEM_3.0, true)),
+ "1" => Some((VK_1.0, false)),
+ "!" => Some((VK_1.0, true)),
+ "2" => Some((VK_2.0, false)),
+ "@" => Some((VK_2.0, true)),
+ "3" => Some((VK_3.0, false)),
+ "#" => Some((VK_3.0, true)),
+ "4" => Some((VK_4.0, false)),
+ "$" => Some((VK_4.0, true)),
+ "5" => Some((VK_5.0, false)),
+ "%" => Some((VK_5.0, true)),
+ "6" => Some((VK_6.0, false)),
+ "^" => Some((VK_6.0, true)),
+ "7" => Some((VK_7.0, false)),
+ "&" => Some((VK_7.0, true)),
+ "8" => Some((VK_8.0, false)),
+ "*" => Some((VK_8.0, true)),
+ "9" => Some((VK_9.0, false)),
+ "(" => Some((VK_9.0, true)),
+ "0" => Some((VK_0.0, false)),
+ ")" => Some((VK_0.0, true)),
+ "-" => Some((VK_OEM_MINUS.0, false)),
+ "_" => Some((VK_OEM_MINUS.0, true)),
+ "=" => Some((VK_OEM_PLUS.0, false)),
+ "+" => Some((VK_OEM_PLUS.0, true)),
+ "[" => Some((VK_OEM_4.0, false)),
+ "{" => Some((VK_OEM_4.0, true)),
+ "]" => Some((VK_OEM_6.0, false)),
+ "}" => Some((VK_OEM_6.0, true)),
+ "\\" => Some((VK_OEM_5.0, false)),
+ "|" => Some((VK_OEM_5.0, true)),
+ ";" => Some((VK_OEM_1.0, false)),
+ ":" => Some((VK_OEM_1.0, true)),
+ "'" => Some((VK_OEM_7.0, false)),
+ "\"" => Some((VK_OEM_7.0, true)),
+ "," => Some((VK_OEM_COMMA.0, false)),
+ "<" => Some((VK_OEM_COMMA.0, true)),
+ "." => Some((VK_OEM_PERIOD.0, false)),
+ ">" => Some((VK_OEM_PERIOD.0, true)),
+ "/" => Some((VK_OEM_2.0, false)),
+ "?" => Some((VK_OEM_2.0, true)),
+ _ => None,
+ }
+}
+
+const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
+ VK_OEM_3,
+ VK_OEM_MINUS,
+ VK_OEM_PLUS,
+ VK_OEM_4,
+ VK_OEM_5,
+ VK_OEM_6,
+ VK_OEM_1,
+ VK_OEM_7,
+ VK_OEM_COMMA,
+ VK_OEM_PERIOD,
+ VK_OEM_2,
+ VK_OEM_102,
+ VK_OEM_8,
+ VK_ABNT_C1,
+ VK_0,
+ VK_1,
+ VK_2,
+ VK_3,
+ VK_4,
+ VK_5,
+ VK_6,
+ VK_7,
+ VK_8,
+ VK_9,
+];
+
+#[cfg(test)]
+mod tests {
+ use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
+
+ #[test]
+ fn test_keyboard_mapper() {
+ let mapper = WindowsKeyboardMapper::new();
+
+ // Normal case
+ let keystroke = Keystroke {
+ modifiers: Modifiers::control(),
+ key: "a".to_string(),
+ key_char: None,
+ };
+ let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
+ assert_eq!(*mapped.inner(), keystroke);
+ assert_eq!(mapped.key(), "a");
+ assert_eq!(*mapped.modifiers(), Modifiers::control());
+
+ // Shifted case, ctrl-$
+ let keystroke = Keystroke {
+ modifiers: Modifiers::control(),
+ key: "$".to_string(),
+ key_char: None,
+ };
+ let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
+ assert_eq!(*mapped.inner(), keystroke);
+ assert_eq!(mapped.key(), "4");
+ assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
+
+ // Shifted case, but shift is true
+ let keystroke = Keystroke {
+ modifiers: Modifiers::control_shift(),
+ key: "$".to_string(),
+ key_char: None,
+ };
+ let mapped = mapper.map_key_equivalent(keystroke, true);
+ assert_eq!(mapped.inner().modifiers, Modifiers::control());
+ assert_eq!(mapped.key(), "4");
+ assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
+
+ // Windows style
+ let keystroke = Keystroke {
+ modifiers: Modifiers::control_shift(),
+ key: "4".to_string(),
+ key_char: None,
+ };
+ let mapped = mapper.map_key_equivalent(keystroke, true);
+ assert_eq!(mapped.inner().modifiers, Modifiers::control());
+ assert_eq!(mapped.inner().key, "$");
+ assert_eq!(mapped.key(), "4");
+ assert_eq!(*mapped.modifiers(), Modifiers::control_shift());
+ }
+}
@@ -1,8 +1,9 @@
use std::{
cell::RefCell,
+ ffi::OsStr,
mem::ManuallyDrop,
path::{Path, PathBuf},
- rc::Rc,
+ rc::{Rc, Weak},
sync::Arc,
};
@@ -17,12 +18,9 @@ use windows::{
UI::ViewManagement::UISettings,
Win32::{
Foundation::*,
- Graphics::{
- Gdi::*,
- Imaging::{CLSID_WICImagingFactory, IWICImagingFactory},
- },
+ Graphics::{Direct3D11::ID3D11Device, Gdi::*},
Security::Credentials::*,
- System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*},
+ System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*},
UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
},
core::*,
@@ -31,28 +29,34 @@ use windows::{
use crate::*;
pub(crate) struct WindowsPlatform {
- state: RefCell<WindowsPlatformState>,
+ inner: Rc<WindowsPlatformInner>,
raw_window_handles: Arc<RwLock<SmallVec<[SafeHwnd; 4]>>>,
// The below members will never change throughout the entire lifecycle of the app.
icon: HICON,
- main_receiver: flume::Receiver<Runnable>,
background_executor: BackgroundExecutor,
foreground_executor: ForegroundExecutor,
text_system: Arc<DirectWriteTextSystem>,
windows_version: WindowsVersion,
- bitmap_factory: ManuallyDrop<IWICImagingFactory>,
drop_target_helper: IDropTargetHelper,
- validation_number: usize,
- main_thread_id_win32: u32,
+ handle: HWND,
disable_direct_composition: bool,
}
+struct WindowsPlatformInner {
+ state: RefCell<WindowsPlatformState>,
+ raw_window_handles: std::sync::Weak<RwLock<SmallVec<[SafeHwnd; 4]>>>,
+ // The below members will never change throughout the entire lifecycle of the app.
+ validation_number: usize,
+ main_receiver: flume::Receiver<Runnable>,
+}
+
pub(crate) struct WindowsPlatformState {
callbacks: PlatformCallbacks,
menus: Vec<OwnedMenu>,
jump_list: JumpList,
// NOTE: standard cursor handles don't need to close.
pub(crate) current_cursor: Option<HCURSOR>,
+ directx_devices: ManuallyDrop<DirectXDevices>,
}
#[derive(Default)]
@@ -67,15 +71,17 @@ struct PlatformCallbacks {
}
impl WindowsPlatformState {
- fn new() -> Self {
+ fn new(directx_devices: DirectXDevices) -> Self {
let callbacks = PlatformCallbacks::default();
let jump_list = JumpList::new();
let current_cursor = load_cursor(CursorStyle::Arrow);
+ let directx_devices = ManuallyDrop::new(directx_devices);
Self {
callbacks,
jump_list,
current_cursor,
+ directx_devices,
menus: Vec::new(),
}
}
@@ -86,51 +92,72 @@ impl WindowsPlatform {
unsafe {
OleInitialize(None).context("unable to initialize Windows OLE")?;
}
+ let directx_devices = DirectXDevices::new().context("Creating DirectX devices")?;
let (main_sender, main_receiver) = flume::unbounded::<Runnable>();
- let main_thread_id_win32 = unsafe { GetCurrentThreadId() };
- let validation_number = rand::random::<usize>();
+ let validation_number = if usize::BITS == 64 {
+ rand::random::<u64>() as usize
+ } else {
+ rand::random::<u32>() as usize
+ };
+ let raw_window_handles = Arc::new(RwLock::new(SmallVec::new()));
+ let text_system = Arc::new(
+ DirectWriteTextSystem::new(&directx_devices)
+ .context("Error creating DirectWriteTextSystem")?,
+ );
+ register_platform_window_class();
+ let mut context = PlatformWindowCreateContext {
+ inner: None,
+ raw_window_handles: Arc::downgrade(&raw_window_handles),
+ validation_number,
+ main_receiver: Some(main_receiver),
+ directx_devices: Some(directx_devices),
+ };
+ let result = unsafe {
+ CreateWindowExW(
+ WINDOW_EX_STYLE(0),
+ PLATFORM_WINDOW_CLASS_NAME,
+ None,
+ WINDOW_STYLE(0),
+ 0,
+ 0,
+ 0,
+ 0,
+ Some(HWND_MESSAGE),
+ None,
+ None,
+ Some(&context as *const _ as *const _),
+ )
+ };
+ let inner = context.inner.take().unwrap()?;
+ let handle = result?;
let dispatcher = Arc::new(WindowsDispatcher::new(
main_sender,
- main_thread_id_win32,
+ handle,
validation_number,
));
let disable_direct_composition = std::env::var(DISABLE_DIRECT_COMPOSITION)
.is_ok_and(|value| value == "true" || value == "1");
let background_executor = BackgroundExecutor::new(dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(dispatcher);
- let directx_devices = DirectXDevices::new(disable_direct_composition)
- .context("Unable to init directx devices.")?;
- let bitmap_factory = ManuallyDrop::new(unsafe {
- CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER)
- .context("Error creating bitmap factory.")?
- });
- let text_system = Arc::new(
- DirectWriteTextSystem::new(&directx_devices, &bitmap_factory)
- .context("Error creating DirectWriteTextSystem")?,
- );
+
let drop_target_helper: IDropTargetHelper = unsafe {
CoCreateInstance(&CLSID_DragDropHelper, None, CLSCTX_INPROC_SERVER)
.context("Error creating drop target helper.")?
};
let icon = load_icon().unwrap_or_default();
- let state = RefCell::new(WindowsPlatformState::new());
- let raw_window_handles = Arc::new(RwLock::new(SmallVec::new()));
let windows_version = WindowsVersion::new().context("Error retrieve windows version")?;
Ok(Self {
- state,
+ inner,
+ handle,
raw_window_handles,
icon,
- main_receiver,
background_executor,
foreground_executor,
text_system,
disable_direct_composition,
windows_version,
- bitmap_factory,
drop_target_helper,
- validation_number,
- main_thread_id_win32,
})
}
@@ -152,119 +179,21 @@ impl WindowsPlatform {
});
}
- fn close_one_window(&self, target_window: HWND) -> bool {
- let mut lock = self.raw_window_handles.write();
- let index = lock
- .iter()
- .position(|handle| handle.as_raw() == target_window)
- .unwrap();
- lock.remove(index);
-
- lock.is_empty()
- }
-
- #[inline]
- fn run_foreground_task(&self) {
- for runnable in self.main_receiver.drain() {
- runnable.run();
- }
- }
-
fn generate_creation_info(&self) -> WindowCreationInfo {
WindowCreationInfo {
icon: self.icon,
executor: self.foreground_executor.clone(),
- current_cursor: self.state.borrow().current_cursor,
+ current_cursor: self.inner.state.borrow().current_cursor,
windows_version: self.windows_version,
drop_target_helper: self.drop_target_helper.clone(),
- validation_number: self.validation_number,
- main_receiver: self.main_receiver.clone(),
- main_thread_id_win32: self.main_thread_id_win32,
+ validation_number: self.inner.validation_number,
+ main_receiver: self.inner.main_receiver.clone(),
+ platform_window_handle: self.handle,
disable_direct_composition: self.disable_direct_composition,
+ directx_devices: (*self.inner.state.borrow().directx_devices).clone(),
}
}
- fn handle_dock_action_event(&self, action_idx: usize) {
- let mut lock = self.state.borrow_mut();
- if let Some(mut callback) = lock.callbacks.app_menu_action.take() {
- let Some(action) = lock
- .jump_list
- .dock_menus
- .get(action_idx)
- .map(|dock_menu| dock_menu.action.boxed_clone())
- else {
- lock.callbacks.app_menu_action = Some(callback);
- log::error!("Dock menu for index {action_idx} not found");
- return;
- };
- drop(lock);
- callback(&*action);
- self.state.borrow_mut().callbacks.app_menu_action = Some(callback);
- }
- }
-
- fn handle_input_lang_change(&self) {
- let mut lock = self.state.borrow_mut();
- if let Some(mut callback) = lock.callbacks.keyboard_layout_change.take() {
- drop(lock);
- callback();
- self.state
- .borrow_mut()
- .callbacks
- .keyboard_layout_change
- .get_or_insert(callback);
- }
- }
-
- // Returns if the app should quit.
- fn handle_events(&self) {
- let mut msg = MSG::default();
- unsafe {
- while GetMessageW(&mut msg, None, 0, 0).as_bool() {
- match msg.message {
- WM_QUIT => return,
- WM_INPUTLANGCHANGE
- | WM_GPUI_CLOSE_ONE_WINDOW
- | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD
- | WM_GPUI_DOCK_MENU_ACTION => {
- if self.handle_gpui_evnets(msg.message, msg.wParam, msg.lParam, &msg) {
- return;
- }
- }
- _ => {
- DispatchMessageW(&msg);
- }
- }
- }
- }
- }
-
- // Returns true if the app should quit.
- fn handle_gpui_evnets(
- &self,
- message: u32,
- wparam: WPARAM,
- lparam: LPARAM,
- msg: *const MSG,
- ) -> bool {
- if wparam.0 != self.validation_number {
- unsafe { DispatchMessageW(msg) };
- return false;
- }
- match message {
- WM_GPUI_CLOSE_ONE_WINDOW => {
- if self.close_one_window(HWND(lparam.0 as _)) {
- return true;
- }
- }
- WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => self.run_foreground_task(),
- WM_GPUI_DOCK_MENU_ACTION => self.handle_dock_action_event(lparam.0 as _),
- WM_INPUTLANGCHANGE => self.handle_input_lang_change(),
- _ => unreachable!(),
- }
- false
- }
-
fn set_dock_menus(&self, menus: Vec<MenuItem>) {
let mut actions = Vec::new();
menus.into_iter().for_each(|menu| {
@@ -272,7 +201,7 @@ impl WindowsPlatform {
actions.push(dock_menu);
}
});
- let mut lock = self.state.borrow_mut();
+ let mut lock = self.inner.state.borrow_mut();
lock.jump_list.dock_menus = actions;
update_jump_list(&lock.jump_list).log_err();
}
@@ -288,7 +217,7 @@ impl WindowsPlatform {
actions.push(dock_menu);
}
});
- let mut lock = self.state.borrow_mut();
+ let mut lock = self.inner.state.borrow_mut();
lock.jump_list.dock_menus = actions;
lock.jump_list.recent_workspaces = entries;
update_jump_list(&lock.jump_list)
@@ -309,19 +238,30 @@ impl WindowsPlatform {
}
fn begin_vsync_thread(&self) {
+ let mut directx_device = (*self.inner.state.borrow().directx_devices).clone();
+ let platform_window: SafeHwnd = self.handle.into();
+ let validation_number = self.inner.validation_number;
let all_windows = Arc::downgrade(&self.raw_window_handles);
+ let text_system = Arc::downgrade(&self.text_system);
std::thread::spawn(move || {
let vsync_provider = VSyncProvider::new();
loop {
vsync_provider.wait_for_vsync();
+ if check_device_lost(&directx_device.device) {
+ handle_gpu_device_lost(
+ &mut directx_device,
+ platform_window.as_raw(),
+ validation_number,
+ &all_windows,
+ &text_system,
+ );
+ }
let Some(all_windows) = all_windows.upgrade() else {
break;
};
for hwnd in all_windows.read().iter() {
unsafe {
- RedrawWindow(Some(hwnd.as_raw()), None, None, RDW_INVALIDATE)
- .ok()
- .log_err();
+ let _ = RedrawWindow(Some(hwnd.as_raw()), None, None, RDW_INVALIDATE);
}
}
}
@@ -350,16 +290,30 @@ impl Platform for WindowsPlatform {
)
}
+ fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
+ Rc::new(WindowsKeyboardMapper::new())
+ }
+
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
- self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
+ self.inner
+ .state
+ .borrow_mut()
+ .callbacks
+ .keyboard_layout_change = Some(callback);
}
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
on_finish_launching();
self.begin_vsync_thread();
- self.handle_events();
- if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit {
+ let mut msg = MSG::default();
+ unsafe {
+ while GetMessageW(&mut msg, None, 0, 0).as_bool() {
+ DispatchMessageW(&msg);
+ }
+ }
+
+ if let Some(ref mut callback) = self.inner.state.borrow_mut().callbacks.quit {
callback();
}
}
@@ -460,19 +414,21 @@ impl Platform for WindowsPlatform {
}
fn open_url(&self, url: &str) {
+ if url.is_empty() {
+ return;
+ }
let url_string = url.to_string();
self.background_executor()
.spawn(async move {
- if url_string.is_empty() {
- return;
- }
- open_target(url_string.as_str());
+ open_target(&url_string)
+ .with_context(|| format!("Opening url: {}", url_string))
+ .log_err();
})
.detach();
}
fn on_open_urls(&self, callback: Box<dyn FnMut(Vec<String>)>) {
- self.state.borrow_mut().callbacks.open_urls = Some(callback);
+ self.inner.state.borrow_mut().callbacks.open_urls = Some(callback);
}
fn prompt_for_paths(
@@ -490,13 +446,18 @@ impl Platform for WindowsPlatform {
rx
}
- fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Result<Option<PathBuf>>> {
+ fn prompt_for_new_path(
+ &self,
+ directory: &Path,
+ suggested_name: Option<&str>,
+ ) -> Receiver<Result<Option<PathBuf>>> {
let directory = directory.to_owned();
+ let suggested_name = suggested_name.map(|s| s.to_owned());
let (tx, rx) = oneshot::channel();
let window = self.find_current_active_window();
self.foreground_executor()
.spawn(async move {
- let _ = tx.send(file_save_dialog(directory, window));
+ let _ = tx.send(file_save_dialog(directory, suggested_name, window));
})
.detach();
@@ -509,55 +470,47 @@ impl Platform for WindowsPlatform {
}
fn reveal_path(&self, path: &Path) {
- let Ok(file_full_path) = path.canonicalize() else {
- log::error!("unable to parse file path");
+ if path.as_os_str().is_empty() {
return;
- };
+ }
+ let path = path.to_path_buf();
self.background_executor()
.spawn(async move {
- let Some(path) = file_full_path.to_str() else {
- return;
- };
- if path.is_empty() {
- return;
- }
- open_target_in_explorer(path);
+ open_target_in_explorer(&path)
+ .with_context(|| format!("Revealing path {} in explorer", path.display()))
+ .log_err();
})
.detach();
}
fn open_with_system(&self, path: &Path) {
- let Ok(full_path) = path.canonicalize() else {
- log::error!("unable to parse file full path: {}", path.display());
+ if path.as_os_str().is_empty() {
return;
- };
+ }
+ let path = path.to_path_buf();
self.background_executor()
.spawn(async move {
- let Some(full_path_str) = full_path.to_str() else {
- return;
- };
- if full_path_str.is_empty() {
- return;
- };
- open_target(full_path_str);
+ open_target(&path)
+ .with_context(|| format!("Opening {} with system", path.display()))
+ .log_err();
})
.detach();
}
fn on_quit(&self, callback: Box<dyn FnMut()>) {
- self.state.borrow_mut().callbacks.quit = Some(callback);
+ self.inner.state.borrow_mut().callbacks.quit = Some(callback);
}
fn on_reopen(&self, callback: Box<dyn FnMut()>) {
- self.state.borrow_mut().callbacks.reopen = Some(callback);
+ self.inner.state.borrow_mut().callbacks.reopen = Some(callback);
}
fn set_menus(&self, menus: Vec<Menu>, _keymap: &Keymap) {
- self.state.borrow_mut().menus = menus.into_iter().map(|menu| menu.owned()).collect();
+ self.inner.state.borrow_mut().menus = menus.into_iter().map(|menu| menu.owned()).collect();
}
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
- Some(self.state.borrow().menus.clone())
+ Some(self.inner.state.borrow().menus.clone())
}
fn set_dock_menu(&self, menus: Vec<MenuItem>, _keymap: &Keymap) {
@@ -565,15 +518,19 @@ impl Platform for WindowsPlatform {
}
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>) {
- self.state.borrow_mut().callbacks.app_menu_action = Some(callback);
+ self.inner.state.borrow_mut().callbacks.app_menu_action = Some(callback);
}
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>) {
- self.state.borrow_mut().callbacks.will_open_app_menu = Some(callback);
+ self.inner.state.borrow_mut().callbacks.will_open_app_menu = Some(callback);
}
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
- self.state.borrow_mut().callbacks.validate_app_menu_command = Some(callback);
+ self.inner
+ .state
+ .borrow_mut()
+ .callbacks
+ .validate_app_menu_command = Some(callback);
}
fn app_path(&self) -> Result<PathBuf> {
@@ -587,7 +544,7 @@ impl Platform for WindowsPlatform {
fn set_cursor_style(&self, style: CursorStyle) {
let hcursor = load_cursor(style);
- let mut lock = self.state.borrow_mut();
+ let mut lock = self.inner.state.borrow_mut();
if lock.current_cursor.map(|c| c.0) != hcursor.map(|c| c.0) {
self.post_message(
WM_GPUI_CURSOR_STYLE_CHANGED,
@@ -690,10 +647,10 @@ impl Platform for WindowsPlatform {
fn perform_dock_menu_action(&self, action: usize) {
unsafe {
- PostThreadMessageW(
- self.main_thread_id_win32,
+ PostMessageW(
+ Some(self.handle),
WM_GPUI_DOCK_MENU_ACTION,
- WPARAM(self.validation_number),
+ WPARAM(self.inner.validation_number),
LPARAM(action as isize),
)
.log_err();
@@ -709,15 +666,147 @@ impl Platform for WindowsPlatform {
}
}
+impl WindowsPlatformInner {
+ fn new(context: &mut PlatformWindowCreateContext) -> Result<Rc<Self>> {
+ let state = RefCell::new(WindowsPlatformState::new(
+ context.directx_devices.take().unwrap(),
+ ));
+ Ok(Rc::new(Self {
+ state,
+ raw_window_handles: context.raw_window_handles.clone(),
+ validation_number: context.validation_number,
+ main_receiver: context.main_receiver.take().unwrap(),
+ }))
+ }
+
+ fn handle_msg(
+ self: &Rc<Self>,
+ handle: HWND,
+ msg: u32,
+ wparam: WPARAM,
+ lparam: LPARAM,
+ ) -> LRESULT {
+ let handled = match msg {
+ WM_GPUI_CLOSE_ONE_WINDOW
+ | WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD
+ | WM_GPUI_DOCK_MENU_ACTION
+ | WM_GPUI_KEYBOARD_LAYOUT_CHANGED
+ | WM_GPUI_GPU_DEVICE_LOST => self.handle_gpui_events(msg, wparam, lparam),
+ _ => None,
+ };
+ if let Some(result) = handled {
+ LRESULT(result)
+ } else {
+ unsafe { DefWindowProcW(handle, msg, wparam, lparam) }
+ }
+ }
+
+ fn handle_gpui_events(&self, message: u32, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
+ if wparam.0 != self.validation_number {
+ log::error!("Wrong validation number while processing message: {message}");
+ return None;
+ }
+ match message {
+ WM_GPUI_CLOSE_ONE_WINDOW => {
+ if self.close_one_window(HWND(lparam.0 as _)) {
+ unsafe { PostQuitMessage(0) };
+ }
+ Some(0)
+ }
+ WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD => self.run_foreground_task(),
+ WM_GPUI_DOCK_MENU_ACTION => self.handle_dock_action_event(lparam.0 as _),
+ WM_GPUI_KEYBOARD_LAYOUT_CHANGED => self.handle_keyboard_layout_change(),
+ WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam),
+ _ => unreachable!(),
+ }
+ }
+
+ fn close_one_window(&self, target_window: HWND) -> bool {
+ let Some(all_windows) = self.raw_window_handles.upgrade() else {
+ log::error!("Failed to upgrade raw window handles");
+ return false;
+ };
+ let mut lock = all_windows.write();
+ let index = lock
+ .iter()
+ .position(|handle| handle.as_raw() == target_window)
+ .unwrap();
+ lock.remove(index);
+
+ lock.is_empty()
+ }
+
+ #[inline]
+ fn run_foreground_task(&self) -> Option<isize> {
+ for runnable in self.main_receiver.drain() {
+ runnable.run();
+ }
+ Some(0)
+ }
+
+ fn handle_dock_action_event(&self, action_idx: usize) -> Option<isize> {
+ let mut lock = self.state.borrow_mut();
+ let mut callback = lock.callbacks.app_menu_action.take()?;
+ let Some(action) = lock
+ .jump_list
+ .dock_menus
+ .get(action_idx)
+ .map(|dock_menu| dock_menu.action.boxed_clone())
+ else {
+ lock.callbacks.app_menu_action = Some(callback);
+ log::error!("Dock menu for index {action_idx} not found");
+ return Some(1);
+ };
+ drop(lock);
+ callback(&*action);
+ self.state.borrow_mut().callbacks.app_menu_action = Some(callback);
+ Some(0)
+ }
+
+ fn handle_keyboard_layout_change(&self) -> Option<isize> {
+ let mut callback = self
+ .state
+ .borrow_mut()
+ .callbacks
+ .keyboard_layout_change
+ .take()?;
+ callback();
+ self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
+ Some(0)
+ }
+
+ fn handle_device_lost(&self, lparam: LPARAM) -> Option<isize> {
+ let mut lock = self.state.borrow_mut();
+ let directx_devices = lparam.0 as *const DirectXDevices;
+ let directx_devices = unsafe { &*directx_devices };
+ unsafe {
+ ManuallyDrop::drop(&mut lock.directx_devices);
+ }
+ lock.directx_devices = ManuallyDrop::new(directx_devices.clone());
+
+ Some(0)
+ }
+}
+
impl Drop for WindowsPlatform {
fn drop(&mut self) {
unsafe {
- ManuallyDrop::drop(&mut self.bitmap_factory);
+ DestroyWindow(self.handle)
+ .context("Destroying platform window")
+ .log_err();
OleUninitialize();
}
}
}
+impl Drop for WindowsPlatformState {
+ fn drop(&mut self) {
+ unsafe {
+ ManuallyDrop::drop(&mut self.directx_devices);
+ }
+ }
+}
+
pub(crate) struct WindowCreationInfo {
pub(crate) icon: HICON,
pub(crate) executor: ForegroundExecutor,
@@ -726,43 +815,80 @@ pub(crate) struct WindowCreationInfo {
pub(crate) drop_target_helper: IDropTargetHelper,
pub(crate) validation_number: usize,
pub(crate) main_receiver: flume::Receiver<Runnable>,
- pub(crate) main_thread_id_win32: u32,
+ pub(crate) platform_window_handle: HWND,
pub(crate) disable_direct_composition: bool,
+ pub(crate) directx_devices: DirectXDevices,
}
-fn open_target(target: &str) {
- unsafe {
- let ret = ShellExecuteW(
+struct PlatformWindowCreateContext {
+ inner: Option<Result<Rc<WindowsPlatformInner>>>,
+ raw_window_handles: std::sync::Weak<RwLock<SmallVec<[SafeHwnd; 4]>>>,
+ validation_number: usize,
+ main_receiver: Option<flume::Receiver<Runnable>>,
+ directx_devices: Option<DirectXDevices>,
+}
+
+fn open_target(target: impl AsRef<OsStr>) -> Result<()> {
+ let target = target.as_ref();
+ let ret = unsafe {
+ ShellExecuteW(
None,
windows::core::w!("open"),
&HSTRING::from(target),
None,
None,
SW_SHOWDEFAULT,
- );
- if ret.0 as isize <= 32 {
- log::error!("Unable to open target: {}", std::io::Error::last_os_error());
- }
+ )
+ };
+ if ret.0 as isize <= 32 {
+ Err(anyhow::anyhow!(
+ "Unable to open target: {}",
+ std::io::Error::last_os_error()
+ ))
+ } else {
+ Ok(())
}
}
-fn open_target_in_explorer(target: &str) {
+fn open_target_in_explorer(target: &Path) -> Result<()> {
+ let dir = target.parent().context("No parent folder found")?;
+ let desktop = unsafe { SHGetDesktopFolder()? };
+
+ let mut dir_item = std::ptr::null_mut();
unsafe {
- let ret = ShellExecuteW(
+ desktop.ParseDisplayName(
+ HWND::default(),
None,
- windows::core::w!("open"),
- windows::core::w!("explorer.exe"),
- &HSTRING::from(format!("/select,{}", target).as_str()),
+ &HSTRING::from(dir),
None,
- SW_SHOWDEFAULT,
- );
- if ret.0 as isize <= 32 {
- log::error!(
- "Unable to open target in explorer: {}",
- std::io::Error::last_os_error()
- );
- }
+ &mut dir_item,
+ std::ptr::null_mut(),
+ )?;
}
+
+ let mut file_item = std::ptr::null_mut();
+ unsafe {
+ desktop.ParseDisplayName(
+ HWND::default(),
+ None,
+ &HSTRING::from(target),
+ None,
+ &mut file_item,
+ std::ptr::null_mut(),
+ )?;
+ }
+
+ let highlight = [file_item as *const _];
+ unsafe { SHOpenFolderAndSelectItems(dir_item as _, Some(&highlight), 0) }.or_else(|err| {
+ if err.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 {
+ // On some systems, the above call mysteriously fails with "file not
+ // found" even though the file is there. In these cases, ShellExecute()
+ // seems to work as a fallback (although it won't select the file).
+ open_target(dir).context("Opening target parent folder")
+ } else {
+ Err(anyhow::anyhow!("Can not open target path: {}", err))
+ }
+ })
}
fn file_open_dialog(
@@ -782,6 +908,12 @@ fn file_open_dialog(
unsafe {
folder_dialog.SetOptions(dialog_options)?;
+
+ if let Some(prompt) = options.prompt {
+ let prompt: &str = &prompt;
+ folder_dialog.SetOkButtonLabel(&HSTRING::from(prompt))?;
+ }
+
if folder_dialog.Show(window).is_err() {
// User cancelled
return Ok(None);
@@ -804,17 +936,26 @@ fn file_open_dialog(
Ok(Some(paths))
}
-fn file_save_dialog(directory: PathBuf, window: Option<HWND>) -> Result<Option<PathBuf>> {
+fn file_save_dialog(
+ directory: PathBuf,
+ suggested_name: Option<String>,
+ window: Option<HWND>,
+) -> Result<Option<PathBuf>> {
let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? };
- if !directory.to_string_lossy().is_empty() {
- if let Some(full_path) = directory.canonicalize().log_err() {
- let full_path = SanitizedPath::from(full_path);
- let full_path_string = full_path.to_string();
- let path_item: IShellItem =
- unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? };
- unsafe { dialog.SetFolder(&path_item).log_err() };
- }
+ if !directory.to_string_lossy().is_empty()
+ && let Some(full_path) = directory.canonicalize().log_err()
+ {
+ let full_path = SanitizedPath::new(&full_path);
+ let full_path_string = full_path.to_string();
+ let path_item: IShellItem =
+ unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? };
+ unsafe { dialog.SetFolder(&path_item).log_err() };
+ }
+
+ if let Some(suggested_name) = suggested_name {
+ unsafe { dialog.SetFileName(&HSTRING::from(suggested_name)).log_err() };
}
+
unsafe {
dialog.SetFileTypes(&[Common::COMDLG_FILTERSPEC {
pszName: windows::core::w!("All files"),
@@ -857,6 +998,135 @@ fn should_auto_hide_scrollbars() -> Result<bool> {
Ok(ui_settings.AutoHideScrollBars()?)
}
+fn check_device_lost(device: &ID3D11Device) -> bool {
+ let device_state = unsafe { device.GetDeviceRemovedReason() };
+ match device_state {
+ Ok(_) => false,
+ Err(err) => {
+ log::error!("DirectX device lost detected: {:?}", err);
+ true
+ }
+ }
+}
+
+fn handle_gpu_device_lost(
+ directx_devices: &mut DirectXDevices,
+ platform_window: HWND,
+ validation_number: usize,
+ all_windows: &std::sync::Weak<RwLock<SmallVec<[SafeHwnd; 4]>>>,
+ text_system: &std::sync::Weak<DirectWriteTextSystem>,
+) {
+ // Here we wait a bit to ensure the the system has time to recover from the device lost state.
+ // If we don't wait, the final drawing result will be blank.
+ std::thread::sleep(std::time::Duration::from_millis(350));
+
+ try_to_recover_from_device_lost(
+ || {
+ DirectXDevices::new()
+ .context("Failed to recreate new DirectX devices after device lost")
+ },
+ |new_devices| *directx_devices = new_devices,
+ || {
+ log::error!("Failed to recover DirectX devices after multiple attempts.");
+ // Do something here?
+ // At this point, the device loss is considered unrecoverable.
+ // std::process::exit(1);
+ },
+ );
+ log::info!("DirectX devices successfully recreated.");
+
+ unsafe {
+ SendMessageW(
+ platform_window,
+ WM_GPUI_GPU_DEVICE_LOST,
+ Some(WPARAM(validation_number)),
+ Some(LPARAM(directx_devices as *const _ as _)),
+ );
+ }
+
+ if let Some(text_system) = text_system.upgrade() {
+ text_system.handle_gpu_lost(&directx_devices);
+ }
+ if let Some(all_windows) = all_windows.upgrade() {
+ for window in all_windows.read().iter() {
+ unsafe {
+ SendMessageW(
+ window.as_raw(),
+ WM_GPUI_GPU_DEVICE_LOST,
+ Some(WPARAM(validation_number)),
+ Some(LPARAM(directx_devices as *const _ as _)),
+ );
+ }
+ }
+ std::thread::sleep(std::time::Duration::from_millis(200));
+ for window in all_windows.read().iter() {
+ unsafe {
+ SendMessageW(
+ window.as_raw(),
+ WM_GPUI_FORCE_UPDATE_WINDOW,
+ Some(WPARAM(validation_number)),
+ None,
+ );
+ }
+ }
+ }
+}
+
+const PLATFORM_WINDOW_CLASS_NAME: PCWSTR = w!("Zed::PlatformWindow");
+
+fn register_platform_window_class() {
+ let wc = WNDCLASSW {
+ lpfnWndProc: Some(window_procedure),
+ lpszClassName: PCWSTR(PLATFORM_WINDOW_CLASS_NAME.as_ptr()),
+ ..Default::default()
+ };
+ unsafe { RegisterClassW(&wc) };
+}
+
+unsafe extern "system" fn window_procedure(
+ hwnd: HWND,
+ msg: u32,
+ wparam: WPARAM,
+ lparam: LPARAM,
+) -> LRESULT {
+ if msg == WM_NCCREATE {
+ let params = lparam.0 as *const CREATESTRUCTW;
+ let params = unsafe { &*params };
+ let creation_context = params.lpCreateParams as *mut PlatformWindowCreateContext;
+ let creation_context = unsafe { &mut *creation_context };
+ return match WindowsPlatformInner::new(creation_context) {
+ Ok(inner) => {
+ let weak = Box::new(Rc::downgrade(&inner));
+ unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) };
+ creation_context.inner = Some(Ok(inner));
+ unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
+ }
+ Err(error) => {
+ creation_context.inner = Some(Err(error));
+ LRESULT(0)
+ }
+ };
+ }
+
+ let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsPlatformInner>;
+ if ptr.is_null() {
+ return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
+ }
+ let inner = unsafe { &*ptr };
+ let result = if let Some(inner) = inner.upgrade() {
+ inner.handle_msg(hwnd, msg, wparam, lparam)
+ } else {
+ unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
+ };
+
+ if msg == WM_NCDESTROY {
+ unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) };
+ unsafe { drop(Box::from_raw(ptr)) };
+ }
+
+ result
+}
+
#[cfg(test)]
mod tests {
use crate::{ClipboardItem, read_from_clipboard, write_to_clipboard};
@@ -1,6 +1,10 @@
+#include "alpha_correction.hlsl"
+
cbuffer GlobalParams: register(b0) {
+ float4 gamma_ratios;
float2 global_viewport_size;
- uint2 _pad;
+ float grayscale_enhanced_contrast;
+ uint _pad;
};
Texture2D<float4> t_sprite: register(t0);
@@ -1098,7 +1102,8 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI
float4 monochrome_sprite_fragment(MonochromeSpriteFragmentInput input): SV_Target {
float sample = t_sprite.Sample(s_sprite, input.tile_position).r;
- return float4(input.color.rgb, input.color.a * sample);
+ float alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, grayscale_enhanced_contrast, gamma_ratios);
+ return float4(input.color.rgb, input.color.a * alpha_corrected);
}
/*
@@ -94,7 +94,7 @@ impl VSyncProvider {
// DwmFlush and DCompositionWaitForCompositorClock returns very early
// instead of waiting until vblank when the monitor goes to sleep or is
// unplugged (nothing to present due to desktop occlusion). We use 1ms as
- // a threshhold for the duration of the wait functions and fallback to
+ // a threshold for the duration of the wait functions and fallback to
// Sleep() if it returns before that. This could happen during normal
// operation for the first call after the vsync thread becomes non-idle,
// but it shouldn't happen often.
@@ -73,12 +73,13 @@ pub(crate) struct WindowsWindowInner {
pub(crate) windows_version: WindowsVersion,
pub(crate) validation_number: usize,
pub(crate) main_receiver: flume::Receiver<Runnable>,
- pub(crate) main_thread_id_win32: u32,
+ pub(crate) platform_window_handle: HWND,
}
impl WindowsWindowState {
fn new(
hwnd: HWND,
+ directx_devices: &DirectXDevices,
window_params: &CREATESTRUCTW,
current_cursor: Option<HCURSOR>,
display: WindowsDisplay,
@@ -104,7 +105,7 @@ impl WindowsWindowState {
};
let border_offset = WindowBorderOffset::default();
let restore_from_minimized = None;
- let renderer = DirectXRenderer::new(hwnd, disable_direct_composition)
+ let renderer = DirectXRenderer::new(hwnd, directx_devices, disable_direct_composition)
.context("Creating DirectX renderer")?;
let callbacks = Callbacks::default();
let input_handler = None;
@@ -205,9 +206,10 @@ impl WindowsWindowState {
}
impl WindowsWindowInner {
- fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result<Rc<Self>> {
+ fn new(context: &mut WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result<Rc<Self>> {
let state = RefCell::new(WindowsWindowState::new(
hwnd,
+ &context.directx_devices,
cs,
context.current_cursor,
context.display,
@@ -228,7 +230,7 @@ impl WindowsWindowInner {
windows_version: context.windows_version,
validation_number: context.validation_number,
main_receiver: context.main_receiver.clone(),
- main_thread_id_win32: context.main_thread_id_win32,
+ platform_window_handle: context.platform_window_handle,
}))
}
@@ -342,9 +344,10 @@ struct WindowCreateContext {
drop_target_helper: IDropTargetHelper,
validation_number: usize,
main_receiver: flume::Receiver<Runnable>,
- main_thread_id_win32: u32,
+ platform_window_handle: HWND,
appearance: WindowAppearance,
disable_direct_composition: bool,
+ directx_devices: DirectXDevices,
}
impl WindowsWindow {
@@ -361,8 +364,9 @@ impl WindowsWindow {
drop_target_helper,
validation_number,
main_receiver,
- main_thread_id_win32,
+ platform_window_handle,
disable_direct_composition,
+ directx_devices,
} = creation_info;
register_window_class(icon);
let hide_title_bar = params
@@ -382,10 +386,17 @@ impl WindowsWindow {
let (mut dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp {
(WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0))
} else {
- (
- WS_EX_APPWINDOW,
- WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
- )
+ let mut dwstyle = WS_SYSMENU;
+
+ if params.is_resizable {
+ dwstyle |= WS_THICKFRAME | WS_MAXIMIZEBOX;
+ }
+
+ if params.is_minimizable {
+ dwstyle |= WS_MINIMIZEBOX;
+ }
+
+ (WS_EX_APPWINDOW, dwstyle)
};
if !disable_direct_composition {
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
@@ -412,9 +423,10 @@ impl WindowsWindow {
drop_target_helper,
validation_number,
main_receiver,
- main_thread_id_win32,
+ platform_window_handle,
appearance,
disable_direct_composition,
+ directx_devices,
};
let creation_result = unsafe {
CreateWindowExW(
@@ -592,10 +604,7 @@ impl PlatformWindow for WindowsWindow {
) -> Option<Receiver<usize>> {
let (done_tx, done_rx) = oneshot::channel();
let msg = msg.to_string();
- let detail_string = match detail {
- Some(info) => Some(info.to_string()),
- None => None,
- };
+ let detail_string = detail.map(|detail| detail.to_string());
let handle = self.0.hwnd;
let answers = answers.to_vec();
self.0
@@ -830,7 +839,7 @@ impl PlatformWindow for WindowsWindow {
self.0.state.borrow().renderer.gpu_specs().log_err()
}
- fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {
+ fn update_ime_position(&self, _bounds: Bounds<Pixels>) {
// There is no such thing on Windows.
}
}
@@ -23,6 +23,11 @@ impl SharedString {
pub fn new(str: impl Into<Arc<str>>) -> Self {
SharedString(ArcCow::Owned(str.into()))
}
+
+ /// Get a &str from the underlying string.
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
}
impl JsonSchema for SharedString {
@@ -103,7 +108,7 @@ impl From<SharedString> for Arc<str> {
fn from(val: SharedString) -> Self {
match val.0 {
ArcCow::Borrowed(borrowed) => Arc::from(borrowed),
- ArcCow::Owned(owned) => owned.clone(),
+ ArcCow::Owned(owned) => owned,
}
}
}
@@ -573,7 +573,7 @@ impl Style {
if self
.border_color
- .map_or(false, |color| !color.is_transparent())
+ .is_some_and(|color| !color.is_transparent())
{
min.x += self.border_widths.left.to_pixels(rem_size);
max.x -= self.border_widths.right.to_pixels(rem_size);
@@ -633,7 +633,7 @@ impl Style {
window.paint_shadows(bounds, corner_radii, &self.box_shadow);
let background_color = self.background.as_ref().and_then(Fill::color);
- if background_color.map_or(false, |color| !color.is_transparent()) {
+ if background_color.is_some_and(|color| !color.is_transparent()) {
let mut border_color = match background_color {
Some(color) => match color.tag {
BackgroundTag::Solid => color.solid,
@@ -729,7 +729,7 @@ impl Style {
fn is_border_visible(&self) -> bool {
self.border_color
- .map_or(false, |color| !color.is_transparent())
+ .is_some_and(|color| !color.is_transparent())
&& self.border_widths.any(|length| !length.is_zero())
}
}
@@ -886,43 +886,32 @@ impl HighlightStyle {
}
/// Blend this highlight style with another.
/// Non-continuous properties, like font_weight and font_style, are overwritten.
- pub fn highlight(&mut self, other: HighlightStyle) {
- match (self.color, other.color) {
- (Some(self_color), Some(other_color)) => {
- self.color = Some(Hsla::blend(other_color, self_color));
- }
- (None, Some(other_color)) => {
- self.color = Some(other_color);
- }
- _ => {}
- }
-
- if other.font_weight.is_some() {
- self.font_weight = other.font_weight;
- }
-
- if other.font_style.is_some() {
- self.font_style = other.font_style;
- }
-
- if other.background_color.is_some() {
- self.background_color = other.background_color;
- }
-
- if other.underline.is_some() {
- self.underline = other.underline;
- }
-
- if other.strikethrough.is_some() {
- self.strikethrough = other.strikethrough;
- }
-
- match (other.fade_out, self.fade_out) {
- (Some(source_fade), None) => self.fade_out = Some(source_fade),
- (Some(source_fade), Some(dest_fade)) => {
- self.fade_out = Some((dest_fade * (1. + source_fade)).clamp(0., 1.));
- }
- _ => {}
+ #[must_use]
+ pub fn highlight(self, other: HighlightStyle) -> Self {
+ Self {
+ color: other
+ .color
+ .map(|other_color| {
+ if let Some(color) = self.color {
+ color.blend(other_color)
+ } else {
+ other_color
+ }
+ })
+ .or(self.color),
+ font_weight: other.font_weight.or(self.font_weight),
+ font_style: other.font_style.or(self.font_style),
+ background_color: other.background_color.or(self.background_color),
+ underline: other.underline.or(self.underline),
+ strikethrough: other.strikethrough.or(self.strikethrough),
+ fade_out: other
+ .fade_out
+ .map(|source_fade| {
+ self.fade_out
+ .map(|dest_fade| (dest_fade * (1. + source_fade)).clamp(0., 1.))
+ .unwrap_or(source_fade)
+ })
+ .or(self.fade_out),
}
}
}
@@ -987,10 +976,11 @@ pub fn combine_highlights(
while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() {
let prev_index = mem::replace(&mut ix, *endpoint_ix);
if ix > prev_index && !active_styles.is_empty() {
- let mut current_style = HighlightStyle::default();
- for highlight_id in &active_styles {
- current_style.highlight(highlights[*highlight_id]);
- }
+ let current_style = active_styles
+ .iter()
+ .fold(HighlightStyle::default(), |acc, highlight_id| {
+ acc.highlight(highlights[*highlight_id])
+ });
return Some((prev_index..ix, current_style));
}
@@ -1306,10 +1296,95 @@ impl From<Position> for taffy::style::Position {
#[cfg(test)]
mod tests {
- use crate::{blue, green, red, yellow};
+ use crate::{blue, green, px, red, yellow};
use super::*;
+ #[test]
+ fn test_basic_highlight_style_combination() {
+ let style_a = HighlightStyle::default();
+ let style_b = HighlightStyle::default();
+ let style_a = style_a.highlight(style_b);
+ assert_eq!(
+ style_a,
+ HighlightStyle::default(),
+ "Combining empty styles should not produce a non-empty style."
+ );
+
+ let mut style_b = HighlightStyle {
+ color: Some(red()),
+ strikethrough: Some(StrikethroughStyle {
+ thickness: px(2.),
+ color: Some(blue()),
+ }),
+ fade_out: Some(0.),
+ font_style: Some(FontStyle::Italic),
+ font_weight: Some(FontWeight(300.)),
+ background_color: Some(yellow()),
+ underline: Some(UnderlineStyle {
+ thickness: px(2.),
+ color: Some(red()),
+ wavy: true,
+ }),
+ };
+ let expected_style = style_b;
+
+ let style_a = style_a.highlight(style_b);
+ assert_eq!(
+ style_a, expected_style,
+ "Blending an empty style with another style should return the other style"
+ );
+
+ let style_b = style_b.highlight(Default::default());
+ assert_eq!(
+ style_b, expected_style,
+ "Blending a style with an empty style should not change the style."
+ );
+
+ let mut style_c = expected_style;
+
+ let style_d = HighlightStyle {
+ color: Some(blue().alpha(0.7)),
+ strikethrough: Some(StrikethroughStyle {
+ thickness: px(4.),
+ color: Some(crate::red()),
+ }),
+ fade_out: Some(0.),
+ font_style: Some(FontStyle::Oblique),
+ font_weight: Some(FontWeight(800.)),
+ background_color: Some(green()),
+ underline: Some(UnderlineStyle {
+ thickness: px(4.),
+ color: None,
+ wavy: false,
+ }),
+ };
+
+ let expected_style = HighlightStyle {
+ color: Some(red().blend(blue().alpha(0.7))),
+ strikethrough: Some(StrikethroughStyle {
+ thickness: px(4.),
+ color: Some(red()),
+ }),
+ // TODO this does not seem right
+ fade_out: Some(0.),
+ font_style: Some(FontStyle::Oblique),
+ font_weight: Some(FontWeight(800.)),
+ background_color: Some(green()),
+ underline: Some(UnderlineStyle {
+ thickness: px(4.),
+ color: None,
+ wavy: false,
+ }),
+ };
+
+ let style_c = style_c.highlight(style_d);
+ assert_eq!(
+ style_c, expected_style,
+ "Blending styles should blend properties where possible and override all others"
+ );
+ }
+
#[test]
fn test_combine_highlights() {
assert_eq!(
@@ -1337,14 +1412,14 @@ mod tests {
(
1..2,
HighlightStyle {
- color: Some(green()),
+ color: Some(blue()),
..Default::default()
}
),
(
2..3,
HighlightStyle {
- color: Some(green()),
+ color: Some(blue()),
font_style: Some(FontStyle::Italic),
..Default::default()
}
@@ -201,3 +201,9 @@ impl Drop for Subscription {
}
}
}
+
+impl std::fmt::Debug for Subscription {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Subscription").finish()
+ }
+}
@@ -45,27 +45,18 @@ impl TabHandles {
})
.unwrap_or_default();
- if let Some(next_handle) = self.handles.get(next_ix) {
- Some(next_handle.clone())
- } else {
- None
- }
+ self.handles.get(next_ix).cloned()
}
pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
let ix = self.current_index(focused_id).unwrap_or_default();
- let prev_ix;
- if ix == 0 {
- prev_ix = self.handles.len().saturating_sub(1);
+ let prev_ix = if ix == 0 {
+ self.handles.len().saturating_sub(1)
} else {
- prev_ix = ix.saturating_sub(1);
- }
+ ix.saturating_sub(1)
+ };
- if let Some(prev_handle) = self.handles.get(prev_ix) {
- Some(prev_handle.clone())
- } else {
- None
- }
+ self.handles.get(prev_ix).cloned()
}
}
@@ -90,7 +81,7 @@ mod tests {
];
for handle in focus_handles.iter() {
- tab.insert(&handle);
+ tab.insert(handle);
}
assert_eq!(
tab.handles
@@ -3,6 +3,7 @@ use crate::{
};
use collections::{FxHashMap, FxHashSet};
use smallvec::SmallVec;
+use stacksafe::{StackSafe, stacksafe};
use std::{fmt::Debug, ops::Range};
use taffy::{
TaffyTree, TraversePartialTree as _,
@@ -11,8 +12,15 @@ use taffy::{
tree::NodeId,
};
-type NodeMeasureFn = Box<
- dyn FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut Window, &mut App) -> Size<Pixels>,
+type NodeMeasureFn = StackSafe<
+ Box<
+ dyn FnMut(
+ Size<Option<Pixels>>,
+ Size<AvailableSpace>,
+ &mut Window,
+ &mut App,
+ ) -> Size<Pixels>,
+ >,
>;
struct NodeContext {
@@ -50,23 +58,21 @@ impl TaffyLayoutEngine {
children: &[LayoutId],
) -> LayoutId {
let taffy_style = style.to_taffy(rem_size);
- let layout_id = if children.is_empty() {
+
+ if children.is_empty() {
self.taffy
.new_leaf(taffy_style)
.expect(EXPECT_MESSAGE)
.into()
} else {
- let parent_id = self
- .taffy
+ self.taffy
// This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId.
.new_with_children(taffy_style, unsafe {
std::mem::transmute::<&[LayoutId], &[taffy::NodeId]>(children)
})
.expect(EXPECT_MESSAGE)
- .into();
- parent_id
- };
- layout_id
+ .into()
+ }
}
pub fn request_measured_layout(
@@ -83,17 +89,15 @@ impl TaffyLayoutEngine {
) -> LayoutId {
let taffy_style = style.to_taffy(rem_size);
- let layout_id = self
- .taffy
+ self.taffy
.new_leaf_with_context(
taffy_style,
NodeContext {
- measure: Box::new(measure),
+ measure: StackSafe::new(Box::new(measure)),
},
)
.expect(EXPECT_MESSAGE)
- .into();
- layout_id
+ .into()
}
// Used to understand performance
@@ -143,6 +147,7 @@ impl TaffyLayoutEngine {
Ok(edges)
}
+ #[stacksafe]
pub fn compute_layout(
&mut self,
id: LayoutId,
@@ -159,7 +164,6 @@ impl TaffyLayoutEngine {
// for (a, b) in self.get_edges(id)? {
// println!("N{} --> N{}", u64::from(a), u64::from(b));
// }
- // println!("");
//
if !self.computed_layouts.insert(id) {
@@ -64,6 +64,9 @@ pub fn run_test(
if attempt < max_retries {
println!("attempt {} failed, retrying", attempt);
attempt += 1;
+ // The panic payload might itself trigger an unwind on drop:
+ // https://doc.rust-lang.org/std/panic/fn.catch_unwind.html#notes
+ std::mem::forget(error);
} else {
if is_multiple_runs {
eprintln!("failing seed: {}", seed);
@@ -351,7 +351,7 @@ impl WindowTextSystem {
///
/// Note that this method can only shape a single line of text. It will panic
/// if the text contains newlines. If you need to shape multiple lines of text,
- /// use `TextLayout::shape_text` instead.
+ /// use [`Self::shape_text`] instead.
pub fn shape_line(
&self,
text: SharedString,
@@ -366,15 +366,14 @@ impl WindowTextSystem {
let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
for run in runs {
- if let Some(last_run) = decoration_runs.last_mut() {
- if last_run.color == run.color
- && last_run.underline == run.underline
- && last_run.strikethrough == run.strikethrough
- && last_run.background_color == run.background_color
- {
- last_run.len += run.len as u32;
- continue;
- }
+ if let Some(last_run) = decoration_runs.last_mut()
+ && last_run.color == run.color
+ && last_run.underline == run.underline
+ && last_run.strikethrough == run.strikethrough
+ && last_run.background_color == run.background_color
+ {
+ last_run.len += run.len as u32;
+ continue;
}
decoration_runs.push(DecorationRun {
len: run.len as u32,
@@ -436,7 +435,7 @@ impl WindowTextSystem {
});
}
- if decoration_runs.last().map_or(false, |last_run| {
+ if decoration_runs.last().is_some_and(|last_run| {
last_run.color == run.color
&& last_run.underline == run.underline
&& last_run.strikethrough == run.strikethrough
@@ -492,14 +491,14 @@ impl WindowTextSystem {
let mut split_lines = text.split('\n');
let mut processed = false;
- if let Some(first_line) = split_lines.next() {
- if let Some(second_line) = split_lines.next() {
- processed = true;
- process_line(first_line.to_string().into());
- process_line(second_line.to_string().into());
- for line_text in split_lines {
- process_line(line_text.to_string().into());
- }
+ if let Some(first_line) = split_lines.next()
+ && let Some(second_line) = split_lines.next()
+ {
+ processed = true;
+ process_line(first_line.to_string().into());
+ process_line(second_line.to_string().into());
+ for line_text in split_lines {
+ process_line(line_text.to_string().into());
}
}
@@ -518,7 +517,7 @@ impl WindowTextSystem {
/// Layout the given line of text, at the given font_size.
/// Subsets of the line can be styled independently with the `runs` parameter.
- /// Generally, you should prefer to use `TextLayout::shape_line` instead, which
+ /// Generally, you should prefer to use [`Self::shape_line`] instead, which
/// can be painted directly.
pub fn layout_line<Text>(
&self,
@@ -534,11 +533,11 @@ impl WindowTextSystem {
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
for run in runs.iter() {
let font_id = self.resolve_font(&run.font);
- if let Some(last_run) = font_runs.last_mut() {
- if last_run.font_id == font_id {
- last_run.len += run.len;
- continue;
- }
+ if let Some(last_run) = font_runs.last_mut()
+ && last_run.font_id == font_id
+ {
+ last_run.len += run.len;
+ continue;
}
font_runs.push(FontRun {
len: run.len,
@@ -669,7 +668,7 @@ impl Display for FontStyle {
}
}
-/// A styled run of text, for use in [`TextLayout`].
+/// A styled run of text, for use in [`crate::TextLayout`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TextRun {
/// A number of utf8 bytes
@@ -695,7 +694,7 @@ impl TextRun {
}
}
-/// An identifier for a specific glyph, as returned by [`TextSystem::layout_line`].
+/// An identifier for a specific glyph, as returned by [`WindowTextSystem::layout_line`].
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[repr(C)]
pub struct GlyphId(pub(crate) u32);
@@ -292,10 +292,10 @@ fn paint_line(
}
if let Some(style_run) = style_run {
- if let Some((_, underline_style)) = &mut current_underline {
- if style_run.underline.as_ref() != Some(underline_style) {
- finished_underline = current_underline.take();
- }
+ if let Some((_, underline_style)) = &mut current_underline
+ && style_run.underline.as_ref() != Some(underline_style)
+ {
+ finished_underline = current_underline.take();
}
if let Some(run_underline) = style_run.underline.as_ref() {
current_underline.get_or_insert((
@@ -310,10 +310,10 @@ fn paint_line(
},
));
}
- if let Some((_, strikethrough_style)) = &mut current_strikethrough {
- if style_run.strikethrough.as_ref() != Some(strikethrough_style) {
- finished_strikethrough = current_strikethrough.take();
- }
+ if let Some((_, strikethrough_style)) = &mut current_strikethrough
+ && style_run.strikethrough.as_ref() != Some(strikethrough_style)
+ {
+ finished_strikethrough = current_strikethrough.take();
}
if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
current_strikethrough.get_or_insert((
@@ -509,10 +509,10 @@ fn paint_line_background(
}
if let Some(style_run) = style_run {
- if let Some((_, background_color)) = &mut current_background {
- if style_run.background_color.as_ref() != Some(background_color) {
- finished_background = current_background.take();
- }
+ if let Some((_, background_color)) = &mut current_background
+ && style_run.background_color.as_ref() != Some(background_color)
+ {
+ finished_background = current_background.take();
}
if let Some(run_background) = style_run.background_color {
current_background.get_or_insert((
@@ -185,10 +185,10 @@ impl LineLayout {
if width > wrap_width && boundary > last_boundary {
// When used line_clamp, we should limit the number of lines.
- if let Some(max_lines) = max_lines {
- if boundaries.len() >= max_lines - 1 {
- break;
- }
+ if let Some(max_lines) = max_lines
+ && boundaries.len() >= max_lines - 1
+ {
+ break;
}
if let Some(last_candidate_ix) = last_candidate_ix.take() {
@@ -44,7 +44,7 @@ impl LineWrapper {
let mut prev_c = '\0';
let mut index = 0;
let mut candidates = fragments
- .into_iter()
+ .iter()
.flat_map(move |fragment| fragment.wrap_boundary_candidates())
.peekable();
iter::from_fn(move || {
@@ -181,7 +181,7 @@ impl LineWrapper {
matches!(c, '\u{0400}'..='\u{04FF}') ||
// Some other known special characters that should be treated as word characters,
// e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`, `2^3`, `a~b`, etc.
- matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~' | ',') ||
+ matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '!' | ';' | '*') ||
// Characters that used in URL, e.g. `https://github.com/zed-industries/zed?a=1&b=2` for better wrapping a long URL.
matches!(c, '/' | ':' | '?' | '&' | '=') ||
// `⋯` character is special used in Zed, to keep this at the end of the line.
@@ -1,13 +1,11 @@
-use std::sync::atomic::AtomicUsize;
-use std::sync::atomic::Ordering::SeqCst;
-#[cfg(any(test, feature = "test-support"))]
-use std::time::Duration;
-
-#[cfg(any(test, feature = "test-support"))]
-use futures::Future;
-
-#[cfg(any(test, feature = "test-support"))]
-use smol::future::FutureExt;
+use crate::{BackgroundExecutor, Task};
+use std::{
+ future::Future,
+ pin::Pin,
+ sync::atomic::{AtomicUsize, Ordering::SeqCst},
+ task,
+ time::Duration,
+};
pub use util::*;
@@ -60,18 +58,63 @@ pub trait FluentBuilder {
where
Self: Sized,
{
- self.map(|this| {
- if let Some(_) = option {
- this
- } else {
- then(this)
- }
- })
+ self.map(|this| if option.is_some() { this } else { then(this) })
+ }
+}
+
+/// Extensions for Future types that provide additional combinators and utilities.
+pub trait FutureExt {
+ /// Requires a Future to complete before the specified duration has elapsed.
+ /// Similar to tokio::timeout.
+ fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout<Self>
+ where
+ Self: Sized;
+}
+
+impl<T: Future> FutureExt for T {
+ fn with_timeout(self, timeout: Duration, executor: &BackgroundExecutor) -> WithTimeout<Self>
+ where
+ Self: Sized,
+ {
+ WithTimeout {
+ future: self,
+ timer: executor.timer(timeout),
+ }
+ }
+}
+
+pub struct WithTimeout<T> {
+ future: T,
+ timer: Task<()>,
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("Timed out before future resolved")]
+/// Error returned by with_timeout when the timeout duration elapsed before the future resolved
+pub struct Timeout;
+
+impl<T: Future> Future for WithTimeout<T> {
+ type Output = Result<T::Output, Timeout>;
+
+ fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll<Self::Output> {
+ // SAFETY: the fields of Timeout are private and we never move the future ourselves
+ // And its already pinned since we are being polled (all futures need to be pinned to be polled)
+ let this = unsafe { &raw mut *self.get_unchecked_mut() };
+ let future = unsafe { Pin::new_unchecked(&mut (*this).future) };
+ let timer = unsafe { Pin::new_unchecked(&mut (*this).timer) };
+
+ if let task::Poll::Ready(output) = future.poll(cx) {
+ task::Poll::Ready(Ok(output))
+ } else if timer.poll(cx).is_ready() {
+ task::Poll::Ready(Err(Timeout))
+ } else {
+ task::Poll::Pending
+ }
}
}
#[cfg(any(test, feature = "test-support"))]
-pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
+pub async fn smol_timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
where
F: Future<Output = T>,
{
@@ -80,7 +123,7 @@ where
Err(())
};
let future = async move { Ok(f.await) };
- timer.race(future).await
+ smol::future::FutureExt::race(timer, future).await
}
/// Increment the given atomic counter if it is not zero.
@@ -205,22 +205,21 @@ impl Element for AnyView {
let content_mask = window.content_mask();
let text_style = window.text_style();
- if let Some(mut element_state) = element_state {
- if element_state.cache_key.bounds == bounds
- && element_state.cache_key.content_mask == content_mask
- && element_state.cache_key.text_style == text_style
- && !window.dirty_views.contains(&self.entity_id())
- && !window.refreshing
- {
- let prepaint_start = window.prepaint_index();
- window.reuse_prepaint(element_state.prepaint_range.clone());
- cx.entities
- .extend_accessed(&element_state.accessed_entities);
- let prepaint_end = window.prepaint_index();
- element_state.prepaint_range = prepaint_start..prepaint_end;
-
- return (None, element_state);
- }
+ if let Some(mut element_state) = element_state
+ && element_state.cache_key.bounds == bounds
+ && element_state.cache_key.content_mask == content_mask
+ && element_state.cache_key.text_style == text_style
+ && !window.dirty_views.contains(&self.entity_id())
+ && !window.refreshing
+ {
+ let prepaint_start = window.prepaint_index();
+ window.reuse_prepaint(element_state.prepaint_range.clone());
+ cx.entities
+ .extend_accessed(&element_state.accessed_entities);
+ let prepaint_end = window.prepaint_index();
+ element_state.prepaint_range = prepaint_start..prepaint_end;
+
+ return (None, element_state);
}
let refreshing = mem::replace(&mut window.refreshing, true);
@@ -12,11 +12,11 @@ use crate::{
PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad,
Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge,
SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size,
- StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task,
- TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle,
- WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations,
- WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size,
- transparent_black,
+ StrikethroughStyle, Style, SubscriberSet, Subscription, SystemWindowTab,
+ SystemWindowTabController, TabHandles, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
+ TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
+ WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
+ point, prelude::*, px, rems, size, transparent_black,
};
use anyhow::{Context as _, Result, anyhow};
use collections::{FxHashMap, FxHashSet};
@@ -243,14 +243,14 @@ impl FocusId {
pub fn contains_focused(&self, window: &Window, cx: &App) -> bool {
window
.focused(cx)
- .map_or(false, |focused| self.contains(focused.id, window))
+ .is_some_and(|focused| self.contains(focused.id, window))
}
/// Obtains whether the element associated with this handle is contained within the
/// focused element or is itself focused.
pub fn within_focused(&self, window: &Window, cx: &App) -> bool {
let focused = window.focused(cx);
- focused.map_or(false, |focused| focused.id.contains(*self, window))
+ focused.is_some_and(|focused| focused.id.contains(*self, window))
}
/// Obtains whether this handle contains the given handle in the most recently rendered frame.
@@ -504,7 +504,7 @@ impl HitboxId {
return true;
}
}
- return false;
+ false
}
/// Checks if the hitbox with this ID contains the mouse and should handle scroll events.
@@ -585,7 +585,7 @@ pub enum HitboxBehavior {
/// if phase == DispatchPhase::Capture && hitbox.is_hovered(window) {
/// cx.stop_propagation();
/// }
- /// }
+ /// })
/// ```
///
/// This has effects beyond event handling - any use of hitbox checking, such as hover
@@ -605,11 +605,11 @@ pub enum HitboxBehavior {
/// bubble-phase handler for every mouse event type **except** `ScrollWheelEvent`:
///
/// ```
- /// window.on_mouse_event(move |_: &EveryMouseEventTypeExceptScroll, phase, window, _cx| {
+ /// window.on_mouse_event(move |_: &EveryMouseEventTypeExceptScroll, phase, window, cx| {
/// if phase == DispatchPhase::Bubble && hitbox.should_handle_scroll(window) {
/// cx.stop_propagation();
/// }
- /// }
+ /// })
/// ```
///
/// See the documentation of [`Hitbox::is_hovered`] for details of why `ScrollWheelEvent` is
@@ -634,7 +634,7 @@ impl TooltipId {
window
.tooltip_bounds
.as_ref()
- .map_or(false, |tooltip_bounds| {
+ .is_some_and(|tooltip_bounds| {
tooltip_bounds.id == *self
&& tooltip_bounds.bounds.contains(&window.mouse_position())
})
@@ -939,11 +939,15 @@ impl Window {
show,
kind,
is_movable,
+ is_resizable,
+ is_minimizable,
display_id,
window_background,
app_id,
window_min_size,
window_decorations,
+ #[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
+ tabbing_identifier,
} = options;
let bounds = window_bounds
@@ -956,12 +960,23 @@ impl Window {
titlebar,
kind,
is_movable,
+ is_resizable,
+ is_minimizable,
focus,
show,
display_id,
window_min_size,
+ #[cfg(target_os = "macos")]
+ tabbing_identifier,
},
)?;
+
+ let tab_bar_visible = platform_window.tab_bar_visible();
+ SystemWindowTabController::init_visible(cx, tab_bar_visible);
+ if let Some(tabs) = platform_window.tabbed_windows() {
+ SystemWindowTabController::add_tab(cx, handle.window_id(), tabs);
+ }
+
let display_id = platform_window.display().map(|display| display.id());
let sprite_atlas = platform_window.sprite_atlas();
let mouse_position = platform_window.mouse_position();
@@ -991,9 +1006,13 @@ impl Window {
}
platform_window.on_close(Box::new({
+ let window_id = handle.window_id();
let mut cx = cx.to_async();
move || {
let _ = handle.update(&mut cx, |_, window, _| window.remove_window());
+ let _ = cx.update(|cx| {
+ SystemWindowTabController::remove_tab(cx, window_id);
+ });
}
}));
platform_window.on_request_frame(Box::new({
@@ -1082,7 +1101,11 @@ impl Window {
.activation_observers
.clone()
.retain(&(), |callback| callback(window, cx));
+
+ window.bounds_changed(cx);
window.refresh();
+
+ SystemWindowTabController::update_last_active(cx, window.handle.id);
})
.log_err();
}
@@ -1123,6 +1146,57 @@ impl Window {
.unwrap_or(None)
})
});
+ platform_window.on_move_tab_to_new_window({
+ let mut cx = cx.to_async();
+ Box::new(move || {
+ handle
+ .update(&mut cx, |_, _window, cx| {
+ SystemWindowTabController::move_tab_to_new_window(cx, handle.window_id());
+ })
+ .log_err();
+ })
+ });
+ platform_window.on_merge_all_windows({
+ let mut cx = cx.to_async();
+ Box::new(move || {
+ handle
+ .update(&mut cx, |_, _window, cx| {
+ SystemWindowTabController::merge_all_windows(cx, handle.window_id());
+ })
+ .log_err();
+ })
+ });
+ platform_window.on_select_next_tab({
+ let mut cx = cx.to_async();
+ Box::new(move || {
+ handle
+ .update(&mut cx, |_, _window, cx| {
+ SystemWindowTabController::select_next_tab(cx, handle.window_id());
+ })
+ .log_err();
+ })
+ });
+ platform_window.on_select_previous_tab({
+ let mut cx = cx.to_async();
+ Box::new(move || {
+ handle
+ .update(&mut cx, |_, _window, cx| {
+ SystemWindowTabController::select_previous_tab(cx, handle.window_id())
+ })
+ .log_err();
+ })
+ });
+ platform_window.on_toggle_tab_bar({
+ let mut cx = cx.to_async();
+ Box::new(move || {
+ handle
+ .update(&mut cx, |_, window, cx| {
+ let tab_bar_visible = window.platform_window.tab_bar_visible();
+ SystemWindowTabController::set_visible(cx, tab_bar_visible);
+ })
+ .log_err();
+ })
+ });
if let Some(app_id) = app_id {
platform_window.set_app_id(&app_id);
@@ -1835,7 +1909,7 @@ impl Window {
}
/// Produces a new frame and assigns it to `rendered_frame`. To actually show
- /// the contents of the new [Scene], use [present].
+ /// the contents of the new [`Scene`], use [`Self::present`].
#[profiling::function]
pub fn draw(&mut self, cx: &mut App) -> ArenaClearNeeded {
self.invalidate_entities();
@@ -2377,7 +2451,7 @@ impl Window {
/// Perform prepaint on child elements in a "retryable" manner, so that any side effects
/// of prepaints can be discarded before prepainting again. This is used to support autoscroll
/// where we need to prepaint children to detect the autoscroll bounds, then adjust the
- /// element offset and prepaint again. See [`List`] for an example. This method should only be
+ /// element offset and prepaint again. See [`crate::List`] for an example. This method should only be
/// called during the prepaint phase of element drawing.
pub fn transact<T, U>(&mut self, f: impl FnOnce(&mut Self) -> Result<T, U>) -> Result<T, U> {
self.invalidator.debug_assert_prepaint();
@@ -2402,9 +2476,9 @@ impl Window {
result
}
- /// When you call this method during [`prepaint`], containing elements will attempt to
+ /// When you call this method during [`Element::prepaint`], containing elements will attempt to
/// scroll to cause the specified bounds to become visible. When they decide to autoscroll, they will call
- /// [`prepaint`] again with a new set of bounds. See [`List`] for an example of an element
+ /// [`Element::prepaint`] again with a new set of bounds. See [`crate::List`] for an example of an element
/// that supports this method being called on the elements it contains. This method should only be
/// called during the prepaint phase of element drawing.
pub fn request_autoscroll(&mut self, bounds: Bounds<Pixels>) {
@@ -2412,8 +2486,8 @@ impl Window {
self.requested_autoscroll = Some(bounds);
}
- /// This method can be called from a containing element such as [`List`] to support the autoscroll behavior
- /// described in [`request_autoscroll`].
+ /// This method can be called from a containing element such as [`crate::List`] to support the autoscroll behavior
+ /// described in [`Self::request_autoscroll`].
pub fn take_autoscroll(&mut self) -> Option<Bounds<Pixels>> {
self.invalidator.debug_assert_prepaint();
self.requested_autoscroll.take()
@@ -2453,7 +2527,7 @@ impl Window {
/// time.
pub fn get_asset<A: Asset>(&mut self, source: &A::Source, cx: &mut App) -> Option<A::Output> {
let (task, _) = cx.fetch_asset::<A>(source);
- task.clone().now_or_never()
+ task.now_or_never()
}
/// Obtain the current element offset. This method should only be called during the
/// prepaint phase of element drawing.
@@ -2504,7 +2578,7 @@ impl Window {
&mut self,
key: impl Into<ElementId>,
cx: &mut App,
- init: impl FnOnce(&mut Self, &mut App) -> S,
+ init: impl FnOnce(&mut Self, &mut Context<S>) -> S,
) -> Entity<S> {
let current_view = self.current_view();
self.with_global_id(key.into(), |global_id, window| {
@@ -2537,7 +2611,7 @@ impl Window {
pub fn use_state<S: 'static>(
&mut self,
cx: &mut App,
- init: impl FnOnce(&mut Self, &mut App) -> S,
+ init: impl FnOnce(&mut Self, &mut Context<S>) -> S,
) -> Entity<S> {
self.use_keyed_state(
ElementId::CodeLocation(*core::panic::Location::caller()),
@@ -2741,7 +2815,7 @@ impl Window {
/// Paint one or more quads into the scene for the next frame at the current stacking context.
/// Quads are colored rectangular regions with an optional background, border, and corner radius.
- /// see [`fill`](crate::fill), [`outline`](crate::outline), and [`quad`](crate::quad) to construct this type.
+ /// see [`fill`], [`outline`], and [`quad`] to construct this type.
///
/// This method should only be called as part of the paint phase of element drawing.
///
@@ -3044,7 +3118,7 @@ impl Window {
let tile = self
.sprite_atlas
- .get_or_insert_with(¶ms.clone().into(), &mut || {
+ .get_or_insert_with(¶ms.into(), &mut || {
Ok(Some((
data.size(frame_index),
Cow::Borrowed(
@@ -3401,16 +3475,16 @@ impl Window {
let focus_id = handle.id;
let (subscription, activate) =
self.new_focus_listener(Box::new(move |event, window, cx| {
- if let Some(blurred_id) = event.previous_focus_path.last().copied() {
- if event.is_focus_out(focus_id) {
- let event = FocusOutEvent {
- blurred: WeakFocusHandle {
- id: blurred_id,
- handles: Arc::downgrade(&cx.focus_handles),
- },
- };
- listener(event, window, cx)
- }
+ if let Some(blurred_id) = event.previous_focus_path.last().copied()
+ && event.is_focus_out(focus_id)
+ {
+ let event = FocusOutEvent {
+ blurred: WeakFocusHandle {
+ id: blurred_id,
+ handles: Arc::downgrade(&cx.focus_handles),
+ },
+ };
+ listener(event, window, cx)
}
true
}));
@@ -3444,12 +3518,12 @@ impl Window {
return true;
}
- if let Some(input) = keystroke.key_char {
- if let Some(mut input_handler) = self.platform_window.take_input_handler() {
- input_handler.dispatch_input(&input, self, cx);
- self.platform_window.set_input_handler(input_handler);
- return true;
- }
+ if let Some(input) = keystroke.key_char
+ && let Some(mut input_handler) = self.platform_window.take_input_handler()
+ {
+ input_handler.dispatch_input(&input, self, cx);
+ self.platform_window.set_input_handler(input_handler);
+ return true;
}
false
@@ -3731,7 +3805,7 @@ impl Window {
self.dispatch_keystroke_observers(
event,
Some(binding.action),
- match_result.context_stack.clone(),
+ match_result.context_stack,
cx,
);
self.pending_input_changed(cx);
@@ -3864,11 +3938,11 @@ impl Window {
if !cx.propagate_event {
continue 'replay;
}
- if let Some(input) = replay.keystroke.key_char.as_ref().cloned() {
- if let Some(mut input_handler) = self.platform_window.take_input_handler() {
- input_handler.dispatch_input(&input, self, cx);
- self.platform_window.set_input_handler(input_handler)
- }
+ if let Some(input) = replay.keystroke.key_char.as_ref().cloned()
+ && let Some(mut input_handler) = self.platform_window.take_input_handler()
+ {
+ input_handler.dispatch_input(&input, self, cx);
+ self.platform_window.set_input_handler(input_handler)
}
}
}
@@ -4022,9 +4096,7 @@ impl Window {
self.on_next_frame(|window, cx| {
if let Some(mut input_handler) = window.platform_window.take_input_handler() {
if let Some(bounds) = input_handler.selected_bounds(window, cx) {
- window
- .platform_window
- .update_ime_position(bounds.scale(window.scale_factor()));
+ window.platform_window.update_ime_position(bounds);
}
window.platform_window.set_input_handler(input_handler);
}
@@ -4275,11 +4347,54 @@ impl Window {
}
/// Perform titlebar double-click action.
- /// This is MacOS specific.
+ /// This is macOS specific.
pub fn titlebar_double_click(&self) {
self.platform_window.titlebar_double_click();
}
+ /// Gets the window's title at the platform level.
+ /// This is macOS specific.
+ pub fn window_title(&self) -> String {
+ self.platform_window.get_title()
+ }
+
+ /// Returns a list of all tabbed windows and their titles.
+ /// This is macOS specific.
+ pub fn tabbed_windows(&self) -> Option<Vec<SystemWindowTab>> {
+ self.platform_window.tabbed_windows()
+ }
+
+ /// Returns the tab bar visibility.
+ /// This is macOS specific.
+ pub fn tab_bar_visible(&self) -> bool {
+ self.platform_window.tab_bar_visible()
+ }
+
+ /// Merges all open windows into a single tabbed window.
+ /// This is macOS specific.
+ pub fn merge_all_windows(&self) {
+ self.platform_window.merge_all_windows()
+ }
+
+ /// Moves the tab to a new containing window.
+ /// This is macOS specific.
+ pub fn move_tab_to_new_window(&self) {
+ self.platform_window.move_tab_to_new_window()
+ }
+
+ /// Shows or hides the window tab overview.
+ /// This is macOS specific.
+ pub fn toggle_window_tab_overview(&self) {
+ self.platform_window.toggle_window_tab_overview()
+ }
+
+ /// Sets the tabbing identifier for the window.
+ /// This is macOS specific.
+ pub fn set_tabbing_identifier(&self, tabbing_identifier: Option<String>) {
+ self.platform_window
+ .set_tabbing_identifier(tabbing_identifier)
+ }
+
/// Toggles the inspector mode on this window.
#[cfg(any(feature = "inspector", debug_assertions))]
pub fn toggle_inspector(&mut self, cx: &mut App) {
@@ -4309,15 +4424,15 @@ impl Window {
cx: &mut App,
f: impl FnOnce(&mut Option<T>, &mut Self) -> R,
) -> R {
- if let Some(inspector_id) = _inspector_id {
- if let Some(inspector) = &self.inspector {
- let inspector = inspector.clone();
- let active_element_id = inspector.read(cx).active_element_id();
- if Some(inspector_id) == active_element_id {
- return inspector.update(cx, |inspector, _cx| {
- inspector.with_active_element_state(self, f)
- });
- }
+ if let Some(inspector_id) = _inspector_id
+ && let Some(inspector) = &self.inspector
+ {
+ let inspector = inspector.clone();
+ let active_element_id = inspector.read(cx).active_element_id();
+ if Some(inspector_id) == active_element_id {
+ return inspector.update(cx, |inspector, _cx| {
+ inspector.with_active_element_state(self, f)
+ });
}
}
f(&mut None, self)
@@ -4389,15 +4504,13 @@ impl Window {
if let Some(inspector) = self.inspector.as_ref() {
let inspector = inspector.read(cx);
if let Some((hitbox_id, _)) = self.hovered_inspector_hitbox(inspector, &self.next_frame)
- {
- if let Some(hitbox) = self
+ && let Some(hitbox) = self
.next_frame
.hitboxes
.iter()
.find(|hitbox| hitbox.id == hitbox_id)
- {
- self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d)));
- }
+ {
+ self.paint_quad(crate::fill(hitbox.bounds, crate::rgba(0x61afef4d)));
}
}
}
@@ -4444,7 +4557,7 @@ impl Window {
if let Some((_, inspector_id)) =
self.hovered_inspector_hitbox(inspector, &self.rendered_frame)
{
- inspector.set_active_element_id(inspector_id.clone(), self);
+ inspector.set_active_element_id(inspector_id, self);
}
}
});
@@ -4468,7 +4581,14 @@ impl Window {
}
}
}
- return None;
+ None
+ }
+
+ /// For testing: set the current modifier keys state.
+ /// This does not generate any events.
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn set_modifiers(&mut self, modifiers: Modifiers) {
+ self.modifiers = modifiers;
}
}
@@ -4585,7 +4705,7 @@ impl<V: 'static + Render> WindowHandle<V> {
where
C: AppContext,
{
- cx.read_window(self, |root_view, _cx| root_view.clone())
+ cx.read_window(self, |root_view, _cx| root_view)
}
/// Check if this window is 'active'.
@@ -4699,7 +4819,7 @@ impl HasDisplayHandle for Window {
}
}
-/// An identifier for an [`Element`](crate::Element).
+/// An identifier for an [`Element`].
///
/// Can be constructed with a string, a number, or both, as well
/// as other internal representations.
@@ -16,6 +16,13 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
let mut deprecated = None;
let mut doc_str: Option<String> = None;
+ /*
+ *
+ * #[action()]
+ * Struct Foo {
+ * bar: bool // is bar considered an attribute
+ }
+ */
for attr in &input.attrs {
if attr.path().is_ident("action") {
attr.parse_nested_meta(|meta| {
@@ -160,16 +160,14 @@ fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
let mut doc_lines = Vec::new();
for attr in attrs {
- if attr.path().is_ident("doc") {
- if let Meta::NameValue(meta) = &attr.meta {
- if let Expr::Lit(expr_lit) = &meta.value {
- if let Lit::Str(lit_str) = &expr_lit.lit {
- let line = lit_str.value();
- let line = line.strip_prefix(' ').unwrap_or(&line);
- doc_lines.push(line.to_string());
- }
- }
- }
+ if attr.path().is_ident("doc")
+ && let Meta::NameValue(meta) = &attr.meta
+ && let Expr::Lit(expr_lit) = &meta.value
+ && let Lit::Str(lit_str) = &expr_lit.lit
+ {
+ let line = lit_str.value();
+ let line = line.strip_prefix(' ').unwrap_or(&line);
+ doc_lines.push(line.to_string());
}
}
@@ -191,7 +189,7 @@ fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec<Attribute> {
fn is_called_from_gpui_crate(_span: Span) -> bool {
// Check if we're being called from within the gpui crate by examining the call site
// This is a heuristic approach - we check if the current crate name is "gpui"
- std::env::var("CARGO_PKG_NAME").map_or(false, |name| name == "gpui")
+ std::env::var("CARGO_PKG_NAME").is_ok_and(|name| name == "gpui")
}
struct MacroExpander;
@@ -172,7 +172,7 @@ pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
/// - `#[gpui::test(iterations = 5)]` runs five times, providing as seed the values in the range `0..5`.
/// - `#[gpui::test(retries = 3)]` runs up to four times if it fails to try and make it pass.
/// - `#[gpui::test(on_failure = "crate::test::report_failure")]` will call the specified function after the
-/// tests fail so that you can write out more detail about the failure.
+/// tests fail so that you can write out more detail about the failure.
///
/// You can combine `iterations = ...` with `seeds(...)`:
/// - `#[gpui::test(iterations = 5, seed = 10)]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10))]`.
@@ -73,7 +73,7 @@ impl Parse for Args {
(Meta::NameValue(meta), "seed") => {
seeds = vec![parse_usize_from_expr(&meta.value)? as u64]
}
- (Meta::List(list), "seeds") => seeds = parse_u64_array(&list)?,
+ (Meta::List(list), "seeds") => seeds = parse_u64_array(list)?,
(Meta::Path(_), _) => {
return Err(syn::Error::new(meta.span(), "invalid path argument"));
}
@@ -86,7 +86,7 @@ impl Parse for Args {
Ok(Args {
seeds,
max_retries,
- max_iterations: max_iterations,
+ max_iterations,
on_failure_fn_name,
})
}
@@ -152,28 +152,28 @@ fn generate_test_function(
}
_ => {}
}
- } else if let Type::Reference(ty) = &*arg.ty {
- if let Type::Path(ty) = &*ty.elem {
- let last_segment = ty.path.segments.last();
- if let Some("TestAppContext") =
- last_segment.map(|s| s.ident.to_string()).as_deref()
- {
- let cx_varname = format_ident!("cx_{}", ix);
- cx_vars.extend(quote!(
- let mut #cx_varname = gpui::TestAppContext::build(
- dispatcher.clone(),
- Some(stringify!(#outer_fn_name)),
- );
- ));
- cx_teardowns.extend(quote!(
- dispatcher.run_until_parked();
- #cx_varname.executor().forbid_parking();
- #cx_varname.quit();
- dispatcher.run_until_parked();
- ));
- inner_fn_args.extend(quote!(&mut #cx_varname,));
- continue;
- }
+ } else if let Type::Reference(ty) = &*arg.ty
+ && let Type::Path(ty) = &*ty.elem
+ {
+ let last_segment = ty.path.segments.last();
+ if let Some("TestAppContext") =
+ last_segment.map(|s| s.ident.to_string()).as_deref()
+ {
+ let cx_varname = format_ident!("cx_{}", ix);
+ cx_vars.extend(quote!(
+ let mut #cx_varname = gpui::TestAppContext::build(
+ dispatcher.clone(),
+ Some(stringify!(#outer_fn_name)),
+ );
+ ));
+ cx_teardowns.extend(quote!(
+ dispatcher.run_until_parked();
+ #cx_varname.executor().forbid_parking();
+ #cx_varname.quit();
+ dispatcher.run_until_parked();
+ ));
+ inner_fn_args.extend(quote!(&mut #cx_varname,));
+ continue;
}
}
}
@@ -215,48 +215,48 @@ fn generate_test_function(
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),));
continue;
}
- } else if let Type::Reference(ty) = &*arg.ty {
- if let Type::Path(ty) = &*ty.elem {
- let last_segment = ty.path.segments.last();
- match last_segment.map(|s| s.ident.to_string()).as_deref() {
- Some("App") => {
- let cx_varname = format_ident!("cx_{}", ix);
- let cx_varname_lock = format_ident!("cx_{}_lock", ix);
- cx_vars.extend(quote!(
- let mut #cx_varname = gpui::TestAppContext::build(
- dispatcher.clone(),
- Some(stringify!(#outer_fn_name))
- );
- let mut #cx_varname_lock = #cx_varname.app.borrow_mut();
- ));
- inner_fn_args.extend(quote!(&mut #cx_varname_lock,));
- cx_teardowns.extend(quote!(
+ } else if let Type::Reference(ty) = &*arg.ty
+ && let Type::Path(ty) = &*ty.elem
+ {
+ let last_segment = ty.path.segments.last();
+ match last_segment.map(|s| s.ident.to_string()).as_deref() {
+ Some("App") => {
+ let cx_varname = format_ident!("cx_{}", ix);
+ let cx_varname_lock = format_ident!("cx_{}_lock", ix);
+ cx_vars.extend(quote!(
+ let mut #cx_varname = gpui::TestAppContext::build(
+ dispatcher.clone(),
+ Some(stringify!(#outer_fn_name))
+ );
+ let mut #cx_varname_lock = #cx_varname.app.borrow_mut();
+ ));
+ inner_fn_args.extend(quote!(&mut #cx_varname_lock,));
+ cx_teardowns.extend(quote!(
drop(#cx_varname_lock);
dispatcher.run_until_parked();
#cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); });
dispatcher.run_until_parked();
));
- continue;
- }
- Some("TestAppContext") => {
- let cx_varname = format_ident!("cx_{}", ix);
- cx_vars.extend(quote!(
- let mut #cx_varname = gpui::TestAppContext::build(
- dispatcher.clone(),
- Some(stringify!(#outer_fn_name))
- );
- ));
- cx_teardowns.extend(quote!(
- dispatcher.run_until_parked();
- #cx_varname.executor().forbid_parking();
- #cx_varname.quit();
- dispatcher.run_until_parked();
- ));
- inner_fn_args.extend(quote!(&mut #cx_varname,));
- continue;
- }
- _ => {}
+ continue;
+ }
+ Some("TestAppContext") => {
+ let cx_varname = format_ident!("cx_{}", ix);
+ cx_vars.extend(quote!(
+ let mut #cx_varname = gpui::TestAppContext::build(
+ dispatcher.clone(),
+ Some(stringify!(#outer_fn_name))
+ );
+ ));
+ cx_teardowns.extend(quote!(
+ dispatcher.run_until_parked();
+ #cx_varname.executor().forbid_parking();
+ #cx_varname.quit();
+ dispatcher.run_until_parked();
+ ));
+ inner_fn_args.extend(quote!(&mut #cx_varname,));
+ continue;
}
+ _ => {}
}
}
}
@@ -34,13 +34,6 @@ trait Transform: Clone {
/// Adds one to the value
fn add_one(self) -> Self;
-
- /// cfg attributes are respected
- #[cfg(all())]
- fn cfg_included(self) -> Self;
-
- #[cfg(any())]
- fn cfg_omitted(self) -> Self;
}
#[derive(Debug, Clone, PartialEq)]
@@ -70,10 +63,6 @@ impl Transform for Number {
fn add_one(self) -> Self {
Number(self.0 + 1)
}
-
- fn cfg_included(self) -> Self {
- Number(self.0)
- }
}
#[test]
@@ -83,14 +72,13 @@ fn test_derive_inspector_reflection() {
// Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self
let methods = methods::<Number>();
- assert_eq!(methods.len(), 6);
+ assert_eq!(methods.len(), 5);
let method_names: Vec<_> = methods.iter().map(|m| m.name).collect();
assert!(method_names.contains(&"double"));
assert!(method_names.contains(&"triple"));
assert!(method_names.contains(&"increment"));
assert!(method_names.contains(&"quadruple"));
assert!(method_names.contains(&"add_one"));
- assert!(method_names.contains(&"cfg_included"));
// Invoke methods by name
let num = Number(5);
@@ -106,9 +94,7 @@ fn test_derive_inspector_reflection() {
.invoke(num.clone());
assert_eq!(incremented, Number(6));
- let quadrupled = find_method::<Number>("quadruple")
- .unwrap()
- .invoke(num.clone());
+ let quadrupled = find_method::<Number>("quadruple").unwrap().invoke(num);
assert_eq!(quadrupled, Number(20));
// Try to invoke a non-existent method
@@ -13,6 +13,7 @@ path = "src/gpui_tokio.rs"
doctest = false
[dependencies]
+anyhow.workspace = true
util.workspace = true
gpui.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
@@ -52,6 +52,28 @@ impl Tokio {
})
}
+ /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task
+ /// Note that the Tokio task will be cancelled if the GPUI task is dropped
+ pub fn spawn_result<C, Fut, R>(cx: &C, f: Fut) -> C::Result<Task<anyhow::Result<R>>>
+ where
+ C: AppContext,
+ Fut: Future<Output = anyhow::Result<R>> + Send + 'static,
+ R: Send + 'static,
+ {
+ cx.read_global(|tokio: &GlobalTokio, cx| {
+ let join_handle = tokio.runtime.spawn(f);
+ let abort_handle = join_handle.abort_handle();
+ let cancel = defer(move || {
+ abort_handle.abort();
+ });
+ cx.background_spawn(async move {
+ let result = join_handle.await?;
+ drop(cancel);
+ result
+ })
+ })
+ }
+
pub fn handle(cx: &App) -> tokio::runtime::Handle {
GlobalTokio::global(cx).runtime.handle().clone()
}
@@ -34,15 +34,14 @@ impl HandleTag for ParagraphHandler {
tag: &HtmlElement,
writer: &mut MarkdownWriter,
) -> StartTagOutcome {
- if tag.is_inline() && writer.is_inside("p") {
- if let Some(parent) = writer.current_element_stack().iter().last() {
- if !(parent.is_inline()
- || writer.markdown.ends_with(' ')
- || writer.markdown.ends_with('\n'))
- {
- writer.push_str(" ");
- }
- }
+ if tag.is_inline()
+ && writer.is_inside("p")
+ && let Some(parent) = writer.current_element_stack().iter().last()
+ && !(parent.is_inline()
+ || writer.markdown.ends_with(' ')
+ || writer.markdown.ends_with('\n'))
+ {
+ writer.push_str(" ");
}
if tag.tag() == "p" {
@@ -40,7 +40,7 @@ impl AsyncBody {
}
pub fn from_bytes(bytes: Bytes) -> Self {
- Self(Inner::Bytes(Cursor::new(bytes.clone())))
+ Self(Inner::Bytes(Cursor::new(bytes)))
}
}
@@ -77,10 +77,10 @@ pub async fn latest_github_release(
.find(|release| release.pre_release == pre_release)
.context("finding a prerelease")?;
release.assets.iter_mut().for_each(|asset| {
- if let Some(digest) = &mut asset.digest {
- if let Some(stripped) = digest.strip_prefix("sha256:") {
- *digest = stripped.to_owned();
- }
+ if let Some(digest) = &mut asset.digest
+ && let Some(stripped) = digest.strip_prefix("sha256:")
+ {
+ *digest = stripped.to_owned();
}
});
Ok(release)
@@ -435,8 +435,7 @@ impl HttpClient for FakeHttpClient {
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
- let future = (self.handler.lock().as_ref().unwrap())(req);
- future
+ ((self.handler.lock().as_ref().unwrap())(req)) as _
}
fn user_agent(&self) -> Option<&HeaderValue> {
@@ -6,7 +6,7 @@ Icons are a big part of Zed, and they're how we convey hundreds of actions witho
When introducing a new icon, it's important to ensure consistency with the existing set, which follows these guidelines:
1. The SVG view box should be 16x16.
-2. For outlined icons, use a 1.5px stroke width.
+2. For outlined icons, use a 1.2px stroke width.
3. Not all icons are mathematically aligned; there's quite a bit of optical adjustment. However, try to keep the icon within an internal 12x12 bounding box as much as possible while ensuring proper visibility.
4. Use the `filled` and `outlined` terminology when introducing icons that will have these two variants.
5. Icons that are deeply contextual may have the feature context as their name prefix. For example, `ToolWeb`, `ReplPlay`, `DebugStepInto`, etc.
@@ -34,6 +34,7 @@ pub enum IconName {
ArrowRightLeft,
ArrowUp,
ArrowUpRight,
+ Attach,
AudioOff,
AudioOn,
Backspace,
@@ -140,10 +141,12 @@ pub enum IconName {
Image,
Indicator,
Info,
+ Json,
Keyboard,
Library,
LineHeight,
ListCollapse,
+ ListFilter,
ListTodo,
ListTree,
ListX,
@@ -154,6 +157,7 @@ pub enum IconName {
Maximize,
Menu,
MenuAlt,
+ MenuAltTemp,
Mic,
MicMute,
Minimize,
@@ -162,6 +166,7 @@ pub enum IconName {
PageDown,
PageUp,
Pencil,
+ PencilUnavailable,
Person,
Pin,
PlayOutlined,
@@ -211,6 +216,7 @@ pub enum IconName {
Tab,
Terminal,
TerminalAlt,
+ TerminalGhost,
TextSnippet,
TextThread,
Thread,
@@ -244,6 +250,8 @@ pub enum IconName {
Warning,
WholeWord,
XCircle,
+ XCircleFilled,
+ ZedAgent,
ZedAssistant,
ZedBurnMode,
ZedBurnModeOn,
@@ -401,12 +401,19 @@ pub fn init(cx: &mut App) {
mod persistence {
use std::path::PathBuf;
- use db::{define_connection, query, sqlez_macros::sql};
+ use db::{
+ query,
+ sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
+ sqlez_macros::sql,
+ };
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
- define_connection! {
- pub static ref IMAGE_VIEWER: ImageViewerDb<WorkspaceDb> =
- &[sql!(
+ pub struct ImageViewerDb(ThreadSafeConnection);
+
+ impl Domain for ImageViewerDb {
+ const NAME: &str = stringify!(ImageViewerDb);
+
+ const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE image_viewers (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
@@ -417,9 +424,11 @@ mod persistence {
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
- )];
+ )];
}
+ db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]);
+
impl ImageViewerDb {
query! {
pub async fn save_image_path(
@@ -1,10 +1,11 @@
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
/// The settings for the image viewer.
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Default, SettingsUi, SettingsKey)]
+#[settings_key(key = "image_viewer")]
pub struct ImageViewerSettings {
/// The unit to use for displaying image file sizes.
///
@@ -24,8 +25,6 @@ pub enum ImageFileSizeUnit {
}
impl Settings for ImageViewerSettings {
- const KEY: Option<&'static str> = Some("image_viewer");
-
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
@@ -1,38 +0,0 @@
-[package]
-name = "indexed_docs"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/indexed_docs.rs"
-
-[dependencies]
-anyhow.workspace = true
-async-trait.workspace = true
-cargo_metadata.workspace = true
-collections.workspace = true
-derive_more.workspace = true
-extension.workspace = true
-fs.workspace = true
-futures.workspace = true
-fuzzy.workspace = true
-gpui.workspace = true
-heed.workspace = true
-html_to_markdown.workspace = true
-http_client.workspace = true
-indexmap.workspace = true
-parking_lot.workspace = true
-paths.workspace = true
-serde.workspace = true
-strum.workspace = true
-util.workspace = true
-workspace-hack.workspace = true
-
-[dev-dependencies]
-indoc.workspace = true
-pretty_assertions.workspace = true
@@ -1,81 +0,0 @@
-use std::path::PathBuf;
-use std::sync::Arc;
-
-use anyhow::Result;
-use async_trait::async_trait;
-use extension::{Extension, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy};
-use gpui::App;
-
-use crate::{
- IndexedDocsDatabase, IndexedDocsProvider, IndexedDocsRegistry, PackageName, ProviderId,
-};
-
-pub fn init(cx: &mut App) {
- let proxy = ExtensionHostProxy::default_global(cx);
- proxy.register_indexed_docs_provider_proxy(IndexedDocsRegistryProxy {
- indexed_docs_registry: IndexedDocsRegistry::global(cx),
- });
-}
-
-struct IndexedDocsRegistryProxy {
- indexed_docs_registry: Arc<IndexedDocsRegistry>,
-}
-
-impl ExtensionIndexedDocsProviderProxy for IndexedDocsRegistryProxy {
- fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>) {
- self.indexed_docs_registry
- .register_provider(Box::new(ExtensionIndexedDocsProvider::new(
- extension,
- ProviderId(provider_id),
- )));
- }
-
- fn unregister_indexed_docs_provider(&self, provider_id: Arc<str>) {
- self.indexed_docs_registry
- .unregister_provider(&ProviderId(provider_id));
- }
-}
-
-pub struct ExtensionIndexedDocsProvider {
- extension: Arc<dyn Extension>,
- id: ProviderId,
-}
-
-impl ExtensionIndexedDocsProvider {
- pub fn new(extension: Arc<dyn Extension>, id: ProviderId) -> Self {
- Self { extension, id }
- }
-}
-
-#[async_trait]
-impl IndexedDocsProvider for ExtensionIndexedDocsProvider {
- fn id(&self) -> ProviderId {
- self.id.clone()
- }
-
- fn database_path(&self) -> PathBuf {
- let mut database_path = PathBuf::from(self.extension.work_dir().as_ref());
- database_path.push("docs");
- database_path.push(format!("{}.0.mdb", self.id));
-
- database_path
- }
-
- async fn suggest_packages(&self) -> Result<Vec<PackageName>> {
- let packages = self
- .extension
- .suggest_docs_packages(self.id.0.clone())
- .await?;
-
- Ok(packages
- .into_iter()
- .map(|package| PackageName::from(package.as_str()))
- .collect())
- }
-
- async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
- self.extension
- .index_docs(self.id.0.clone(), package.as_ref().into(), database)
- .await
- }
-}
@@ -1,16 +0,0 @@
-mod extension_indexed_docs_provider;
-mod providers;
-mod registry;
-mod store;
-
-use gpui::App;
-
-pub use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider;
-pub use crate::providers::rustdoc::*;
-pub use crate::registry::*;
-pub use crate::store::*;
-
-pub fn init(cx: &mut App) {
- IndexedDocsRegistry::init_global(cx);
- extension_indexed_docs_provider::init(cx);
-}
@@ -1 +0,0 @@
-pub mod rustdoc;
@@ -1,291 +0,0 @@
-mod item;
-mod to_markdown;
-
-use cargo_metadata::MetadataCommand;
-use futures::future::BoxFuture;
-pub use item::*;
-use parking_lot::RwLock;
-pub use to_markdown::convert_rustdoc_to_markdown;
-
-use std::collections::BTreeSet;
-use std::path::PathBuf;
-use std::sync::{Arc, LazyLock};
-use std::time::{Duration, Instant};
-
-use anyhow::{Context as _, Result, bail};
-use async_trait::async_trait;
-use collections::{HashSet, VecDeque};
-use fs::Fs;
-use futures::{AsyncReadExt, FutureExt};
-use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
-
-use crate::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId};
-
-#[derive(Debug)]
-struct RustdocItemWithHistory {
- pub item: RustdocItem,
- #[cfg(debug_assertions)]
- pub history: Vec<String>,
-}
-
-pub struct LocalRustdocProvider {
- fs: Arc<dyn Fs>,
- cargo_workspace_root: PathBuf,
-}
-
-impl LocalRustdocProvider {
- pub fn id() -> ProviderId {
- ProviderId("rustdoc".into())
- }
-
- pub fn new(fs: Arc<dyn Fs>, cargo_workspace_root: PathBuf) -> Self {
- Self {
- fs,
- cargo_workspace_root,
- }
- }
-}
-
-#[async_trait]
-impl IndexedDocsProvider for LocalRustdocProvider {
- fn id(&self) -> ProviderId {
- Self::id()
- }
-
- fn database_path(&self) -> PathBuf {
- paths::data_dir().join("docs/rust/rustdoc-db.1.mdb")
- }
-
- async fn suggest_packages(&self) -> Result<Vec<PackageName>> {
- static WORKSPACE_CRATES: LazyLock<RwLock<Option<(BTreeSet<PackageName>, Instant)>>> =
- LazyLock::new(|| RwLock::new(None));
-
- if let Some((crates, fetched_at)) = &*WORKSPACE_CRATES.read() {
- if fetched_at.elapsed() < Duration::from_secs(300) {
- return Ok(crates.iter().cloned().collect());
- }
- }
-
- let workspace = MetadataCommand::new()
- .manifest_path(self.cargo_workspace_root.join("Cargo.toml"))
- .exec()
- .context("failed to load cargo metadata")?;
-
- let workspace_crates = workspace
- .packages
- .into_iter()
- .map(|package| PackageName::from(package.name.as_str()))
- .collect::<BTreeSet<_>>();
-
- *WORKSPACE_CRATES.write() = Some((workspace_crates.clone(), Instant::now()));
-
- Ok(workspace_crates.into_iter().collect())
- }
-
- async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
- index_rustdoc(package, database, {
- move |crate_name, item| {
- let fs = self.fs.clone();
- let cargo_workspace_root = self.cargo_workspace_root.clone();
- let crate_name = crate_name.clone();
- let item = item.cloned();
- async move {
- let target_doc_path = cargo_workspace_root.join("target/doc");
- let mut local_cargo_doc_path = target_doc_path.join(crate_name.as_ref().replace('-', "_"));
-
- if !fs.is_dir(&local_cargo_doc_path).await {
- let cargo_doc_exists_at_all = fs.is_dir(&target_doc_path).await;
- if cargo_doc_exists_at_all {
- bail!(
- "no docs directory for '{crate_name}'. if this is a valid crate name, try running `cargo doc`"
- );
- } else {
- bail!("no cargo doc directory. run `cargo doc`");
- }
- }
-
- if let Some(item) = item {
- local_cargo_doc_path.push(item.url_path());
- } else {
- local_cargo_doc_path.push("index.html");
- }
-
- let Ok(contents) = fs.load(&local_cargo_doc_path).await else {
- return Ok(None);
- };
-
- Ok(Some(contents))
- }
- .boxed()
- }
- })
- .await
- }
-}
-
-pub struct DocsDotRsProvider {
- http_client: Arc<HttpClientWithUrl>,
-}
-
-impl DocsDotRsProvider {
- pub fn id() -> ProviderId {
- ProviderId("docs-rs".into())
- }
-
- pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
- Self { http_client }
- }
-}
-
-#[async_trait]
-impl IndexedDocsProvider for DocsDotRsProvider {
- fn id(&self) -> ProviderId {
- Self::id()
- }
-
- fn database_path(&self) -> PathBuf {
- paths::data_dir().join("docs/rust/docs-rs-db.1.mdb")
- }
-
- async fn suggest_packages(&self) -> Result<Vec<PackageName>> {
- static POPULAR_CRATES: LazyLock<Vec<PackageName>> = LazyLock::new(|| {
- include_str!("./rustdoc/popular_crates.txt")
- .lines()
- .filter(|line| !line.starts_with('#'))
- .map(|line| PackageName::from(line.trim()))
- .collect()
- });
-
- Ok(POPULAR_CRATES.clone())
- }
-
- async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
- index_rustdoc(package, database, {
- move |crate_name, item| {
- let http_client = self.http_client.clone();
- let crate_name = crate_name.clone();
- let item = item.cloned();
- async move {
- let version = "latest";
- let path = format!(
- "{crate_name}/{version}/{crate_name}{item_path}",
- item_path = item
- .map(|item| format!("/{}", item.url_path()))
- .unwrap_or_default()
- );
-
- let mut response = http_client
- .get(
- &format!("https://docs.rs/{path}"),
- AsyncBody::default(),
- true,
- )
- .await?;
-
- let mut body = Vec::new();
- response
- .body_mut()
- .read_to_end(&mut body)
- .await
- .context("error reading docs.rs response body")?;
-
- if response.status().is_client_error() {
- let text = String::from_utf8_lossy(body.as_slice());
- bail!(
- "status error {}, response: {text:?}",
- response.status().as_u16()
- );
- }
-
- Ok(Some(String::from_utf8(body)?))
- }
- .boxed()
- }
- })
- .await
- }
-}
-
-async fn index_rustdoc(
- package: PackageName,
- database: Arc<IndexedDocsDatabase>,
- fetch_page: impl Fn(
- &PackageName,
- Option<&RustdocItem>,
- ) -> BoxFuture<'static, Result<Option<String>>>
- + Send
- + Sync,
-) -> Result<()> {
- let Some(package_root_content) = fetch_page(&package, None).await? else {
- return Ok(());
- };
-
- let (crate_root_markdown, items) =
- convert_rustdoc_to_markdown(package_root_content.as_bytes())?;
-
- database
- .insert(package.to_string(), crate_root_markdown)
- .await?;
-
- let mut seen_items = HashSet::from_iter(items.clone());
- let mut items_to_visit: VecDeque<RustdocItemWithHistory> =
- VecDeque::from_iter(items.into_iter().map(|item| RustdocItemWithHistory {
- item,
- #[cfg(debug_assertions)]
- history: Vec::new(),
- }));
-
- while let Some(item_with_history) = items_to_visit.pop_front() {
- let item = &item_with_history.item;
-
- let Some(result) = fetch_page(&package, Some(item)).await.with_context(|| {
- #[cfg(debug_assertions)]
- {
- format!(
- "failed to fetch {item:?}: {history:?}",
- history = item_with_history.history
- )
- }
-
- #[cfg(not(debug_assertions))]
- {
- format!("failed to fetch {item:?}")
- }
- })?
- else {
- continue;
- };
-
- let (markdown, referenced_items) = convert_rustdoc_to_markdown(result.as_bytes())?;
-
- database
- .insert(format!("{package}::{}", item.display()), markdown)
- .await?;
-
- let parent_item = item;
- for mut item in referenced_items {
- if seen_items.contains(&item) {
- continue;
- }
-
- seen_items.insert(item.clone());
-
- item.path.extend(parent_item.path.clone());
- if parent_item.kind == RustdocItemKind::Mod {
- item.path.push(parent_item.name.clone());
- }
-
- items_to_visit.push_back(RustdocItemWithHistory {
- #[cfg(debug_assertions)]
- history: {
- let mut history = item_with_history.history.clone();
- history.push(item.url_path());
- history
- },
- item,
- });
- }
- }
-
- Ok(())
-}
@@ -1,82 +0,0 @@
-use std::sync::Arc;
-
-use serde::{Deserialize, Serialize};
-use strum::EnumIter;
-
-#[derive(
- Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize, EnumIter,
-)]
-#[serde(rename_all = "snake_case")]
-pub enum RustdocItemKind {
- Mod,
- Macro,
- Struct,
- Enum,
- Constant,
- Trait,
- Function,
- TypeAlias,
- AttributeMacro,
- DeriveMacro,
-}
-
-impl RustdocItemKind {
- pub(crate) const fn class(&self) -> &'static str {
- match self {
- Self::Mod => "mod",
- Self::Macro => "macro",
- Self::Struct => "struct",
- Self::Enum => "enum",
- Self::Constant => "constant",
- Self::Trait => "trait",
- Self::Function => "fn",
- Self::TypeAlias => "type",
- Self::AttributeMacro => "attr",
- Self::DeriveMacro => "derive",
- }
- }
-}
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
-pub struct RustdocItem {
- pub kind: RustdocItemKind,
- /// The item path, up until the name of the item.
- pub path: Vec<Arc<str>>,
- /// The name of the item.
- pub name: Arc<str>,
-}
-
-impl RustdocItem {
- pub fn display(&self) -> String {
- let mut path_segments = self.path.clone();
- path_segments.push(self.name.clone());
-
- path_segments.join("::")
- }
-
- pub fn url_path(&self) -> String {
- let name = &self.name;
- let mut path_components = self.path.clone();
-
- match self.kind {
- RustdocItemKind::Mod => {
- path_components.push(name.clone());
- path_components.push("index.html".into());
- }
- RustdocItemKind::Macro
- | RustdocItemKind::Struct
- | RustdocItemKind::Enum
- | RustdocItemKind::Constant
- | RustdocItemKind::Trait
- | RustdocItemKind::Function
- | RustdocItemKind::TypeAlias
- | RustdocItemKind::AttributeMacro
- | RustdocItemKind::DeriveMacro => {
- path_components
- .push(format!("{kind}.{name}.html", kind = self.kind.class()).into());
- }
- }
-
- path_components.join("/")
- }
-}
@@ -1,252 +0,0 @@
-# A list of the most popular Rust crates.
-# Sourced from https://lib.rs/std.
-serde
-serde_json
-syn
-clap
-thiserror
-rand
-log
-tokio
-anyhow
-regex
-quote
-proc-macro2
-base64
-itertools
-chrono
-lazy_static
-once_cell
-libc
-reqwest
-futures
-bitflags
-tracing
-url
-bytes
-toml
-tempfile
-uuid
-indexmap
-env_logger
-num-traits
-async-trait
-sha2
-hex
-tracing-subscriber
-http
-parking_lot
-cfg-if
-futures-util
-cc
-hashbrown
-rayon
-hyper
-getrandom
-semver
-strum
-flate2
-tokio-util
-smallvec
-criterion
-paste
-heck
-rand_core
-nom
-rustls
-nix
-glob
-time
-byteorder
-strum_macros
-serde_yaml
-wasm-bindgen
-ahash
-either
-num_cpus
-rand_chacha
-prost
-percent-encoding
-pin-project-lite
-tokio-stream
-bincode
-walkdir
-bindgen
-axum
-windows-sys
-futures-core
-ring
-digest
-num-bigint
-rustls-pemfile
-serde_with
-crossbeam-channel
-tokio-rustls
-hmac
-fastrand
-dirs
-zeroize
-socket2
-pin-project
-tower
-derive_more
-memchr
-toml_edit
-static_assertions
-pretty_assertions
-js-sys
-convert_case
-unicode-width
-pkg-config
-itoa
-colored
-rustc-hash
-darling
-mime
-web-sys
-image
-bytemuck
-which
-sha1
-dashmap
-arrayvec
-fnv
-tonic
-humantime
-libloading
-winapi
-rustc_version
-http-body
-indoc
-num
-home
-serde_urlencoded
-http-body-util
-unicode-segmentation
-num-integer
-webpki-roots
-phf
-futures-channel
-indicatif
-petgraph
-ordered-float
-strsim
-zstd
-console
-encoding_rs
-wasm-bindgen-futures
-urlencoding
-subtle
-crc32fast
-slab
-rustix
-predicates
-spin
-hyper-rustls
-backtrace
-rustversion
-mio
-scopeguard
-proc-macro-error
-hyper-util
-ryu
-prost-types
-textwrap
-memmap2
-zip
-zerocopy
-generic-array
-tar
-pyo3
-async-stream
-quick-xml
-memoffset
-csv
-crossterm
-windows
-num_enum
-tokio-tungstenite
-crossbeam-utils
-async-channel
-lru
-aes
-futures-lite
-tracing-core
-prettyplease
-httparse
-serde_bytes
-tracing-log
-tower-service
-cargo_metadata
-pest
-mime_guess
-tower-http
-data-encoding
-native-tls
-prost-build
-proptest
-derivative
-serial_test
-libm
-half
-futures-io
-bitvec
-rustls-native-certs
-ureq
-object
-anstyle
-tonic-build
-form_urlencoded
-num-derive
-pest_derive
-schemars
-proc-macro-crate
-rstest
-futures-executor
-assert_cmd
-termcolor
-serde_repr
-ctrlc
-sha3
-clap_complete
-flume
-mockall
-ipnet
-aho-corasick
-atty
-signal-hook
-async-std
-filetime
-num-complex
-opentelemetry
-cmake
-arc-swap
-derive_builder
-async-recursion
-dyn-clone
-bumpalo
-fs_extra
-git2
-sysinfo
-shlex
-instant
-approx
-rmp-serde
-rand_distr
-rustls-pki-types
-maplit
-sqlx
-blake3
-hyper-tls
-dotenvy
-jsonwebtoken
-openssl-sys
-crossbeam
-camino
-winreg
-config
-rsa
-bit-vec
-chrono-tz
-async-lock
-bstr
@@ -1,618 +0,0 @@
-use std::cell::RefCell;
-use std::io::Read;
-use std::rc::Rc;
-
-use anyhow::Result;
-use html_to_markdown::markdown::{
- HeadingHandler, ListHandler, ParagraphHandler, StyledTextHandler, TableHandler,
-};
-use html_to_markdown::{
- HandleTag, HandlerOutcome, HtmlElement, MarkdownWriter, StartTagOutcome, TagHandler,
- convert_html_to_markdown,
-};
-use indexmap::IndexSet;
-use strum::IntoEnumIterator;
-
-use crate::{RustdocItem, RustdocItemKind};
-
-/// Converts the provided rustdoc HTML to Markdown.
-pub fn convert_rustdoc_to_markdown(html: impl Read) -> Result<(String, Vec<RustdocItem>)> {
- let item_collector = Rc::new(RefCell::new(RustdocItemCollector::new()));
-
- let mut handlers: Vec<TagHandler> = vec![
- Rc::new(RefCell::new(ParagraphHandler)),
- Rc::new(RefCell::new(HeadingHandler)),
- Rc::new(RefCell::new(ListHandler)),
- Rc::new(RefCell::new(TableHandler::new())),
- Rc::new(RefCell::new(StyledTextHandler)),
- Rc::new(RefCell::new(RustdocChromeRemover)),
- Rc::new(RefCell::new(RustdocHeadingHandler)),
- Rc::new(RefCell::new(RustdocCodeHandler)),
- Rc::new(RefCell::new(RustdocItemHandler)),
- item_collector.clone(),
- ];
-
- let markdown = convert_html_to_markdown(html, &mut handlers)?;
-
- let items = item_collector
- .borrow()
- .items
- .iter()
- .cloned()
- .collect::<Vec<_>>();
-
- Ok((markdown, items))
-}
-
-pub struct RustdocHeadingHandler;
-
-impl HandleTag for RustdocHeadingHandler {
- fn should_handle(&self, _tag: &str) -> bool {
- // We're only handling text, so we don't need to visit any tags.
- false
- }
-
- fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
- if writer.is_inside("h1")
- || writer.is_inside("h2")
- || writer.is_inside("h3")
- || writer.is_inside("h4")
- || writer.is_inside("h5")
- || writer.is_inside("h6")
- {
- let text = text
- .trim_matches(|char| char == '\n' || char == '\r')
- .replace('\n', " ");
- writer.push_str(&text);
-
- return HandlerOutcome::Handled;
- }
-
- HandlerOutcome::NoOp
- }
-}
-
-pub struct RustdocCodeHandler;
-
-impl HandleTag for RustdocCodeHandler {
- fn should_handle(&self, tag: &str) -> bool {
- matches!(tag, "pre" | "code")
- }
-
- fn handle_tag_start(
- &mut self,
- tag: &HtmlElement,
- writer: &mut MarkdownWriter,
- ) -> StartTagOutcome {
- match tag.tag() {
- "code" => {
- if !writer.is_inside("pre") {
- writer.push_str("`");
- }
- }
- "pre" => {
- let classes = tag.classes();
- let is_rust = classes.iter().any(|class| class == "rust");
- let language = is_rust
- .then_some("rs")
- .or_else(|| {
- classes.iter().find_map(|class| {
- if let Some((_, language)) = class.split_once("language-") {
- Some(language.trim())
- } else {
- None
- }
- })
- })
- .unwrap_or("");
-
- writer.push_str(&format!("\n\n```{language}\n"));
- }
- _ => {}
- }
-
- StartTagOutcome::Continue
- }
-
- fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
- match tag.tag() {
- "code" => {
- if !writer.is_inside("pre") {
- writer.push_str("`");
- }
- }
- "pre" => writer.push_str("\n```\n"),
- _ => {}
- }
- }
-
- fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
- if writer.is_inside("pre") {
- writer.push_str(text);
- return HandlerOutcome::Handled;
- }
-
- HandlerOutcome::NoOp
- }
-}
-
-const RUSTDOC_ITEM_NAME_CLASS: &str = "item-name";
-
-pub struct RustdocItemHandler;
-
-impl RustdocItemHandler {
- /// Returns whether we're currently inside of an `.item-name` element, which
- /// rustdoc uses to display Rust items in a list.
- fn is_inside_item_name(writer: &MarkdownWriter) -> bool {
- writer
- .current_element_stack()
- .iter()
- .any(|element| element.has_class(RUSTDOC_ITEM_NAME_CLASS))
- }
-}
-
-impl HandleTag for RustdocItemHandler {
- fn should_handle(&self, tag: &str) -> bool {
- matches!(tag, "div" | "span")
- }
-
- fn handle_tag_start(
- &mut self,
- tag: &HtmlElement,
- writer: &mut MarkdownWriter,
- ) -> StartTagOutcome {
- match tag.tag() {
- "div" | "span" => {
- if Self::is_inside_item_name(writer) && tag.has_class("stab") {
- writer.push_str(" [");
- }
- }
- _ => {}
- }
-
- StartTagOutcome::Continue
- }
-
- fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
- match tag.tag() {
- "div" | "span" => {
- if tag.has_class(RUSTDOC_ITEM_NAME_CLASS) {
- writer.push_str(": ");
- }
-
- if Self::is_inside_item_name(writer) && tag.has_class("stab") {
- writer.push_str("]");
- }
- }
- _ => {}
- }
- }
-
- fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
- if Self::is_inside_item_name(writer)
- && !writer.is_inside("span")
- && !writer.is_inside("code")
- {
- writer.push_str(&format!("`{text}`"));
- return HandlerOutcome::Handled;
- }
-
- HandlerOutcome::NoOp
- }
-}
-
-pub struct RustdocChromeRemover;
-
-impl HandleTag for RustdocChromeRemover {
- fn should_handle(&self, tag: &str) -> bool {
- matches!(
- tag,
- "head" | "script" | "nav" | "summary" | "button" | "a" | "div" | "span"
- )
- }
-
- fn handle_tag_start(
- &mut self,
- tag: &HtmlElement,
- _writer: &mut MarkdownWriter,
- ) -> StartTagOutcome {
- match tag.tag() {
- "head" | "script" | "nav" => return StartTagOutcome::Skip,
- "summary" => {
- if tag.has_class("hideme") {
- return StartTagOutcome::Skip;
- }
- }
- "button" => {
- if tag.attr("id").as_deref() == Some("copy-path") {
- return StartTagOutcome::Skip;
- }
- }
- "a" => {
- if tag.has_any_classes(&["anchor", "doc-anchor", "src"]) {
- return StartTagOutcome::Skip;
- }
- }
- "div" | "span" => {
- if tag.has_any_classes(&["nav-container", "sidebar-elems", "out-of-band"]) {
- return StartTagOutcome::Skip;
- }
- }
-
- _ => {}
- }
-
- StartTagOutcome::Continue
- }
-}
-
-pub struct RustdocItemCollector {
- pub items: IndexSet<RustdocItem>,
-}
-
-impl RustdocItemCollector {
- pub fn new() -> Self {
- Self {
- items: IndexSet::new(),
- }
- }
-
- fn parse_item(tag: &HtmlElement) -> Option<RustdocItem> {
- if tag.tag() != "a" {
- return None;
- }
-
- let href = tag.attr("href")?;
- if href.starts_with('#') || href.starts_with("https://") || href.starts_with("../") {
- return None;
- }
-
- for kind in RustdocItemKind::iter() {
- if tag.has_class(kind.class()) {
- let mut parts = href.trim_end_matches("/index.html").split('/');
-
- if let Some(last_component) = parts.next_back() {
- let last_component = match last_component.split_once('#') {
- Some((component, _fragment)) => component,
- None => last_component,
- };
-
- let name = last_component
- .trim_start_matches(&format!("{}.", kind.class()))
- .trim_end_matches(".html");
-
- return Some(RustdocItem {
- kind,
- name: name.into(),
- path: parts.map(Into::into).collect(),
- });
- }
- }
- }
-
- None
- }
-}
-
-impl HandleTag for RustdocItemCollector {
- fn should_handle(&self, tag: &str) -> bool {
- tag == "a"
- }
-
- fn handle_tag_start(
- &mut self,
- tag: &HtmlElement,
- writer: &mut MarkdownWriter,
- ) -> StartTagOutcome {
- if tag.tag() == "a" {
- let is_reexport = writer.current_element_stack().iter().any(|element| {
- if let Some(id) = element.attr("id") {
- id.starts_with("reexport.") || id.starts_with("method.")
- } else {
- false
- }
- });
-
- if !is_reexport {
- if let Some(item) = Self::parse_item(tag) {
- self.items.insert(item);
- }
- }
- }
-
- StartTagOutcome::Continue
- }
-}
-
-#[cfg(test)]
-mod tests {
- use html_to_markdown::{TagHandler, convert_html_to_markdown};
- use indoc::indoc;
- use pretty_assertions::assert_eq;
-
- use super::*;
-
- fn rustdoc_handlers() -> Vec<TagHandler> {
- vec![
- Rc::new(RefCell::new(ParagraphHandler)),
- Rc::new(RefCell::new(HeadingHandler)),
- Rc::new(RefCell::new(ListHandler)),
- Rc::new(RefCell::new(TableHandler::new())),
- Rc::new(RefCell::new(StyledTextHandler)),
- Rc::new(RefCell::new(RustdocChromeRemover)),
- Rc::new(RefCell::new(RustdocHeadingHandler)),
- Rc::new(RefCell::new(RustdocCodeHandler)),
- Rc::new(RefCell::new(RustdocItemHandler)),
- ]
- }
-
- #[test]
- fn test_main_heading_buttons_get_removed() {
- let html = indoc! {r##"
- <div class="main-heading">
- <h1>Crate <a class="mod" href="#">serde</a><button id="copy-path" title="Copy item path to clipboard">Copy item path</button></h1>
- <span class="out-of-band">
- <a class="src" href="../src/serde/lib.rs.html#1-340">source</a> · <button id="toggle-all-docs" title="collapse all docs">[<span>−</span>]</button>
- </span>
- </div>
- "##};
- let expected = indoc! {"
- # Crate serde
- "}
- .trim();
-
- assert_eq!(
- convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
- expected
- )
- }
-
- #[test]
- fn test_single_paragraph() {
- let html = indoc! {r#"
- <p>In particular, the last point is what sets <code>axum</code> apart from other frameworks.
- <code>axum</code> doesn’t have its own middleware system but instead uses
- <a href="https://docs.rs/tower-service/0.3.2/x86_64-unknown-linux-gnu/tower_service/trait.Service.html" title="trait tower_service::Service"><code>tower::Service</code></a>. This means <code>axum</code> gets timeouts, tracing, compression,
- authorization, and more, for free. It also enables you to share middleware with
- applications written using <a href="http://crates.io/crates/hyper"><code>hyper</code></a> or <a href="http://crates.io/crates/tonic"><code>tonic</code></a>.</p>
- "#};
- let expected = indoc! {"
- In particular, the last point is what sets `axum` apart from other frameworks. `axum` doesn’t have its own middleware system but instead uses `tower::Service`. This means `axum` gets timeouts, tracing, compression, authorization, and more, for free. It also enables you to share middleware with applications written using `hyper` or `tonic`.
- "}
- .trim();
-
- assert_eq!(
- convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
- expected
- )
- }
-
- #[test]
- fn test_multiple_paragraphs() {
- let html = indoc! {r##"
- <h2 id="serde"><a class="doc-anchor" href="#serde">§</a>Serde</h2>
- <p>Serde is a framework for <em><strong>ser</strong></em>ializing and <em><strong>de</strong></em>serializing Rust data
- structures efficiently and generically.</p>
- <p>The Serde ecosystem consists of data structures that know how to serialize
- and deserialize themselves along with data formats that know how to
- serialize and deserialize other things. Serde provides the layer by which
- these two groups interact with each other, allowing any supported data
- structure to be serialized and deserialized using any supported data format.</p>
- <p>See the Serde website <a href="https://serde.rs/">https://serde.rs/</a> for additional documentation and
- usage examples.</p>
- <h3 id="design"><a class="doc-anchor" href="#design">§</a>Design</h3>
- <p>Where many other languages rely on runtime reflection for serializing data,
- Serde is instead built on Rust’s powerful trait system. A data structure
- that knows how to serialize and deserialize itself is one that implements
- Serde’s <code>Serialize</code> and <code>Deserialize</code> traits (or uses Serde’s derive
- attribute to automatically generate implementations at compile time). This
- avoids any overhead of reflection or runtime type information. In fact in
- many situations the interaction between data structure and data format can
- be completely optimized away by the Rust compiler, leaving Serde
- serialization to perform the same speed as a handwritten serializer for the
- specific selection of data structure and data format.</p>
- "##};
- let expected = indoc! {"
- ## Serde
-
- Serde is a framework for _**ser**_ializing and _**de**_serializing Rust data structures efficiently and generically.
-
- The Serde ecosystem consists of data structures that know how to serialize and deserialize themselves along with data formats that know how to serialize and deserialize other things. Serde provides the layer by which these two groups interact with each other, allowing any supported data structure to be serialized and deserialized using any supported data format.
-
- See the Serde website https://serde.rs/ for additional documentation and usage examples.
-
- ### Design
-
- Where many other languages rely on runtime reflection for serializing data, Serde is instead built on Rust’s powerful trait system. A data structure that knows how to serialize and deserialize itself is one that implements Serde’s `Serialize` and `Deserialize` traits (or uses Serde’s derive attribute to automatically generate implementations at compile time). This avoids any overhead of reflection or runtime type information. In fact in many situations the interaction between data structure and data format can be completely optimized away by the Rust compiler, leaving Serde serialization to perform the same speed as a handwritten serializer for the specific selection of data structure and data format.
- "}
- .trim();
-
- assert_eq!(
- convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
- expected
- )
- }
-
- #[test]
- fn test_styled_text() {
- let html = indoc! {r#"
- <p>This text is <strong>bolded</strong>.</p>
- <p>This text is <em>italicized</em>.</p>
- "#};
- let expected = indoc! {"
- This text is **bolded**.
-
- This text is _italicized_.
- "}
- .trim();
-
- assert_eq!(
- convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
- expected
- )
- }
-
- #[test]
- fn test_rust_code_block() {
- let html = indoc! {r#"
- <pre class="rust rust-example-rendered"><code><span class="kw">use </span>axum::extract::{Path, Query, Json};
- <span class="kw">use </span>std::collections::HashMap;
-
- <span class="comment">// `Path` gives you the path parameters and deserializes them.
- </span><span class="kw">async fn </span>path(Path(user_id): Path<u32>) {}
-
- <span class="comment">// `Query` gives you the query parameters and deserializes them.
- </span><span class="kw">async fn </span>query(Query(params): Query<HashMap<String, String>>) {}
-
- <span class="comment">// Buffer the request body and deserialize it as JSON into a
- // `serde_json::Value`. `Json` supports any type that implements
- // `serde::Deserialize`.
- </span><span class="kw">async fn </span>json(Json(payload): Json<serde_json::Value>) {}</code></pre>
- "#};
- let expected = indoc! {"
- ```rs
- use axum::extract::{Path, Query, Json};
- use std::collections::HashMap;
-
- // `Path` gives you the path parameters and deserializes them.
- async fn path(Path(user_id): Path<u32>) {}
-
- // `Query` gives you the query parameters and deserializes them.
- async fn query(Query(params): Query<HashMap<String, String>>) {}
-
- // Buffer the request body and deserialize it as JSON into a
- // `serde_json::Value`. `Json` supports any type that implements
- // `serde::Deserialize`.
- async fn json(Json(payload): Json<serde_json::Value>) {}
- ```
- "}
- .trim();
-
- assert_eq!(
- convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
- expected
- )
- }
-
- #[test]
- fn test_toml_code_block() {
- let html = indoc! {r##"
- <h2 id="required-dependencies"><a class="doc-anchor" href="#required-dependencies">§</a>Required dependencies</h2>
- <p>To use axum there are a few dependencies you have to pull in as well:</p>
- <div class="example-wrap"><pre class="language-toml"><code>[dependencies]
- axum = "<latest-version>"
- tokio = { version = "<latest-version>", features = ["full"] }
- tower = "<latest-version>"
- </code></pre></div>
- "##};
- let expected = indoc! {r#"
- ## Required dependencies
-
- To use axum there are a few dependencies you have to pull in as well:
-
- ```toml
- [dependencies]
- axum = "<latest-version>"
- tokio = { version = "<latest-version>", features = ["full"] }
- tower = "<latest-version>"
-
- ```
- "#}
- .trim();
-
- assert_eq!(
- convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
- expected
- )
- }
-
- #[test]
- fn test_item_table() {
- let html = indoc! {r##"
- <h2 id="structs" class="section-header">Structs<a href="#structs" class="anchor">§</a></h2>
- <ul class="item-table">
- <li><div class="item-name"><a class="struct" href="struct.Error.html" title="struct axum::Error">Error</a></div><div class="desc docblock-short">Errors that can happen when using axum.</div></li>
- <li><div class="item-name"><a class="struct" href="struct.Extension.html" title="struct axum::Extension">Extension</a></div><div class="desc docblock-short">Extractor and response for extensions.</div></li>
- <li><div class="item-name"><a class="struct" href="struct.Form.html" title="struct axum::Form">Form</a><span class="stab portability" title="Available on crate feature `form` only"><code>form</code></span></div><div class="desc docblock-short">URL encoded extractor and response.</div></li>
- <li><div class="item-name"><a class="struct" href="struct.Json.html" title="struct axum::Json">Json</a><span class="stab portability" title="Available on crate feature `json` only"><code>json</code></span></div><div class="desc docblock-short">JSON Extractor / Response.</div></li>
- <li><div class="item-name"><a class="struct" href="struct.Router.html" title="struct axum::Router">Router</a></div><div class="desc docblock-short">The router type for composing handlers and services.</div></li></ul>
- <h2 id="functions" class="section-header">Functions<a href="#functions" class="anchor">§</a></h2>
- <ul class="item-table">
- <li><div class="item-name"><a class="fn" href="fn.serve.html" title="fn axum::serve">serve</a><span class="stab portability" title="Available on crate feature `tokio` and (crate features `http1` or `http2`) only"><code>tokio</code> and (<code>http1</code> or <code>http2</code>)</span></div><div class="desc docblock-short">Serve the service with the supplied listener.</div></li>
- </ul>
- "##};
- let expected = indoc! {r#"
- ## Structs
-
- - `Error`: Errors that can happen when using axum.
- - `Extension`: Extractor and response for extensions.
- - `Form` [`form`]: URL encoded extractor and response.
- - `Json` [`json`]: JSON Extractor / Response.
- - `Router`: The router type for composing handlers and services.
-
- ## Functions
-
- - `serve` [`tokio` and (`http1` or `http2`)]: Serve the service with the supplied listener.
- "#}
- .trim();
-
- assert_eq!(
- convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
- expected
- )
- }
-
- #[test]
- fn test_table() {
- let html = indoc! {r##"
- <h2 id="feature-flags"><a class="doc-anchor" href="#feature-flags">§</a>Feature flags</h2>
- <p>axum uses a set of <a href="https://doc.rust-lang.org/cargo/reference/features.html#the-features-section">feature flags</a> to reduce the amount of compiled and
- optional dependencies.</p>
- <p>The following optional features are available:</p>
- <div><table><thead><tr><th>Name</th><th>Description</th><th>Default?</th></tr></thead><tbody>
- <tr><td><code>http1</code></td><td>Enables hyper’s <code>http1</code> feature</td><td>Yes</td></tr>
- <tr><td><code>http2</code></td><td>Enables hyper’s <code>http2</code> feature</td><td>No</td></tr>
- <tr><td><code>json</code></td><td>Enables the <a href="struct.Json.html" title="struct axum::Json"><code>Json</code></a> type and some similar convenience functionality</td><td>Yes</td></tr>
- <tr><td><code>macros</code></td><td>Enables optional utility macros</td><td>No</td></tr>
- <tr><td><code>matched-path</code></td><td>Enables capturing of every request’s router path and the <a href="extract/struct.MatchedPath.html" title="struct axum::extract::MatchedPath"><code>MatchedPath</code></a> extractor</td><td>Yes</td></tr>
- <tr><td><code>multipart</code></td><td>Enables parsing <code>multipart/form-data</code> requests with <a href="extract/struct.Multipart.html" title="struct axum::extract::Multipart"><code>Multipart</code></a></td><td>No</td></tr>
- <tr><td><code>original-uri</code></td><td>Enables capturing of every request’s original URI and the <a href="extract/struct.OriginalUri.html" title="struct axum::extract::OriginalUri"><code>OriginalUri</code></a> extractor</td><td>Yes</td></tr>
- <tr><td><code>tokio</code></td><td>Enables <code>tokio</code> as a dependency and <code>axum::serve</code>, <code>SSE</code> and <code>extract::connect_info</code> types.</td><td>Yes</td></tr>
- <tr><td><code>tower-log</code></td><td>Enables <code>tower</code>’s <code>log</code> feature</td><td>Yes</td></tr>
- <tr><td><code>tracing</code></td><td>Log rejections from built-in extractors</td><td>Yes</td></tr>
- <tr><td><code>ws</code></td><td>Enables WebSockets support via <a href="extract/ws/index.html" title="mod axum::extract::ws"><code>extract::ws</code></a></td><td>No</td></tr>
- <tr><td><code>form</code></td><td>Enables the <code>Form</code> extractor</td><td>Yes</td></tr>
- <tr><td><code>query</code></td><td>Enables the <code>Query</code> extractor</td><td>Yes</td></tr>
- </tbody></table>
- "##};
- let expected = indoc! {r#"
- ## Feature flags
-
- axum uses a set of feature flags to reduce the amount of compiled and optional dependencies.
-
- The following optional features are available:
-
- | Name | Description | Default? |
- | --- | --- | --- |
- | `http1` | Enables hyper’s `http1` feature | Yes |
- | `http2` | Enables hyper’s `http2` feature | No |
- | `json` | Enables the `Json` type and some similar convenience functionality | Yes |
- | `macros` | Enables optional utility macros | No |
- | `matched-path` | Enables capturing of every request’s router path and the `MatchedPath` extractor | Yes |
- | `multipart` | Enables parsing `multipart/form-data` requests with `Multipart` | No |
- | `original-uri` | Enables capturing of every request’s original URI and the `OriginalUri` extractor | Yes |
- | `tokio` | Enables `tokio` as a dependency and `axum::serve`, `SSE` and `extract::connect_info` types. | Yes |
- | `tower-log` | Enables `tower`’s `log` feature | Yes |
- | `tracing` | Log rejections from built-in extractors | Yes |
- | `ws` | Enables WebSockets support via `extract::ws` | No |
- | `form` | Enables the `Form` extractor | Yes |
- | `query` | Enables the `Query` extractor | Yes |
- "#}
- .trim();
-
- assert_eq!(
- convert_html_to_markdown(html.as_bytes(), &mut rustdoc_handlers()).unwrap(),
- expected
- )
- }
-}
@@ -1,62 +0,0 @@
-use std::sync::Arc;
-
-use collections::HashMap;
-use gpui::{App, BackgroundExecutor, Global, ReadGlobal, UpdateGlobal};
-use parking_lot::RwLock;
-
-use crate::{IndexedDocsProvider, IndexedDocsStore, ProviderId};
-
-struct GlobalIndexedDocsRegistry(Arc<IndexedDocsRegistry>);
-
-impl Global for GlobalIndexedDocsRegistry {}
-
-pub struct IndexedDocsRegistry {
- executor: BackgroundExecutor,
- stores_by_provider: RwLock<HashMap<ProviderId, Arc<IndexedDocsStore>>>,
-}
-
-impl IndexedDocsRegistry {
- pub fn global(cx: &App) -> Arc<Self> {
- GlobalIndexedDocsRegistry::global(cx).0.clone()
- }
-
- pub(crate) fn init_global(cx: &mut App) {
- GlobalIndexedDocsRegistry::set_global(
- cx,
- GlobalIndexedDocsRegistry(Arc::new(Self::new(cx.background_executor().clone()))),
- );
- }
-
- pub fn new(executor: BackgroundExecutor) -> Self {
- Self {
- executor,
- stores_by_provider: RwLock::new(HashMap::default()),
- }
- }
-
- pub fn list_providers(&self) -> Vec<ProviderId> {
- self.stores_by_provider
- .read()
- .keys()
- .cloned()
- .collect::<Vec<_>>()
- }
-
- pub fn register_provider(
- &self,
- provider: Box<dyn IndexedDocsProvider + Send + Sync + 'static>,
- ) {
- self.stores_by_provider.write().insert(
- provider.id(),
- Arc::new(IndexedDocsStore::new(provider, self.executor.clone())),
- );
- }
-
- pub fn unregister_provider(&self, provider_id: &ProviderId) {
- self.stores_by_provider.write().remove(provider_id);
- }
-
- pub fn get_provider_store(&self, provider_id: ProviderId) -> Option<Arc<IndexedDocsStore>> {
- self.stores_by_provider.read().get(&provider_id).cloned()
- }
-}
@@ -1,346 +0,0 @@
-use std::path::PathBuf;
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use anyhow::{Context as _, Result, anyhow};
-use async_trait::async_trait;
-use collections::HashMap;
-use derive_more::{Deref, Display};
-use futures::FutureExt;
-use futures::future::{self, BoxFuture, Shared};
-use fuzzy::StringMatchCandidate;
-use gpui::{App, BackgroundExecutor, Task};
-use heed::Database;
-use heed::types::SerdeBincode;
-use parking_lot::RwLock;
-use serde::{Deserialize, Serialize};
-use util::ResultExt;
-
-use crate::IndexedDocsRegistry;
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)]
-pub struct ProviderId(pub Arc<str>);
-
-/// The name of a package.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)]
-pub struct PackageName(Arc<str>);
-
-impl From<&str> for PackageName {
- fn from(value: &str) -> Self {
- Self(value.into())
- }
-}
-
-#[async_trait]
-pub trait IndexedDocsProvider {
- /// Returns the ID of this provider.
- fn id(&self) -> ProviderId;
-
- /// Returns the path to the database for this provider.
- fn database_path(&self) -> PathBuf;
-
- /// Returns a list of packages as suggestions to be included in the search
- /// results.
- ///
- /// This can be used to provide completions for known packages (e.g., from the
- /// local project or a registry) before a package has been indexed.
- async fn suggest_packages(&self) -> Result<Vec<PackageName>>;
-
- /// Indexes the package with the given name.
- async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()>;
-}
-
-/// A store for indexed docs.
-pub struct IndexedDocsStore {
- executor: BackgroundExecutor,
- database_future:
- Shared<BoxFuture<'static, Result<Arc<IndexedDocsDatabase>, Arc<anyhow::Error>>>>,
- provider: Box<dyn IndexedDocsProvider + Send + Sync + 'static>,
- indexing_tasks_by_package:
- RwLock<HashMap<PackageName, Shared<Task<Result<(), Arc<anyhow::Error>>>>>>,
- latest_errors_by_package: RwLock<HashMap<PackageName, Arc<str>>>,
-}
-
-impl IndexedDocsStore {
- pub fn try_global(provider: ProviderId, cx: &App) -> Result<Arc<Self>> {
- let registry = IndexedDocsRegistry::global(cx);
- registry
- .get_provider_store(provider.clone())
- .with_context(|| format!("no indexed docs store found for {provider}"))
- }
-
- pub fn new(
- provider: Box<dyn IndexedDocsProvider + Send + Sync + 'static>,
- executor: BackgroundExecutor,
- ) -> Self {
- let database_future = executor
- .spawn({
- let executor = executor.clone();
- let database_path = provider.database_path();
- async move { IndexedDocsDatabase::new(database_path, executor) }
- })
- .then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
- .boxed()
- .shared();
-
- Self {
- executor,
- database_future,
- provider,
- indexing_tasks_by_package: RwLock::new(HashMap::default()),
- latest_errors_by_package: RwLock::new(HashMap::default()),
- }
- }
-
- pub fn latest_error_for_package(&self, package: &PackageName) -> Option<Arc<str>> {
- self.latest_errors_by_package.read().get(package).cloned()
- }
-
- /// Returns whether the package with the given name is currently being indexed.
- pub fn is_indexing(&self, package: &PackageName) -> bool {
- self.indexing_tasks_by_package.read().contains_key(package)
- }
-
- pub async fn load(&self, key: String) -> Result<MarkdownDocs> {
- self.database_future
- .clone()
- .await
- .map_err(|err| anyhow!(err))?
- .load(key)
- .await
- }
-
- pub async fn load_many_by_prefix(&self, prefix: String) -> Result<Vec<(String, MarkdownDocs)>> {
- self.database_future
- .clone()
- .await
- .map_err(|err| anyhow!(err))?
- .load_many_by_prefix(prefix)
- .await
- }
-
- /// Returns whether any entries exist with the given prefix.
- pub async fn any_with_prefix(&self, prefix: String) -> Result<bool> {
- self.database_future
- .clone()
- .await
- .map_err(|err| anyhow!(err))?
- .any_with_prefix(prefix)
- .await
- }
-
- pub fn suggest_packages(self: Arc<Self>) -> Task<Result<Vec<PackageName>>> {
- let this = self.clone();
- self.executor
- .spawn(async move { this.provider.suggest_packages().await })
- }
-
- pub fn index(
- self: Arc<Self>,
- package: PackageName,
- ) -> Shared<Task<Result<(), Arc<anyhow::Error>>>> {
- if let Some(existing_task) = self.indexing_tasks_by_package.read().get(&package) {
- return existing_task.clone();
- }
-
- let indexing_task = self
- .executor
- .spawn({
- let this = self.clone();
- let package = package.clone();
- async move {
- let _finally = util::defer({
- let this = this.clone();
- let package = package.clone();
- move || {
- this.indexing_tasks_by_package.write().remove(&package);
- }
- });
-
- let index_task = {
- let package = package.clone();
- async {
- let database = this
- .database_future
- .clone()
- .await
- .map_err(|err| anyhow!(err))?;
- this.provider.index(package, database).await
- }
- };
-
- let result = index_task.await.map_err(Arc::new);
- match &result {
- Ok(_) => {
- this.latest_errors_by_package.write().remove(&package);
- }
- Err(err) => {
- this.latest_errors_by_package
- .write()
- .insert(package, err.to_string().into());
- }
- }
-
- result
- }
- })
- .shared();
-
- self.indexing_tasks_by_package
- .write()
- .insert(package, indexing_task.clone());
-
- indexing_task
- }
-
- pub fn search(&self, query: String) -> Task<Vec<String>> {
- let executor = self.executor.clone();
- let database_future = self.database_future.clone();
- self.executor.spawn(async move {
- let Some(database) = database_future.await.map_err(|err| anyhow!(err)).log_err() else {
- return Vec::new();
- };
-
- let Some(items) = database.keys().await.log_err() else {
- return Vec::new();
- };
-
- let candidates = items
- .iter()
- .enumerate()
- .map(|(ix, item_path)| StringMatchCandidate::new(ix, &item_path))
- .collect::<Vec<_>>();
-
- let matches = fuzzy::match_strings(
- &candidates,
- &query,
- false,
- true,
- 100,
- &AtomicBool::default(),
- executor,
- )
- .await;
-
- matches
- .into_iter()
- .map(|mat| items[mat.candidate_id].clone())
- .collect()
- })
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Display, Serialize, Deserialize)]
-pub struct MarkdownDocs(pub String);
-
-pub struct IndexedDocsDatabase {
- executor: BackgroundExecutor,
- env: heed::Env,
- entries: Database<SerdeBincode<String>, SerdeBincode<MarkdownDocs>>,
-}
-
-impl IndexedDocsDatabase {
- pub fn new(path: PathBuf, executor: BackgroundExecutor) -> Result<Self> {
- std::fs::create_dir_all(&path)?;
-
- const ONE_GB_IN_BYTES: usize = 1024 * 1024 * 1024;
- let env = unsafe {
- heed::EnvOpenOptions::new()
- .map_size(ONE_GB_IN_BYTES)
- .max_dbs(1)
- .open(path)?
- };
-
- let mut txn = env.write_txn()?;
- let entries = env.create_database(&mut txn, Some("rustdoc_entries"))?;
- txn.commit()?;
-
- Ok(Self {
- executor,
- env,
- entries,
- })
- }
-
- pub fn keys(&self) -> Task<Result<Vec<String>>> {
- let env = self.env.clone();
- let entries = self.entries;
-
- self.executor.spawn(async move {
- let txn = env.read_txn()?;
- let mut iter = entries.iter(&txn)?;
- let mut keys = Vec::new();
- while let Some((key, _value)) = iter.next().transpose()? {
- keys.push(key);
- }
-
- Ok(keys)
- })
- }
-
- pub fn load(&self, key: String) -> Task<Result<MarkdownDocs>> {
- let env = self.env.clone();
- let entries = self.entries;
-
- self.executor.spawn(async move {
- let txn = env.read_txn()?;
- entries
- .get(&txn, &key)?
- .with_context(|| format!("no docs found for {key}"))
- })
- }
-
- pub fn load_many_by_prefix(&self, prefix: String) -> Task<Result<Vec<(String, MarkdownDocs)>>> {
- let env = self.env.clone();
- let entries = self.entries;
-
- self.executor.spawn(async move {
- let txn = env.read_txn()?;
- let results = entries
- .iter(&txn)?
- .filter_map(|entry| {
- let (key, value) = entry.ok()?;
- if key.starts_with(&prefix) {
- Some((key, value))
- } else {
- None
- }
- })
- .collect::<Vec<_>>();
-
- Ok(results)
- })
- }
-
- /// Returns whether any entries exist with the given prefix.
- pub fn any_with_prefix(&self, prefix: String) -> Task<Result<bool>> {
- let env = self.env.clone();
- let entries = self.entries;
-
- self.executor.spawn(async move {
- let txn = env.read_txn()?;
- let any = entries
- .iter(&txn)?
- .any(|entry| entry.map_or(false, |(key, _value)| key.starts_with(&prefix)));
- Ok(any)
- })
- }
-
- pub fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
- let env = self.env.clone();
- let entries = self.entries;
-
- self.executor.spawn(async move {
- let mut txn = env.write_txn()?;
- entries.put(&mut txn, &key, &MarkdownDocs(docs))?;
- txn.commit()?;
- Ok(())
- })
- }
-}
-
-impl extension::KeyValueStoreDelegate for IndexedDocsDatabase {
- fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
- IndexedDocsDatabase::insert(&self, key, docs)
- }
-}
@@ -24,6 +24,7 @@ serde_json_lenient.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
+util_macros.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
@@ -14,7 +14,10 @@ use language::{
DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _,
};
use project::lsp_store::CompletionDocumentation;
-use project::{Completion, CompletionResponse, CompletionSource, Project, ProjectPath};
+use project::{
+ Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, Project,
+ ProjectPath,
+};
use std::fmt::Write as _;
use std::ops::Range;
use std::path::Path;
@@ -25,7 +28,7 @@ use util::split_str_with_ranges;
/// Path used for unsaved buffer that contains style json. To support the json language server, this
/// matches the name used in the generated schemas.
-const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json";
+const ZED_INSPECTOR_STYLE_JSON: &str = util_macros::path!("/zed-inspector-style.json");
pub(crate) struct DivInspector {
state: State,
@@ -93,8 +96,8 @@ impl DivInspector {
Ok((json_style_buffer, rust_style_buffer)) => {
this.update_in(cx, |this, window, cx| {
this.state = State::BuffersLoaded {
- json_style_buffer: json_style_buffer,
- rust_style_buffer: rust_style_buffer,
+ json_style_buffer,
+ rust_style_buffer,
};
// Initialize editors immediately instead of waiting for
@@ -200,8 +203,8 @@ impl DivInspector {
cx.subscribe_in(&json_style_editor, window, {
let id = id.clone();
let rust_style_buffer = rust_style_buffer.clone();
- move |this, editor, event: &EditorEvent, window, cx| match event {
- EditorEvent::BufferEdited => {
+ move |this, editor, event: &EditorEvent, window, cx| {
+ if event == &EditorEvent::BufferEdited {
let style_json = editor.read(cx).text(cx);
match serde_json_lenient::from_str_lenient::<StyleRefinement>(&style_json) {
Ok(new_style) => {
@@ -243,7 +246,6 @@ impl DivInspector {
Err(err) => this.json_style_error = Some(err.to_string().into()),
}
}
- _ => {}
}
})
.detach();
@@ -251,11 +253,10 @@ impl DivInspector {
cx.subscribe(&rust_style_editor, {
let json_style_buffer = json_style_buffer.clone();
let rust_style_buffer = rust_style_buffer.clone();
- move |this, _editor, event: &EditorEvent, cx| match event {
- EditorEvent::BufferEdited => {
+ move |this, _editor, event: &EditorEvent, cx| {
+ if let EditorEvent::BufferEdited = event {
this.update_json_style_from_rust(&json_style_buffer, &rust_style_buffer, cx);
}
- _ => {}
}
})
.detach();
@@ -271,23 +272,19 @@ impl DivInspector {
}
fn reset_style(&mut self, cx: &mut App) {
- match &self.state {
- State::Ready {
- rust_style_buffer,
- json_style_buffer,
- ..
- } => {
- if let Err(err) = self.reset_style_editors(
- &rust_style_buffer.clone(),
- &json_style_buffer.clone(),
- cx,
- ) {
- self.json_style_error = Some(format!("{err}").into());
- } else {
- self.json_style_error = None;
- }
+ if let State::Ready {
+ rust_style_buffer,
+ json_style_buffer,
+ ..
+ } = &self.state
+ {
+ if let Err(err) =
+ self.reset_style_editors(&rust_style_buffer.clone(), &json_style_buffer.clone(), cx)
+ {
+ self.json_style_error = Some(format!("{err}").into());
+ } else {
+ self.json_style_error = None;
}
- _ => {}
}
}
@@ -395,11 +392,11 @@ impl DivInspector {
.zip(self.rust_completion_replace_range.as_ref())
{
let before_text = snapshot
- .text_for_range(0..completion_range.start.to_offset(&snapshot))
+ .text_for_range(0..completion_range.start.to_offset(snapshot))
.collect::<String>();
let after_text = snapshot
.text_for_range(
- completion_range.end.to_offset(&snapshot)
+ completion_range.end.to_offset(snapshot)
..snapshot.clip_offset(usize::MAX, Bias::Left),
)
.collect::<String>();
@@ -670,6 +667,7 @@ impl CompletionProvider for RustStyleCompletionProvider {
confirm: None,
})
.collect(),
+ display_options: CompletionDisplayOptions::default(),
is_incomplete: false,
}]))
}
@@ -702,10 +700,10 @@ impl CompletionProvider for RustStyleCompletionProvider {
}
fn completion_replace_range(snapshot: &BufferSnapshot, anchor: &Anchor) -> Option<Range<Anchor>> {
- let point = anchor.to_point(&snapshot);
- let offset = point.to_offset(&snapshot);
- let line_start = Point::new(point.row, 0).to_offset(&snapshot);
- let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(&snapshot);
+ let point = anchor.to_point(snapshot);
+ let offset = point.to_offset(snapshot);
+ let line_start = Point::new(point.row, 0).to_offset(snapshot);
+ let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(snapshot);
let mut lines = snapshot.text_for_range(line_start..line_end).lines();
let line = lines.next()?;
@@ -1,112 +1,7 @@
-use anyhow::{Context as _, Result};
-use client::ZED_URL_SCHEME;
-use gpui::{AppContext as _, AsyncApp, Context, PromptLevel, Window, actions};
-use release_channel::ReleaseChannel;
-use std::ops::Deref;
-use std::path::{Path, PathBuf};
-use util::ResultExt;
-use workspace::notifications::{DetachAndPromptErr, NotificationId};
-use workspace::{Toast, Workspace};
+#[cfg(not(target_os = "windows"))]
+mod install_cli_binary;
+mod register_zed_scheme;
-actions!(
- cli,
- [
- /// Installs the Zed CLI tool to the system PATH.
- Install,
- /// Registers the zed:// URL scheme handler.
- RegisterZedScheme
- ]
-);
-
-async fn install_script(cx: &AsyncApp) -> Result<PathBuf> {
- let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??;
- let link_path = Path::new("/usr/local/bin/zed");
- let bin_dir_path = link_path.parent().unwrap();
-
- // Don't re-create symlink if it points to the same CLI binary.
- if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
- return Ok(link_path.into());
- }
-
- // If the symlink is not there or is outdated, first try replacing it
- // without escalating.
- smol::fs::remove_file(link_path).await.log_err();
- // todo("windows")
- #[cfg(not(windows))]
- {
- if smol::fs::unix::symlink(&cli_path, link_path)
- .await
- .log_err()
- .is_some()
- {
- return Ok(link_path.into());
- }
- }
-
- // The symlink could not be created, so use osascript with admin privileges
- // to create it.
- let status = smol::process::Command::new("/usr/bin/osascript")
- .args([
- "-e",
- &format!(
- "do shell script \" \
- mkdir -p \'{}\' && \
- ln -sf \'{}\' \'{}\' \
- \" with administrator privileges",
- bin_dir_path.to_string_lossy(),
- cli_path.to_string_lossy(),
- link_path.to_string_lossy(),
- ),
- ])
- .stdout(smol::process::Stdio::inherit())
- .stderr(smol::process::Stdio::inherit())
- .output()
- .await?
- .status;
- anyhow::ensure!(status.success(), "error running osascript");
- Ok(link_path.into())
-}
-
-pub async fn register_zed_scheme(cx: &AsyncApp) -> anyhow::Result<()> {
- cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME))?
- .await
-}
-
-pub fn install_cli(window: &mut Window, cx: &mut Context<Workspace>) {
- const LINUX_PROMPT_DETAIL: &str = "If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source like your package manager, then you may need to create an alias/symlink manually.\n\nDepending on your package manager, the CLI might be named zeditor, zedit, zed-editor or something else.";
-
- cx.spawn_in(window, async move |workspace, cx| {
- if cfg!(any(target_os = "linux", target_os = "freebsd")) {
- let prompt = cx.prompt(
- PromptLevel::Warning,
- "CLI should already be installed",
- Some(LINUX_PROMPT_DETAIL),
- &["Ok"],
- );
- cx.background_spawn(prompt).detach();
- return Ok(());
- }
- let path = install_script(cx.deref())
- .await
- .context("error creating CLI symlink")?;
-
- workspace.update_in(cx, |workspace, _, cx| {
- struct InstalledZedCli;
-
- workspace.show_toast(
- Toast::new(
- NotificationId::unique::<InstalledZedCli>(),
- format!(
- "Installed `zed` to {}. You can launch {} from your terminal.",
- path.to_string_lossy(),
- ReleaseChannel::global(cx).display_name()
- ),
- ),
- cx,
- )
- })?;
- register_zed_scheme(&cx).await.log_err();
- Ok(())
- })
- .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None);
-}
+#[cfg(not(target_os = "windows"))]
+pub use install_cli_binary::{InstallCliBinary, install_cli_binary};
+pub use register_zed_scheme::{RegisterZedScheme, register_zed_scheme};
@@ -0,0 +1,101 @@
+use super::register_zed_scheme;
+use anyhow::{Context as _, Result};
+use gpui::{AppContext as _, AsyncApp, Context, PromptLevel, Window, actions};
+use release_channel::ReleaseChannel;
+use std::ops::Deref;
+use std::path::{Path, PathBuf};
+use util::ResultExt;
+use workspace::notifications::{DetachAndPromptErr, NotificationId};
+use workspace::{Toast, Workspace};
+
+actions!(
+ cli,
+ [
+ /// Installs the Zed CLI tool to the system PATH.
+ InstallCliBinary,
+ ]
+);
+
+async fn install_script(cx: &AsyncApp) -> Result<PathBuf> {
+ let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??;
+ let link_path = Path::new("/usr/local/bin/zed");
+ let bin_dir_path = link_path.parent().unwrap();
+
+ // Don't re-create symlink if it points to the same CLI binary.
+ if smol::fs::read_link(link_path).await.ok().as_ref() == Some(&cli_path) {
+ return Ok(link_path.into());
+ }
+
+ // If the symlink is not there or is outdated, first try replacing it
+ // without escalating.
+ smol::fs::remove_file(link_path).await.log_err();
+ if smol::fs::unix::symlink(&cli_path, link_path)
+ .await
+ .log_err()
+ .is_some()
+ {
+ return Ok(link_path.into());
+ }
+
+ // The symlink could not be created, so use osascript with admin privileges
+ // to create it.
+ let status = smol::process::Command::new("/usr/bin/osascript")
+ .args([
+ "-e",
+ &format!(
+ "do shell script \" \
+ mkdir -p \'{}\' && \
+ ln -sf \'{}\' \'{}\' \
+ \" with administrator privileges",
+ bin_dir_path.to_string_lossy(),
+ cli_path.to_string_lossy(),
+ link_path.to_string_lossy(),
+ ),
+ ])
+ .stdout(smol::process::Stdio::inherit())
+ .stderr(smol::process::Stdio::inherit())
+ .output()
+ .await?
+ .status;
+ anyhow::ensure!(status.success(), "error running osascript");
+ Ok(link_path.into())
+}
+
+pub fn install_cli_binary(window: &mut Window, cx: &mut Context<Workspace>) {
+ const LINUX_PROMPT_DETAIL: &str = "If you installed Zed from our official release add ~/.local/bin to your PATH.\n\nIf you installed Zed from a different source like your package manager, then you may need to create an alias/symlink manually.\n\nDepending on your package manager, the CLI might be named zeditor, zedit, zed-editor or something else.";
+
+ cx.spawn_in(window, async move |workspace, cx| {
+ if cfg!(any(target_os = "linux", target_os = "freebsd")) {
+ let prompt = cx.prompt(
+ PromptLevel::Warning,
+ "CLI should already be installed",
+ Some(LINUX_PROMPT_DETAIL),
+ &["Ok"],
+ );
+ cx.background_spawn(prompt).detach();
+ return Ok(());
+ }
+ let path = install_script(cx.deref())
+ .await
+ .context("error creating CLI symlink")?;
+
+ workspace.update_in(cx, |workspace, _, cx| {
+ struct InstalledZedCli;
+
+ workspace.show_toast(
+ Toast::new(
+ NotificationId::unique::<InstalledZedCli>(),
+ format!(
+ "Installed `zed` to {}. You can launch {} from your terminal.",
+ path.to_string_lossy(),
+ ReleaseChannel::global(cx).display_name()
+ ),
+ ),
+ cx,
+ )
+ })?;
+ register_zed_scheme(cx).await.log_err();
+ Ok(())
+ })
+ .detach_and_prompt_err("Error installing zed cli", window, cx, |_, _, _| None);
+}
@@ -0,0 +1,15 @@
+use client::ZED_URL_SCHEME;
+use gpui::{AsyncApp, actions};
+
+actions!(
+ cli,
+ [
+ /// Registers the zed:// URL scheme handler.
+ RegisterZedScheme
+ ]
+);
+
+pub async fn register_zed_scheme(cx: &AsyncApp) -> anyhow::Result<()> {
+ cx.update(|cx| cx.register_url_scheme(ZED_URL_SCHEME))?
+ .await
+}
@@ -50,16 +50,13 @@ impl RealJujutsuRepository {
impl JujutsuRepository for RealJujutsuRepository {
fn list_bookmarks(&self) -> Vec<Bookmark> {
- let bookmarks = self
- .repository
+ self.repository
.view()
.bookmarks()
.map(|(ref_name, _target)| Bookmark {
ref_name: ref_name.as_str().to_string().into(),
})
- .collect();
-
- bookmarks
+ .collect()
}
}
@@ -16,7 +16,7 @@ pub struct JujutsuStore {
impl JujutsuStore {
pub fn init_global(cx: &mut App) {
- let Some(repository) = RealJujutsuRepository::new(&Path::new(".")).ok() else {
+ let Some(repository) = RealJujutsuRepository::new(Path::new(".")).ok() else {
return;
};
@@ -182,7 +182,7 @@ impl PickerDelegate for BookmarkPickerDelegate {
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let entry = &self.matches[ix];
+ let entry = &self.matches.get(ix)?;
Some(
ListItem::new(ix)
@@ -5,7 +5,7 @@ use editor::{Editor, SelectionEffects};
use gpui::{App, AppContext as _, Context, Window, actions};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use std::{
fs::OpenOptions,
path::{Path, PathBuf},
@@ -22,7 +22,8 @@ actions!(
);
/// Settings specific to journaling
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
+#[settings_key(key = "journal")]
pub struct JournalSettings {
/// The path of the directory where journal entries are stored.
///
@@ -52,8 +53,6 @@ pub enum HourFormat {
}
impl settings::Settings for JournalSettings {
- const KEY: Option<&'static str> = Some("journal");
-
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -123,7 +122,7 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
}
let app_state = workspace.app_state().clone();
- let view_snapshot = workspace.weak_handle().clone();
+ let view_snapshot = workspace.weak_handle();
window
.spawn(cx, async move |cx| {
@@ -170,23 +169,23 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
.await
};
- if let Some(Some(Ok(item))) = opened.first() {
- if let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade()) {
- editor.update_in(cx, |editor, window, cx| {
- let len = editor.buffer().read(cx).len(cx);
- editor.change_selections(
- SelectionEffects::scroll(Autoscroll::center()),
- window,
- cx,
- |s| s.select_ranges([len..len]),
- );
- if len > 0 {
- editor.insert("\n\n", window, cx);
- }
- editor.insert(&entry_heading, window, cx);
+ if let Some(Some(Ok(item))) = opened.first()
+ && let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade())
+ {
+ editor.update_in(cx, |editor, window, cx| {
+ let len = editor.buffer().read(cx).len(cx);
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::center()),
+ window,
+ cx,
+ |s| s.select_ranges([len..len]),
+ );
+ if len > 0 {
editor.insert("\n\n", window, cx);
- })?;
- }
+ }
+ editor.insert(&entry_heading, window, cx);
+ editor.insert("\n\n", window, cx);
+ })?;
}
anyhow::Ok(())
@@ -195,11 +194,9 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
}
fn journal_dir(path: &str) -> Option<PathBuf> {
- let expanded_journal_dir = shellexpand::full(path) //TODO handle this better
+ shellexpand::full(path) //TODO handle this better
.ok()
- .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal"));
-
- expanded_journal_dir
+ .map(|dir| Path::new(&dir.to_string()).to_path_buf().join("journal"))
}
fn heading_entry(now: NaiveTime, hour_format: &Option<HourFormat>) -> String {
@@ -0,0 +1,53 @@
+[package]
+name = "keymap_editor"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/keymap_editor.rs"
+
+[dependencies]
+anyhow.workspace = true
+collections.workspace = true
+command_palette.workspace = true
+component.workspace = true
+db.workspace = true
+editor.workspace = true
+fs.workspace = true
+fuzzy.workspace = true
+gpui.workspace = true
+itertools.workspace = true
+language.workspace = true
+log.workspace = true
+menu.workspace = true
+notifications.workspace = true
+paths.workspace = true
+project.workspace = true
+search.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+settings.workspace = true
+telemetry.workspace = true
+tempfile.workspace = true
+theme.workspace = true
+tree-sitter-json.workspace = true
+tree-sitter-rust.workspace = true
+ui.workspace = true
+ui_input.workspace = true
+util.workspace = true
+vim.workspace = true
+workspace-hack.workspace = true
+workspace.workspace = true
+zed_actions.workspace = true
+
+[dev-dependencies]
+db = {"workspace"= true, "features" = ["test-support"]}
+fs = { workspace = true, features = ["test-support"] }
+gpui = { workspace = true, features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
+workspace = { workspace = true, features = ["test-support"] }
@@ -5,6 +5,8 @@ use std::{
time::Duration,
};
+mod ui_components;
+
use anyhow::{Context as _, anyhow};
use collections::{HashMap, HashSet};
use editor::{CompletionProvider, Editor, EditorEvent};
@@ -12,18 +14,20 @@ use fs::Fs;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
- EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, Keystroke, MouseButton,
- Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,
- TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,
+ EventEmitter, FocusHandle, Focusable, Global, IsZero,
+ KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
+ KeyContext, KeybindingKeystroke, MouseButton, PlatformKeyboardMapper, Point, ScrollStrategy,
+ ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity,
+ actions, anchored, deferred, div,
};
use language::{Language, LanguageConfig, ToOffset as _};
use notifications::status_toast::{StatusToast, ToastIcon};
-use project::Project;
+use project::{CompletionDisplayOptions, Project};
use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets};
use ui::{
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator,
Modal, ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString,
- Styled as _, Tooltip, Window, prelude::*,
+ Styled as _, Tooltip, Window, prelude::*, right_click_menu,
};
use ui_input::SingleLineInput;
use util::ResultExt;
@@ -32,8 +36,11 @@ use workspace::{
register_serializable_item,
};
+pub use ui_components::*;
+use zed_actions::OpenKeymapEditor;
+
use crate::{
- keybindings::persistence::KEYBINDING_EDITORS,
+ persistence::KEYBINDING_EDITORS,
ui_components::{
keystroke_input::{ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording},
table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState},
@@ -42,14 +49,6 @@ use crate::{
const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
-actions!(
- zed,
- [
- /// Opens the keymap editor.
- OpenKeymapEditor
- ]
-);
-
actions!(
keymap_editor,
[
@@ -172,7 +171,7 @@ impl FilterState {
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
struct ActionMapping {
- keystrokes: Vec<Keystroke>,
+ keystrokes: Vec<KeybindingKeystroke>,
context: Option<SharedString>,
}
@@ -182,15 +181,6 @@ struct KeybindConflict {
remaining_conflict_amount: usize,
}
-impl KeybindConflict {
- fn from_iter<'a>(mut indices: impl Iterator<Item = &'a ConflictOrigin>) -> Option<Self> {
- indices.next().map(|origin| Self {
- first_conflict_index: origin.index,
- remaining_conflict_amount: indices.count(),
- })
- }
-}
-
#[derive(Clone, Copy, PartialEq)]
struct ConflictOrigin {
override_source: KeybindSource,
@@ -238,13 +228,21 @@ impl ConflictOrigin {
#[derive(Default)]
struct ConflictState {
conflicts: Vec<Option<ConflictOrigin>>,
- keybind_mapping: HashMap<ActionMapping, Vec<ConflictOrigin>>,
+ keybind_mapping: ConflictKeybindMapping,
has_user_conflicts: bool,
}
+type ConflictKeybindMapping = HashMap<
+ Vec<KeybindingKeystroke>,
+ Vec<(
+ Option<gpui::KeyBindingContextPredicate>,
+ Vec<ConflictOrigin>,
+ )>,
+>;
+
impl ConflictState {
fn new(key_bindings: &[ProcessedBinding]) -> Self {
- let mut action_keybind_mapping: HashMap<_, Vec<ConflictOrigin>> = HashMap::default();
+ let mut action_keybind_mapping = ConflictKeybindMapping::default();
let mut largest_index = 0;
for (index, binding) in key_bindings
@@ -252,29 +250,48 @@ impl ConflictState {
.enumerate()
.flat_map(|(index, binding)| Some(index).zip(binding.keybind_information()))
{
- action_keybind_mapping
- .entry(binding.get_action_mapping())
- .or_default()
- .push(ConflictOrigin::new(binding.source, index));
+ let mapping = binding.get_action_mapping();
+ let predicate = mapping
+ .context
+ .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok());
+ let entry = action_keybind_mapping
+ .entry(mapping.keystrokes)
+ .or_default();
+ let origin = ConflictOrigin::new(binding.source, index);
+ if let Some((_, origins)) =
+ entry
+ .iter_mut()
+ .find(|(other_predicate, _)| match (&predicate, other_predicate) {
+ (None, None) => true,
+ (Some(a), Some(b)) => normalized_ctx_eq(a, b),
+ _ => false,
+ })
+ {
+ origins.push(origin);
+ } else {
+ entry.push((predicate, vec![origin]));
+ }
largest_index = index;
}
let mut conflicts = vec![None; largest_index + 1];
let mut has_user_conflicts = false;
- for indices in action_keybind_mapping.values_mut() {
- indices.sort_unstable_by_key(|origin| origin.override_source);
- let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else {
- continue;
- };
+ for entries in action_keybind_mapping.values_mut() {
+ for (_, indices) in entries.iter_mut() {
+ indices.sort_unstable_by_key(|origin| origin.override_source);
+ let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else {
+ continue;
+ };
- for origin in indices.iter() {
- conflicts[origin.index] =
- origin.get_conflict_with(if origin == fst { &snd } else { &fst })
- }
+ for origin in indices.iter() {
+ conflicts[origin.index] =
+ origin.get_conflict_with(if origin == fst { snd } else { fst })
+ }
- has_user_conflicts |= fst.override_source == KeybindSource::User
- && snd.override_source == KeybindSource::User;
+ has_user_conflicts |= fst.override_source == KeybindSource::User
+ && snd.override_source == KeybindSource::User;
+ }
}
Self {
@@ -289,15 +306,34 @@ impl ConflictState {
action_mapping: &ActionMapping,
keybind_idx: Option<usize>,
) -> Option<KeybindConflict> {
- self.keybind_mapping
- .get(action_mapping)
- .and_then(|indices| {
- KeybindConflict::from_iter(
- indices
+ let ActionMapping {
+ keystrokes,
+ context,
+ } = action_mapping;
+ let predicate = context
+ .as_deref()
+ .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok());
+ self.keybind_mapping.get(keystrokes).and_then(|entries| {
+ entries
+ .iter()
+ .find_map(|(other_predicate, indices)| {
+ match (&predicate, other_predicate) {
+ (None, None) => true,
+ (Some(pred), Some(other)) => normalized_ctx_eq(pred, other),
+ _ => false,
+ }
+ .then_some(indices)
+ })
+ .and_then(|indices| {
+ let mut indices = indices
.iter()
- .filter(|&conflict| Some(conflict.index) != keybind_idx),
- )
- })
+ .filter(|&conflict| Some(conflict.index) != keybind_idx);
+ indices.next().map(|origin| KeybindConflict {
+ first_conflict_index: origin.index,
+ remaining_conflict_amount: indices.count(),
+ })
+ })
+ })
}
fn conflict_for_idx(&self, idx: usize) -> Option<ConflictOrigin> {
@@ -375,12 +411,14 @@ impl Focusable for KeymapEditor {
}
}
/// Helper function to check if two keystroke sequences match exactly
-fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool {
+fn keystrokes_match_exactly(
+ keystrokes1: &[KeybindingKeystroke],
+ keystrokes2: &[KeybindingKeystroke],
+) -> bool {
keystrokes1.len() == keystrokes2.len()
- && keystrokes1
- .iter()
- .zip(keystrokes2)
- .all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers)
+ && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {
+ k1.inner().key == k2.inner().key && k1.inner().modifiers == k2.inner().modifiers
+ })
}
impl KeymapEditor {
@@ -397,7 +435,7 @@ impl KeymapEditor {
let filter_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("Filter action names…", cx);
+ editor.set_placeholder_text("Filter action names…", window, cx);
editor
});
@@ -470,15 +508,9 @@ impl KeymapEditor {
self.filter_editor.read(cx).text(cx)
}
- fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
+ fn current_keystroke_query(&self, cx: &App) -> Vec<KeybindingKeystroke> {
match self.search_mode {
- SearchMode::KeyStroke { .. } => self
- .keystroke_editor
- .read(cx)
- .keystrokes()
- .iter()
- .cloned()
- .collect(),
+ SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
SearchMode::Normal => Default::default(),
}
}
@@ -497,7 +529,7 @@ impl KeymapEditor {
let keystroke_query = keystroke_query
.into_iter()
- .map(|keystroke| keystroke.unparse())
+ .map(|keystroke| keystroke.inner().unparse())
.collect::<Vec<String>>()
.join(" ");
@@ -521,7 +553,7 @@ impl KeymapEditor {
async fn update_matches(
this: WeakEntity<Self>,
action_query: String,
- keystroke_query: Vec<Keystroke>,
+ keystroke_query: Vec<KeybindingKeystroke>,
cx: &mut AsyncApp,
) -> anyhow::Result<()> {
let action_query = command_palette::normalize_action_query(&action_query);
@@ -559,7 +591,7 @@ impl KeymapEditor {
if exact_match {
keystrokes_match_exactly(&keystroke_query, keystrokes)
} else if keystroke_query.len() > keystrokes.len() {
- return false;
+ false
} else {
for keystroke_offset in 0..keystrokes.len() {
let mut found_count = 0;
@@ -570,16 +602,15 @@ impl KeymapEditor {
{
let query = &keystroke_query[query_cursor];
let keystroke = &keystrokes[keystroke_cursor];
- let matches =
- query.modifiers.is_subset_of(&keystroke.modifiers)
- && ((query.key.is_empty()
- || query.key == keystroke.key)
- && query
- .key_char
- .as_ref()
- .map_or(true, |q_kc| {
- q_kc == &keystroke.key
- }));
+ let matches = query
+ .inner()
+ .modifiers
+ .is_subset_of(&keystroke.inner().modifiers)
+ && ((query.inner().key.is_empty()
+ || query.inner().key == keystroke.inner().key)
+ && query.inner().key_char.as_ref().is_none_or(
+ |q_kc| q_kc == &keystroke.inner().key,
+ ));
if matches {
found_count += 1;
query_cursor += 1;
@@ -591,7 +622,7 @@ impl KeymapEditor {
return true;
}
}
- return false;
+ false
}
})
});
@@ -630,8 +661,7 @@ impl KeymapEditor {
let key_bindings_ptr = cx.key_bindings();
let lock = key_bindings_ptr.borrow();
let key_bindings = lock.bindings();
- let mut unmapped_action_names =
- HashSet::from_iter(cx.all_action_names().into_iter().copied());
+ let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names().iter().copied());
let action_documentation = cx.action_documentation();
let mut generator = KeymapFile::action_schema_generator();
let actions_with_schemas = HashSet::from_iter(
@@ -649,7 +679,7 @@ impl KeymapEditor {
.map(KeybindSource::from_meta)
.unwrap_or(KeybindSource::Unknown);
- let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
+ let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx);
let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
.vim_mode(source == KeybindSource::Vim);
@@ -673,8 +703,8 @@ impl KeymapEditor {
action_name,
action_arguments,
&actions_with_schemas,
- &action_documentation,
- &humanized_action_names,
+ action_documentation,
+ humanized_action_names,
);
let index = processed_bindings.len();
@@ -696,8 +726,8 @@ impl KeymapEditor {
action_name,
None,
&actions_with_schemas,
- &action_documentation,
- &humanized_action_names,
+ action_documentation,
+ humanized_action_names,
);
let string_match_candidate =
StringMatchCandidate::new(index, &action_information.humanized_name);
@@ -1173,8 +1203,11 @@ impl KeymapEditor {
.read(cx)
.get_scrollbar_offset(Axis::Vertical),
));
- cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
- .detach_and_notify_err(window, cx);
+ let keyboard_mapper = cx.keyboard_mapper().clone();
+ cx.spawn(async move |_, _| {
+ remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await
+ })
+ .detach_and_notify_err(window, cx);
}
fn copy_context_to_clipboard(
@@ -1192,8 +1225,8 @@ impl KeymapEditor {
return;
};
- telemetry::event!("Keybinding Context Copied", context = context.clone());
- cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone()));
+ telemetry::event!("Keybinding Context Copied", context = context);
+ cx.write_to_clipboard(gpui::ClipboardItem::new_string(context));
}
fn copy_action_to_clipboard(
@@ -1209,8 +1242,8 @@ impl KeymapEditor {
return;
};
- telemetry::event!("Keybinding Action Copied", action = action.clone());
- cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone()));
+ telemetry::event!("Keybinding Action Copied", action = action);
+ cx.write_to_clipboard(gpui::ClipboardItem::new_string(action));
}
fn toggle_conflict_filter(
@@ -1298,7 +1331,7 @@ struct HumanizedActionNameCache {
impl HumanizedActionNameCache {
fn new(cx: &App) -> Self {
- let cache = HashMap::from_iter(cx.all_action_names().into_iter().map(|&action_name| {
+ let cache = HashMap::from_iter(cx.all_action_names().iter().map(|&action_name| {
(
action_name,
command_palette::humanize_action_name(action_name).into(),
@@ -1393,7 +1426,7 @@ impl ProcessedBinding {
.map(|keybind| keybind.get_action_mapping())
}
- fn keystrokes(&self) -> Option<&[Keystroke]> {
+ fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> {
self.ui_key_binding()
.map(|binding| binding.keystrokes.as_slice())
}
@@ -1474,7 +1507,7 @@ impl RenderOnce for KeybindContextString {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
match self {
KeybindContextString::Global => {
- muted_styled_text(KeybindContextString::GLOBAL.clone(), cx).into_any_element()
+ muted_styled_text(KeybindContextString::GLOBAL, cx).into_any_element()
}
KeybindContextString::Local(name, language) => {
SyntaxHighlightedText::new(name, language).into_any_element()
@@ -1550,73 +1583,139 @@ impl Render for KeymapEditor {
.py_1()
.border_1()
.border_color(theme.colors().border)
- .rounded_lg()
+ .rounded_md()
.child(self.filter_editor.clone()),
)
.child(
- IconButton::new(
- "KeymapEditorToggleFiltersIcon",
- IconName::Keyboard,
- )
- .shape(ui::IconButtonShape::Square)
- .tooltip({
- let focus_handle = focus_handle.clone();
-
- move |window, cx| {
- Tooltip::for_action_in(
- "Search by Keystroke",
- &ToggleKeystrokeSearch,
- &focus_handle.clone(),
- window,
- cx,
+ h_flex()
+ .gap_1()
+ .min_w_64()
+ .child(
+ IconButton::new(
+ "KeymapEditorToggleFiltersIcon",
+ IconName::Keyboard,
)
- }
- })
- .toggle_state(matches!(
- self.search_mode,
- SearchMode::KeyStroke { .. }
- ))
- .on_click(|_, window, cx| {
- window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx);
- }),
- )
- .child(
- IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
- .shape(ui::IconButtonShape::Square)
- .when(
- self.keybinding_conflict_state.any_user_binding_conflicts(),
- |this| {
- this.indicator(Indicator::dot().color(Color::Warning))
- },
+ .icon_size(IconSize::Small)
+ .tooltip({
+ let focus_handle = focus_handle.clone();
+
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Search by Keystroke",
+ &ToggleKeystrokeSearch,
+ &focus_handle.clone(),
+ window,
+ cx,
+ )
+ }
+ })
+ .toggle_state(matches!(
+ self.search_mode,
+ SearchMode::KeyStroke { .. }
+ ))
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ ToggleKeystrokeSearch.boxed_clone(),
+ cx,
+ );
+ }),
)
- .tooltip({
- let filter_state = self.filter_state;
- let focus_handle = focus_handle.clone();
-
- move |window, cx| {
- Tooltip::for_action_in(
- match filter_state {
- FilterState::All => "Show Conflicts",
- FilterState::Conflicts => "Hide Conflicts",
+ .child(
+ IconButton::new("KeymapEditorConflictIcon", IconName::Warning)
+ .icon_size(IconSize::Small)
+ .when(
+ self.keybinding_conflict_state
+ .any_user_binding_conflicts(),
+ |this| {
+ this.indicator(
+ Indicator::dot().color(Color::Warning),
+ )
},
- &ToggleConflictFilter,
- &focus_handle.clone(),
- window,
- cx,
)
- }
- })
- .selected_icon_color(Color::Warning)
- .toggle_state(matches!(
- self.filter_state,
- FilterState::Conflicts
- ))
- .on_click(|_, window, cx| {
- window.dispatch_action(
- ToggleConflictFilter.boxed_clone(),
- cx,
- );
- }),
+ .tooltip({
+ let filter_state = self.filter_state;
+ let focus_handle = focus_handle.clone();
+
+ move |window, cx| {
+ Tooltip::for_action_in(
+ match filter_state {
+ FilterState::All => "Show Conflicts",
+ FilterState::Conflicts => {
+ "Hide Conflicts"
+ }
+ },
+ &ToggleConflictFilter,
+ &focus_handle.clone(),
+ window,
+ cx,
+ )
+ }
+ })
+ .selected_icon_color(Color::Warning)
+ .toggle_state(matches!(
+ self.filter_state,
+ FilterState::Conflicts
+ ))
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ ToggleConflictFilter.boxed_clone(),
+ cx,
+ );
+ }),
+ )
+ .child(
+ div()
+ .ml_1()
+ .pl_2()
+ .border_l_1()
+ .border_color(cx.theme().colors().border_variant)
+ .child(
+ right_click_menu("open-keymap-menu")
+ .menu(|window, cx| {
+ ContextMenu::build(window, cx, |menu, _, _| {
+ menu.header("Open Keymap JSON")
+ .action(
+ "User",
+ zed_actions::OpenKeymap.boxed_clone(),
+ )
+ .action(
+ "Zed Default",
+ zed_actions::OpenDefaultKeymap
+ .boxed_clone(),
+ )
+ .action(
+ "Vim Default",
+ vim::OpenDefaultKeymap.boxed_clone(),
+ )
+ })
+ })
+ .anchor(gpui::Corner::TopLeft)
+ .trigger(|open, _, _| {
+ IconButton::new(
+ "OpenKeymapJsonButton",
+ IconName::Json,
+ )
+ .icon_size(IconSize::Small)
+ .when(!open, |this| {
+ this.tooltip(move |window, cx| {
+ Tooltip::with_meta(
+ "Open keymap.json",
+ Some(&zed_actions::OpenKeymap),
+ "Right click to view more options",
+ window,
+ cx,
+ )
+ })
+ })
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ zed_actions::OpenKeymap.boxed_clone(),
+ cx,
+ );
+ })
+ }),
+ ),
+ )
),
)
.when_some(
@@ -1627,48 +1726,42 @@ impl Render for KeymapEditor {
|this, exact_match| {
this.child(
h_flex()
- .map(|this| {
- if self
- .keybinding_conflict_state
- .any_user_binding_conflicts()
- {
- this.pr(rems_from_px(54.))
- } else {
- this.pr_7()
- }
- })
.gap_2()
.child(self.keystroke_editor.clone())
.child(
- IconButton::new(
- "keystrokes-exact-match",
- IconName::CaseSensitive,
- )
- .tooltip({
- let keystroke_focus_handle =
- self.keystroke_editor.read(cx).focus_handle(cx);
-
- move |window, cx| {
- Tooltip::for_action_in(
- "Toggle Exact Match Mode",
- &ToggleExactKeystrokeMatching,
- &keystroke_focus_handle,
- window,
- cx,
+ h_flex()
+ .min_w_64()
+ .child(
+ IconButton::new(
+ "keystrokes-exact-match",
+ IconName::CaseSensitive,
)
- }
- })
- .shape(IconButtonShape::Square)
- .toggle_state(exact_match)
- .on_click(
- cx.listener(|_, _, window, cx| {
- window.dispatch_action(
- ToggleExactKeystrokeMatching.boxed_clone(),
- cx,
- );
- }),
- ),
- ),
+ .tooltip({
+ let keystroke_focus_handle =
+ self.keystroke_editor.read(cx).focus_handle(cx);
+
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Toggle Exact Match Mode",
+ &ToggleExactKeystrokeMatching,
+ &keystroke_focus_handle,
+ window,
+ cx,
+ )
+ }
+ })
+ .shape(IconButtonShape::Square)
+ .toggle_state(exact_match)
+ .on_click(
+ cx.listener(|_, _, window, cx| {
+ window.dispatch_action(
+ ToggleExactKeystrokeMatching.boxed_clone(),
+ cx,
+ );
+ }),
+ ),
+ ),
+ )
)
},
),
@@ -1731,7 +1824,7 @@ impl Render for KeymapEditor {
} else {
const NULL: SharedString =
SharedString::new_static("<null>");
- muted_styled_text(NULL.clone(), cx)
+ muted_styled_text(NULL, cx)
.into_any_element()
}
})
@@ -1839,18 +1932,15 @@ impl Render for KeymapEditor {
mouse_down_event: &gpui::MouseDownEvent,
window,
cx| {
- match mouse_down_event.button {
- MouseButton::Right => {
- this.select_index(
- row_index, None, window, cx,
- );
- this.create_context_menu(
- mouse_down_event.position,
- window,
- cx,
- );
- }
- _ => {}
+ if mouse_down_event.button == MouseButton::Right {
+ this.select_index(
+ row_index, None, window, cx,
+ );
+ this.create_context_menu(
+ mouse_down_event.position,
+ window,
+ cx,
+ );
}
},
))
@@ -1994,21 +2084,21 @@ impl RenderOnce for SyntaxHighlightedText {
#[derive(PartialEq)]
struct InputError {
- severity: ui::Severity,
+ severity: Severity,
content: SharedString,
}
impl InputError {
fn warning(message: impl Into<SharedString>) -> Self {
Self {
- severity: ui::Severity::Warning,
+ severity: Severity::Warning,
content: message.into(),
}
}
fn error(message: anyhow::Error) -> Self {
Self {
- severity: ui::Severity::Error,
+ severity: Severity::Error,
content: message.to_string().into(),
}
}
@@ -2135,9 +2225,11 @@ impl KeybindingEditorModal {
}
fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool {
- if self.error.as_ref().is_some_and(|old_error| {
- old_error.severity == ui::Severity::Warning && *old_error == error
- }) {
+ if self
+ .error
+ .as_ref()
+ .is_some_and(|old_error| old_error.severity == Severity::Warning && *old_error == error)
+ {
false
} else {
self.error = Some(error);
@@ -2150,7 +2242,8 @@ impl KeybindingEditorModal {
let action_arguments = self
.action_arguments_editor
.as_ref()
- .map(|editor| editor.read(cx).editor.read(cx).text(cx));
+ .map(|arguments_editor| arguments_editor.read(cx).editor.read(cx).text(cx))
+ .filter(|args| !args.is_empty());
let value = action_arguments
.as_ref()
@@ -2159,12 +2252,12 @@ impl KeybindingEditorModal {
})
.transpose()?;
- cx.build_action(&self.editing_keybind.action().name, value)
+ cx.build_action(self.editing_keybind.action().name, value)
.context("Failed to validate action arguments")?;
Ok(action_arguments)
}
- fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
+ fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<KeybindingKeystroke>> {
let new_keystrokes = self
.keybind_editor
.read_with(cx, |editor, _| editor.keystrokes().to_vec());
@@ -2193,12 +2286,10 @@ impl KeybindingEditorModal {
let fs = self.fs.clone();
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
- let new_keystrokes = self
- .validate_keystrokes(cx)
- .map_err(InputError::error)?
- .into_iter()
- .map(remove_key_char)
- .collect::<Vec<_>>();
+ let mut new_keystrokes = self.validate_keystrokes(cx).map_err(InputError::error)?;
+ new_keystrokes
+ .iter_mut()
+ .for_each(|ks| ks.remove_key_char());
let new_context = self.validate_context(cx).map_err(InputError::error)?;
let new_action_args = self
@@ -2260,58 +2351,60 @@ impl KeybindingEditorModal {
}).unwrap_or(Ok(()))?;
let create = self.creating;
-
- let status_toast = StatusToast::new(
- format!(
- "Saved edits to the {} action.",
- &self.editing_keybind.action().humanized_name
- ),
- cx,
- move |this, _cx| {
- this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
- .dismiss_button(true)
- // .action("Undo", f) todo: wire the undo functionality
- },
- );
-
- self.workspace
- .update(cx, |workspace, cx| {
- workspace.toggle_status_toast(status_toast, cx);
- })
- .log_err();
+ let keyboard_mapper = cx.keyboard_mapper().clone();
cx.spawn(async move |this, cx| {
let action_name = existing_keybind.action().name;
+ let humanized_action_name = existing_keybind.action().humanized_name.clone();
- if let Err(err) = save_keybinding_update(
+ match save_keybinding_update(
create,
existing_keybind,
&action_mapping,
new_action_args.as_deref(),
&fs,
tab_size,
+ keyboard_mapper.as_ref(),
)
.await
{
- this.update(cx, |this, cx| {
- this.set_error(InputError::error(err), cx);
- })
- .log_err();
- } else {
- this.update(cx, |this, cx| {
- this.keymap_editor.update(cx, |keymap, cx| {
- keymap.previous_edit = Some(PreviousEdit::Keybinding {
- action_mapping,
- action_name,
- fallback: keymap
- .table_interaction_state
- .read(cx)
- .get_scrollbar_offset(Axis::Vertical),
- })
- });
- cx.emit(DismissEvent);
- })
- .ok();
+ Ok(_) => {
+ this.update(cx, |this, cx| {
+ this.keymap_editor.update(cx, |keymap, cx| {
+ keymap.previous_edit = Some(PreviousEdit::Keybinding {
+ action_mapping,
+ action_name,
+ fallback: keymap
+ .table_interaction_state
+ .read(cx)
+ .get_scrollbar_offset(Axis::Vertical),
+ });
+ let status_toast = StatusToast::new(
+ format!("Saved edits to the {} action.", humanized_action_name),
+ cx,
+ move |this, _cx| {
+ this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
+ .dismiss_button(true)
+ // .action("Undo", f) todo: wire the undo functionality
+ },
+ );
+
+ this.workspace
+ .update(cx, |workspace, cx| {
+ workspace.toggle_status_toast(status_toast, cx);
+ })
+ .log_err();
+ });
+ cx.emit(DismissEvent);
+ })
+ .ok();
+ }
+ Err(err) => {
+ this.update(cx, |this, cx| {
+ this.set_error(InputError::error(err), cx);
+ })
+ .log_err();
+ }
}
})
.detach();
@@ -2389,14 +2482,6 @@ impl KeybindingEditorModal {
}
}
-fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
- Keystroke {
- modifiers,
- key,
- ..Default::default()
- }
-}
-
impl Render for KeybindingEditorModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().colors();
@@ -2691,7 +2776,7 @@ impl ActionArgumentsEditor {
})
.ok();
}
- return result;
+ result
})
.detach_and_log_err(cx);
Self {
@@ -2712,7 +2797,7 @@ impl ActionArgumentsEditor {
editor.set_text(arguments, window, cx);
} else {
// TODO: default value from schema?
- editor.set_placeholder_text("Action Arguments", cx);
+ editor.set_placeholder_text("Action Arguments", window, cx);
}
}
@@ -2794,7 +2879,7 @@ impl Render for ActionArgumentsEditor {
self.editor
.update(cx, |editor, _| editor.set_text_style_refinement(text_style));
- return v_flex().w_full().child(
+ v_flex().w_full().child(
h_flex()
.min_h_8()
.min_w_48()
@@ -2807,7 +2892,7 @@ impl Render for ActionArgumentsEditor {
.border_color(border_color)
.track_focus(&self.focus_handle)
.child(self.editor.clone()),
- );
+ )
}
}
@@ -2834,11 +2919,8 @@ impl CompletionProvider for KeyContextCompletionProvider {
break;
}
}
- let start_anchor = buffer.anchor_before(
- buffer_position
- .to_offset(&buffer)
- .saturating_sub(count_back),
- );
+ let start_anchor =
+ buffer.anchor_before(buffer_position.to_offset(buffer).saturating_sub(count_back));
let replace_range = start_anchor..buffer_position;
gpui::Task::ready(Ok(vec![project::CompletionResponse {
completions: self
@@ -2855,6 +2937,7 @@ impl CompletionProvider for KeyContextCompletionProvider {
confirm: None,
})
.collect(),
+ display_options: CompletionDisplayOptions::default(),
is_incomplete: false,
}]))
}
@@ -1,6 +1,6 @@
use gpui::{
Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
- Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
+ KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
};
use ui::{
ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
@@ -19,7 +19,7 @@ actions!(
]
);
-const KEY_CONTEXT_VALUE: &'static str = "KeystrokeInput";
+const KEY_CONTEXT_VALUE: &str = "KeystrokeInput";
const CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT: std::time::Duration =
std::time::Duration::from_millis(300);
@@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult {
}
pub struct KeystrokeInput {
- keystrokes: Vec<Keystroke>,
- placeholder_keystrokes: Option<Vec<Keystroke>>,
+ keystrokes: Vec<KeybindingKeystroke>,
+ placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
outer_focus_handle: FocusHandle,
inner_focus_handle: FocusHandle,
intercept_subscription: Option<Subscription>,
@@ -70,7 +70,7 @@ impl KeystrokeInput {
const KEYSTROKE_COUNT_MAX: usize = 3;
pub fn new(
- placeholder_keystrokes: Option<Vec<Keystroke>>,
+ placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -97,7 +97,7 @@ impl KeystrokeInput {
}
}
- pub fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
+ pub fn set_keystrokes(&mut self, keystrokes: Vec<KeybindingKeystroke>, cx: &mut Context<Self>) {
self.keystrokes = keystrokes;
self.keystrokes_changed(cx);
}
@@ -106,7 +106,7 @@ impl KeystrokeInput {
self.search = search;
}
- pub fn keystrokes(&self) -> &[Keystroke] {
+ pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
&& self.keystrokes.is_empty()
{
@@ -116,19 +116,19 @@ impl KeystrokeInput {
&& self
.keystrokes
.last()
- .map_or(false, |last| last.key.is_empty())
+ .is_some_and(|last| last.key().is_empty())
{
return &self.keystrokes[..self.keystrokes.len() - 1];
}
- return &self.keystrokes;
+ &self.keystrokes
}
- fn dummy(modifiers: Modifiers) -> Keystroke {
- return Keystroke {
+ fn dummy(modifiers: Modifiers) -> KeybindingKeystroke {
+ KeybindingKeystroke::from_keystroke(Keystroke {
modifiers,
key: "".to_string(),
key_char: None,
- };
+ })
}
fn keystrokes_changed(&self, cx: &mut Context<Self>) {
@@ -182,7 +182,7 @@ impl KeystrokeInput {
fn end_close_keystrokes_capture(&mut self) -> Option<usize> {
self.close_keystrokes.take();
self.clear_close_keystrokes_timer.take();
- return self.close_keystrokes_start.take();
+ self.close_keystrokes_start.take()
}
fn handle_possible_close_keystroke(
@@ -233,7 +233,7 @@ impl KeystrokeInput {
return CloseKeystrokeResult::Partial;
}
self.end_close_keystrokes_capture();
- return CloseKeystrokeResult::None;
+ CloseKeystrokeResult::None
}
fn on_modifiers_changed(
@@ -254,7 +254,7 @@ impl KeystrokeInput {
self.keystrokes_changed(cx);
if let Some(last) = self.keystrokes.last_mut()
- && last.key.is_empty()
+ && last.key().is_empty()
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
{
if !self.search && !event.modifiers.modified() {
@@ -263,13 +263,14 @@ impl KeystrokeInput {
}
if self.search {
if self.previous_modifiers.modified() {
- last.modifiers |= event.modifiers;
+ let modifiers = *last.modifiers() | event.modifiers;
+ last.set_modifiers(modifiers);
} else {
self.keystrokes.push(Self::dummy(event.modifiers));
}
self.previous_modifiers |= event.modifiers;
} else {
- last.modifiers = event.modifiers;
+ last.set_modifiers(event.modifiers);
return;
}
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
@@ -297,14 +298,15 @@ impl KeystrokeInput {
return;
}
- let mut keystroke = keystroke.clone();
+ let keystroke = KeybindingKeystroke::new_with_mapper(
+ keystroke.clone(),
+ false,
+ cx.keyboard_mapper().as_ref(),
+ );
if let Some(last) = self.keystrokes.last()
- && last.key.is_empty()
+ && last.key().is_empty()
&& (!self.search || self.previous_modifiers.modified())
{
- let key = keystroke.key.clone();
- keystroke = last.clone();
- keystroke.key = key;
self.keystrokes.pop();
}
@@ -320,15 +322,19 @@ impl KeystrokeInput {
return;
}
- self.keystrokes.push(keystroke.clone());
+ self.keystrokes.push(keystroke);
self.keystrokes_changed(cx);
+ // The reason we use the real modifiers from the window instead of the keystroke's modifiers
+ // is that for keystrokes like `ctrl-$` the modifiers reported by keystroke is `ctrl` which
+ // is wrong, it should be `ctrl-shift`. The window's modifiers are always correct.
+ let real_modifiers = window.modifiers();
if self.search {
- self.previous_modifiers = keystroke.modifiers;
+ self.previous_modifiers = real_modifiers;
return;
}
- if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() {
- self.keystrokes.push(Self::dummy(keystroke.modifiers));
+ if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && real_modifiers.modified() {
+ self.keystrokes.push(Self::dummy(real_modifiers));
}
}
@@ -364,7 +370,7 @@ impl KeystrokeInput {
&self.keystrokes
};
keystrokes.iter().map(move |keystroke| {
- h_flex().children(ui::render_keystroke(
+ h_flex().children(ui::render_keybinding_keystroke(
keystroke,
Some(Color::Default),
Some(rems(0.875).into()),
@@ -437,7 +443,7 @@ impl KeystrokeInput {
// is a much more reliable check, as the intercept keystroke handlers are installed
// on focus of the inner focus handle, thereby ensuring our recording state does
// not get de-synced
- return self.inner_focus_handle.is_focused(window);
+ self.inner_focus_handle.is_focused(window)
}
}
@@ -455,7 +461,7 @@ impl Render for KeystrokeInput {
let is_focused = self.outer_focus_handle.contains_focused(window, cx);
let is_recording = self.is_recording(window);
- let horizontal_padding = rems_from_px(64.);
+ let width = rems_from_px(64.);
let recording_bg_color = colors
.editor_background
@@ -522,6 +528,9 @@ impl Render for KeystrokeInput {
h_flex()
.id("keystroke-input")
.track_focus(&self.outer_focus_handle)
+ .key_context(Self::key_context())
+ .on_action(cx.listener(Self::start_recording))
+ .on_action(cx.listener(Self::clear_keystrokes))
.py_2()
.px_3()
.gap_2()
@@ -529,7 +538,7 @@ impl Render for KeystrokeInput {
.w_full()
.flex_1()
.justify_between()
- .rounded_sm()
+ .rounded_md()
.overflow_hidden()
.map(|this| {
if is_recording {
@@ -539,16 +548,16 @@ impl Render for KeystrokeInput {
}
})
.border_1()
- .border_color(colors.border_variant)
- .when(is_focused, |parent| {
- parent.border_color(colors.border_focused)
+ .map(|this| {
+ if is_focused {
+ this.border_color(colors.border_focused)
+ } else {
+ this.border_color(colors.border_variant)
+ }
})
- .key_context(Self::key_context())
- .on_action(cx.listener(Self::start_recording))
- .on_action(cx.listener(Self::clear_keystrokes))
.child(
h_flex()
- .w(horizontal_padding)
+ .w(width)
.gap_0p5()
.justify_start()
.flex_none()
@@ -567,14 +576,13 @@ impl Render for KeystrokeInput {
.id("keystroke-input-inner")
.track_focus(&self.inner_focus_handle)
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
- .size_full()
.when(!self.search, |this| {
this.focus(|mut style| {
style.border_color = Some(colors.border_focused);
style
})
})
- .w_full()
+ .size_full()
.min_w_0()
.justify_center()
.flex_wrap()
@@ -583,7 +591,7 @@ impl Render for KeystrokeInput {
)
.child(
h_flex()
- .w(horizontal_padding)
+ .w(width)
.gap_0p5()
.justify_end()
.flex_none()
@@ -635,9 +643,7 @@ impl Render for KeystrokeInput {
"Clear Keystrokes",
&ClearKeystrokes,
))
- .when(!is_recording || !is_focused, |this| {
- this.icon_color(Color::Muted)
- })
+ .when(!is_focused, |this| this.icon_color(Color::Muted))
.on_click(cx.listener(|this, _event, window, cx| {
this.clear_keystrokes(&ClearKeystrokes, window, cx);
})),
@@ -706,8 +712,11 @@ mod tests {
// Combine current modifiers with keystroke modifiers
keystroke.modifiers |= self.current_modifiers;
+ let real_modifiers = keystroke.modifiers;
+ keystroke = to_gpui_keystroke(keystroke);
self.update_input(|input, window, cx| {
+ window.set_modifiers(real_modifiers);
input.handle_keystroke(&keystroke, window, cx);
});
@@ -735,6 +744,7 @@ mod tests {
};
self.update_input(|input, window, cx| {
+ window.set_modifiers(new_modifiers);
input.on_modifiers_changed(&event, window, cx);
});
@@ -809,9 +819,13 @@ mod tests {
/// Verifies that the keystrokes match the expected strings
#[track_caller]
pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
- let actual = self
- .input
- .read_with(&mut self.cx, |input, _| input.keystrokes.clone());
+ let actual: Vec<Keystroke> = self.input.read_with(&self.cx, |input, _| {
+ input
+ .keystrokes
+ .iter()
+ .map(|keystroke| keystroke.inner().clone())
+ .collect()
+ });
Self::expect_keystrokes_equal(&actual, expected);
self
}
@@ -820,7 +834,7 @@ mod tests {
pub fn expect_close_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
let actual = self
.input
- .read_with(&mut self.cx, |input, _| input.close_keystrokes.clone())
+ .read_with(&self.cx, |input, _| input.close_keystrokes.clone())
.unwrap_or_default();
Self::expect_keystrokes_equal(&actual, expected);
self
@@ -934,12 +948,106 @@ mod tests {
let change_tracker = KeystrokeUpdateTracker::new(self.input.clone(), &mut self.cx);
let result = self.input.update_in(&mut self.cx, cb);
KeystrokeUpdateTracker::finish(change_tracker, &self.cx);
- return result;
+ result
}
}
+ /// For GPUI, when you press `ctrl-shift-2`, it produces `ctrl-@` without the shift modifier.
+ fn to_gpui_keystroke(mut keystroke: Keystroke) -> Keystroke {
+ if keystroke.modifiers.shift {
+ match keystroke.key.as_str() {
+ "`" => {
+ keystroke.key = "~".into();
+ keystroke.modifiers.shift = false;
+ }
+ "1" => {
+ keystroke.key = "!".into();
+ keystroke.modifiers.shift = false;
+ }
+ "2" => {
+ keystroke.key = "@".into();
+ keystroke.modifiers.shift = false;
+ }
+ "3" => {
+ keystroke.key = "#".into();
+ keystroke.modifiers.shift = false;
+ }
+ "4" => {
+ keystroke.key = "$".into();
+ keystroke.modifiers.shift = false;
+ }
+ "5" => {
+ keystroke.key = "%".into();
+ keystroke.modifiers.shift = false;
+ }
+ "6" => {
+ keystroke.key = "^".into();
+ keystroke.modifiers.shift = false;
+ }
+ "7" => {
+ keystroke.key = "&".into();
+ keystroke.modifiers.shift = false;
+ }
+ "8" => {
+ keystroke.key = "*".into();
+ keystroke.modifiers.shift = false;
+ }
+ "9" => {
+ keystroke.key = "(".into();
+ keystroke.modifiers.shift = false;
+ }
+ "0" => {
+ keystroke.key = ")".into();
+ keystroke.modifiers.shift = false;
+ }
+ "-" => {
+ keystroke.key = "_".into();
+ keystroke.modifiers.shift = false;
+ }
+ "=" => {
+ keystroke.key = "+".into();
+ keystroke.modifiers.shift = false;
+ }
+ "[" => {
+ keystroke.key = "{".into();
+ keystroke.modifiers.shift = false;
+ }
+ "]" => {
+ keystroke.key = "}".into();
+ keystroke.modifiers.shift = false;
+ }
+ "\\" => {
+ keystroke.key = "|".into();
+ keystroke.modifiers.shift = false;
+ }
+ ";" => {
+ keystroke.key = ":".into();
+ keystroke.modifiers.shift = false;
+ }
+ "'" => {
+ keystroke.key = "\"".into();
+ keystroke.modifiers.shift = false;
+ }
+ "," => {
+ keystroke.key = "<".into();
+ keystroke.modifiers.shift = false;
+ }
+ "." => {
+ keystroke.key = ">".into();
+ keystroke.modifiers.shift = false;
+ }
+ "/" => {
+ keystroke.key = "?".into();
+ keystroke.modifiers.shift = false;
+ }
+ _ => {}
+ }
+ }
+ keystroke
+ }
+
struct KeystrokeUpdateTracker {
- initial_keystrokes: Vec<Keystroke>,
+ initial_keystrokes: Vec<KeybindingKeystroke>,
_subscription: Subscription,
input: Entity<KeystrokeInput>,
received_keystrokes_updated: bool,
@@ -983,8 +1091,8 @@ mod tests {
);
}
- fn keystrokes_str(ks: &[Keystroke]) -> String {
- ks.iter().map(|ks| ks.unparse()).join(" ")
+ fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String {
+ ks.iter().map(|ks| ks.inner().unparse()).join(" ")
}
}
}
@@ -1041,7 +1149,15 @@ mod tests {
.send_events(&["+cmd", "shift-f", "-cmd"])
// In search mode, when completing a modifier-only keystroke with a key,
// only the original modifiers are preserved, not the keystroke's modifiers
- .expect_keystrokes(&["cmd-f"]);
+ //
+ // Update:
+ // This behavior was changed to preserve all modifiers in search mode, this is now reflected in the expected keystrokes.
+ // Specifically, considering the sequence: `+cmd +shift -shift 2`, we expect it to produce the same result as `+cmd +shift 2`
+ // which is `cmd-@`. But in the case of `+cmd +shift -shift 2`, the keystroke we receive is `cmd-2`, which means that
+ // we need to dynamically map the key from `2` to `@` when the shift modifier is not present, which is not possible.
+ // Therefore, we now preserve all modifiers in search mode to ensure consistent behavior.
+ // And also, VSCode seems to preserve all modifiers in search mode as well.
+ .expect_keystrokes(&["cmd-shift-f"]);
}
#[gpui::test]
@@ -1218,7 +1334,7 @@ mod tests {
.await
.with_search_mode(true)
.send_events(&["+ctrl", "+shift", "-shift", "a", "-ctrl"])
- .expect_keystrokes(&["ctrl-shift-a"]);
+ .expect_keystrokes(&["ctrl-a"]);
}
#[gpui::test]
@@ -1326,7 +1442,7 @@ mod tests {
.await
.with_search_mode(true)
.send_events(&["+ctrl+alt", "-ctrl", "j"])
- .expect_keystrokes(&["ctrl-alt-j"]);
+ .expect_keystrokes(&["alt-j"]);
}
#[gpui::test]
@@ -1348,11 +1464,11 @@ mod tests {
.send_events(&["+ctrl+alt", "-ctrl", "+shift"])
.expect_keystrokes(&["ctrl-shift-alt-"])
.send_keystroke("j")
- .expect_keystrokes(&["ctrl-shift-alt-j"])
+ .expect_keystrokes(&["shift-alt-j"])
.send_keystroke("i")
- .expect_keystrokes(&["ctrl-shift-alt-j", "shift-alt-i"])
+ .expect_keystrokes(&["shift-alt-j", "shift-alt-i"])
.send_events(&["-shift-alt", "+cmd"])
- .expect_keystrokes(&["ctrl-shift-alt-j", "shift-alt-i", "cmd-"]);
+ .expect_keystrokes(&["shift-alt-j", "shift-alt-i", "cmd-"]);
}
#[gpui::test]
@@ -1385,4 +1501,13 @@ mod tests {
.send_events(&["+ctrl", "-ctrl", "+alt", "-alt", "+shift", "-shift"])
.expect_empty();
}
+
+ #[gpui::test]
+ async fn test_not_search_shifted_keys(cx: &mut TestAppContext) {
+ init_test(cx)
+ .await
+ .with_search_mode(false)
+ .send_events(&["+ctrl", "+shift", "4", "-all"])
+ .expect_keystrokes(&["ctrl-$"]);
+ }
}
@@ -213,7 +213,7 @@ impl TableInteractionState {
let mut column_ix = 0;
let resizable_columns_slice = *resizable_columns;
- let mut resizable_columns = resizable_columns.into_iter();
+ let mut resizable_columns = resizable_columns.iter();
let dividers = intersperse_with(spacers, || {
window.with_id(column_ix, |window| {
@@ -343,7 +343,7 @@ impl TableInteractionState {
.on_any_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
- .on_scroll_wheel(Self::listener(&this, |_, _, _, cx| {
+ .on_scroll_wheel(Self::listener(this, |_, _, _, cx| {
cx.notify();
}))
.children(Scrollbar::vertical(
@@ -731,7 +731,7 @@ impl<const COLS: usize> ColumnWidths<COLS> {
}
widths[col_idx] = widths[col_idx] + (diff - diff_remaining);
- return diff_remaining;
+ diff_remaining
}
}
@@ -801,7 +801,7 @@ impl<const COLS: usize> Table<COLS> {
) -> Self {
self.rows = TableContents::UniformList(UniformListData {
element_id: id.into(),
- row_count: row_count,
+ row_count,
render_item_fn: Box::new(render_item_fn),
});
self
@@ -27,12 +27,13 @@ use gpui::{
App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, SharedString, StyledText,
Task, TaskLabel, TextStyle,
};
+
use lsp::{LanguageServerId, NumberOrString};
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
-use settings::WorktreeId;
+use settings::{SettingsUi, WorktreeId};
use smallvec::SmallVec;
use smol::future::yield_now;
use std::{
@@ -173,7 +174,9 @@ pub enum IndentKind {
}
/// The shape of a selection cursor.
-#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(
+ Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub enum CursorShape {
/// A vertical bar
@@ -202,7 +205,7 @@ pub struct Diagnostic {
pub source: Option<String>,
/// A machine-readable code that identifies this diagnostic.
pub code: Option<NumberOrString>,
- pub code_description: Option<lsp::Url>,
+ pub code_description: Option<lsp::Uri>,
/// Whether this diagnostic is a hint, warning, or error.
pub severity: DiagnosticSeverity,
/// The human-readable message associated with this diagnostic.
@@ -282,6 +285,14 @@ pub enum Operation {
/// The language server ID.
server_id: LanguageServerId,
},
+
+ /// An update to the line ending type of this buffer.
+ UpdateLineEnding {
+ /// The line ending type.
+ line_ending: LineEnding,
+ /// The buffer's lamport timestamp.
+ lamport_timestamp: clock::Lamport,
+ },
}
/// An event that occurs in a buffer.
@@ -313,10 +324,6 @@ pub enum BufferEvent {
DiagnosticsUpdated,
/// The buffer gained or lost editing capabilities.
CapabilityChanged,
- /// The buffer was explicitly requested to close.
- Closed,
- /// The buffer was discarded when closing.
- Discarded,
}
/// The file associated with a buffer.
@@ -494,6 +501,10 @@ pub struct Chunk<'a> {
pub is_unnecessary: bool,
/// Whether this chunk of text was originally a tab character.
pub is_tab: bool,
+ /// A bitset of which characters are tabs in this string.
+ pub tabs: u128,
+ /// Bitmap of character indices in this chunk
+ pub chars: u128,
/// Whether this chunk of text was originally a tab character.
pub is_inlay: bool,
/// Whether to underline the corresponding text range in the editor.
@@ -629,13 +640,13 @@ impl HighlightedTextBuilder {
self.text.push_str(chunk.text);
let end = self.text.len();
- if let Some(mut highlight_style) = chunk
+ if let Some(highlight_style) = chunk
.syntax_highlight_id
.and_then(|id| id.style(syntax_theme))
{
- if let Some(override_style) = override_style {
- highlight_style.highlight(override_style);
- }
+ let highlight_style = override_style.map_or(highlight_style, |override_style| {
+ highlight_style.highlight(override_style)
+ });
self.highlights.push((start..end, highlight_style));
} else if let Some(override_style) = override_style {
self.highlights.push((start..end, override_style));
@@ -716,7 +727,7 @@ impl EditPreview {
&self.applied_edits_snapshot,
&self.syntax_snapshot,
None,
- &syntax_theme,
+ syntax_theme,
);
}
@@ -727,7 +738,7 @@ impl EditPreview {
¤t_snapshot.text,
¤t_snapshot.syntax,
Some(deletion_highlight_style),
- &syntax_theme,
+ syntax_theme,
);
}
@@ -737,7 +748,7 @@ impl EditPreview {
&self.applied_edits_snapshot,
&self.syntax_snapshot,
Some(insertion_highlight_style),
- &syntax_theme,
+ syntax_theme,
);
}
@@ -749,7 +760,7 @@ impl EditPreview {
&self.applied_edits_snapshot,
&self.syntax_snapshot,
None,
- &syntax_theme,
+ syntax_theme,
);
highlighted_text.build()
@@ -974,8 +985,6 @@ impl Buffer {
TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot();
let mut syntax = SyntaxMap::new(&text).snapshot();
if let Some(language) = language.clone() {
- let text = text.clone();
- let language = language.clone();
let language_registry = language_registry.clone();
syntax.reparse(&text, language_registry, language);
}
@@ -1020,9 +1029,6 @@ impl Buffer {
let text = TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot();
let mut syntax = SyntaxMap::new(&text).snapshot();
if let Some(language) = language.clone() {
- let text = text.clone();
- let language = language.clone();
- let language_registry = language_registry.clone();
syntax.reparse(&text, language_registry, language);
}
BufferSnapshot {
@@ -1128,7 +1134,7 @@ impl Buffer {
} else {
ranges.as_slice()
}
- .into_iter()
+ .iter()
.peekable();
let mut edits = Vec::new();
@@ -1158,13 +1164,12 @@ impl Buffer {
base_buffer.edit(edits, None, cx)
});
- if let Some(operation) = operation {
- if let Some(BufferBranchState {
+ if let Some(operation) = operation
+ && let Some(BufferBranchState {
merged_operations, ..
}) = &mut self.branch_state
- {
- merged_operations.push(operation);
- }
+ {
+ merged_operations.push(operation);
}
}
@@ -1185,11 +1190,11 @@ impl Buffer {
};
let mut operation_to_undo = None;
- if let Operation::Buffer(text::Operation::Edit(operation)) = &operation {
- if let Ok(ix) = merged_operations.binary_search(&operation.timestamp) {
- merged_operations.remove(ix);
- operation_to_undo = Some(operation.timestamp);
- }
+ if let Operation::Buffer(text::Operation::Edit(operation)) = &operation
+ && let Ok(ix) = merged_operations.binary_search(&operation.timestamp)
+ {
+ merged_operations.remove(ix);
+ operation_to_undo = Some(operation.timestamp);
}
self.apply_ops([operation.clone()], cx);
@@ -1248,10 +1253,27 @@ impl Buffer {
self.syntax_map.lock().language_registry()
}
+ /// Assign the line ending type to the buffer.
+ pub fn set_line_ending(&mut self, line_ending: LineEnding, cx: &mut Context<Self>) {
+ self.text.set_line_ending(line_ending);
+
+ let lamport_timestamp = self.text.lamport_clock.tick();
+ self.send_operation(
+ Operation::UpdateLineEnding {
+ line_ending,
+ lamport_timestamp,
+ },
+ true,
+ cx,
+ );
+ }
+
/// Assign the buffer a new [`Capability`].
pub fn set_capability(&mut self, capability: Capability, cx: &mut Context<Self>) {
- self.capability = capability;
- cx.emit(BufferEvent::CapabilityChanged)
+ if self.capability != capability {
+ self.capability = capability;
+ cx.emit(BufferEvent::CapabilityChanged)
+ }
}
/// This method is called to signal that the buffer has been saved.
@@ -1271,12 +1293,6 @@ impl Buffer {
cx.notify();
}
- /// This method is called to signal that the buffer has been discarded.
- pub fn discarded(&self, cx: &mut Context<Self>) {
- cx.emit(BufferEvent::Discarded);
- cx.notify();
- }
-
/// Reloads the contents of the buffer from disk.
pub fn reload(&mut self, cx: &Context<Self>) -> oneshot::Receiver<Option<Transaction>> {
let (tx, rx) = futures::channel::oneshot::channel();
@@ -1396,7 +1412,8 @@ impl Buffer {
is_first = false;
return true;
}
- let any_sub_ranges_contain_range = layer
+
+ layer
.included_sub_ranges
.map(|sub_ranges| {
sub_ranges.iter().any(|sub_range| {
@@ -1405,9 +1422,7 @@ impl Buffer {
!is_before_start && !is_after_end
})
})
- .unwrap_or(true);
- let result = any_sub_ranges_contain_range;
- return result;
+ .unwrap_or(true)
})
.last()
.map(|info| info.language.clone())
@@ -1424,10 +1439,10 @@ impl Buffer {
.map(|info| info.language.clone())
.collect();
- if languages.is_empty() {
- if let Some(buffer_language) = self.language() {
- languages.push(buffer_language.clone());
- }
+ if languages.is_empty()
+ && let Some(buffer_language) = self.language()
+ {
+ languages.push(buffer_language.clone());
}
languages
@@ -1521,12 +1536,12 @@ impl Buffer {
let new_syntax_map = parse_task.await;
this.update(cx, move |this, cx| {
let grammar_changed =
- this.language.as_ref().map_or(true, |current_language| {
+ this.language.as_ref().is_none_or(|current_language| {
!Arc::ptr_eq(&language, current_language)
});
let language_registry_changed = new_syntax_map
.contains_unknown_injections()
- && language_registry.map_or(false, |registry| {
+ && language_registry.is_some_and(|registry| {
registry.version() != new_syntax_map.language_registry_version()
});
let parse_again = language_registry_changed
@@ -1571,15 +1586,26 @@ impl Buffer {
diagnostics: diagnostics.iter().cloned().collect(),
lamport_timestamp,
};
+
self.apply_diagnostic_update(server_id, diagnostics, lamport_timestamp, cx);
self.send_operation(op, true, cx);
}
- pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> {
- let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else {
- return None;
- };
- Some(&self.diagnostics[idx].1)
+ pub fn buffer_diagnostics(
+ &self,
+ for_server: Option<LanguageServerId>,
+ ) -> Vec<&DiagnosticEntry<Anchor>> {
+ match for_server {
+ Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) {
+ Ok(idx) => self.diagnostics[idx].1.iter().collect(),
+ Err(_) => Vec::new(),
+ },
+ None => self
+ .diagnostics
+ .iter()
+ .flat_map(|(_, diagnostic_set)| diagnostic_set.iter())
+ .collect(),
+ }
}
fn request_autoindent(&mut self, cx: &mut Context<Self>) {
@@ -1719,8 +1745,7 @@ impl Buffer {
})
.with_delta(suggestion.delta, language_indent_size);
- if old_suggestions.get(&new_row).map_or(
- true,
+ if old_suggestions.get(&new_row).is_none_or(
|(old_indentation, was_within_error)| {
suggested_indent != *old_indentation
&& (!suggestion.within_error || *was_within_error)
@@ -2014,7 +2039,7 @@ impl Buffer {
fn was_changed(&mut self) {
self.change_bits.retain(|change_bit| {
- change_bit.upgrade().map_or(false, |bit| {
+ change_bit.upgrade().is_some_and(|bit| {
bit.replace(true);
true
})
@@ -2191,7 +2216,7 @@ impl Buffer {
if self
.remote_selections
.get(&self.text.replica_id())
- .map_or(true, |set| !set.selections.is_empty())
+ .is_none_or(|set| !set.selections.is_empty())
{
self.set_active_selections(Arc::default(), false, Default::default(), cx);
}
@@ -2208,7 +2233,7 @@ impl Buffer {
self.remote_selections.insert(
AGENT_REPLICA_ID,
SelectionSet {
- selections: selections.clone(),
+ selections,
lamport_timestamp,
line_mode,
cursor_shape,
@@ -2270,13 +2295,11 @@ impl Buffer {
}
let new_text = new_text.into();
if !new_text.is_empty() || !range.is_empty() {
- if let Some((prev_range, prev_text)) = edits.last_mut() {
- if prev_range.end >= range.start {
- prev_range.end = cmp::max(prev_range.end, range.end);
- *prev_text = format!("{prev_text}{new_text}").into();
- } else {
- edits.push((range, new_text));
- }
+ if let Some((prev_range, prev_text)) = edits.last_mut()
+ && prev_range.end >= range.start
+ {
+ prev_range.end = cmp::max(prev_range.end, range.end);
+ *prev_text = format!("{prev_text}{new_text}").into();
} else {
edits.push((range, new_text));
}
@@ -2296,10 +2319,27 @@ impl Buffer {
if let Some((before_edit, mode)) = autoindent_request {
let mut delta = 0isize;
- let entries = edits
+ let mut previous_setting = None;
+ let entries: Vec<_> = edits
.into_iter()
.enumerate()
.zip(&edit_operation.as_edit().unwrap().new_text)
+ .filter(|((_, (range, _)), _)| {
+ let language = before_edit.language_at(range.start);
+ let language_id = language.map(|l| l.id());
+ if let Some((cached_language_id, auto_indent)) = previous_setting
+ && cached_language_id == language_id
+ {
+ auto_indent
+ } else {
+ // The auto-indent setting is not present in editorconfigs, hence
+ // we can avoid passing the file here.
+ let auto_indent =
+ language_settings(language.map(|l| l.name()), None, cx).auto_indent;
+ previous_setting = Some((language_id, auto_indent));
+ auto_indent
+ }
+ })
.map(|((ix, (range, _)), new_text)| {
let new_text_length = new_text.len();
let old_start = range.start.to_point(&before_edit);
@@ -2373,12 +2413,14 @@ impl Buffer {
})
.collect();
- self.autoindent_requests.push(Arc::new(AutoindentRequest {
- before_edit,
- entries,
- is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
- ignore_empty_lines: false,
- }));
+ if !entries.is_empty() {
+ self.autoindent_requests.push(Arc::new(AutoindentRequest {
+ before_edit,
+ entries,
+ is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
+ ignore_empty_lines: false,
+ }));
+ }
}
self.end_transaction(cx);
@@ -2543,7 +2585,7 @@ impl Buffer {
Operation::UpdateSelections { selections, .. } => selections
.iter()
.all(|s| self.can_resolve(&s.start) && self.can_resolve(&s.end)),
- Operation::UpdateCompletionTriggers { .. } => true,
+ Operation::UpdateCompletionTriggers { .. } | Operation::UpdateLineEnding { .. } => true,
}
}
@@ -2571,10 +2613,10 @@ impl Buffer {
line_mode,
cursor_shape,
} => {
- if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id) {
- if set.lamport_timestamp > lamport_timestamp {
- return;
- }
+ if let Some(set) = self.remote_selections.get(&lamport_timestamp.replica_id)
+ && set.lamport_timestamp > lamport_timestamp
+ {
+ return;
}
self.remote_selections.insert(
@@ -2600,7 +2642,7 @@ impl Buffer {
self.completion_triggers = self
.completion_triggers_per_language_server
.values()
- .flat_map(|triggers| triggers.into_iter().cloned())
+ .flat_map(|triggers| triggers.iter().cloned())
.collect();
} else {
self.completion_triggers_per_language_server
@@ -2609,6 +2651,13 @@ impl Buffer {
}
self.text.lamport_clock.observe(lamport_timestamp);
}
+ Operation::UpdateLineEnding {
+ line_ending,
+ lamport_timestamp,
+ } => {
+ self.text.set_line_ending(line_ending);
+ self.text.lamport_clock.observe(lamport_timestamp);
+ }
}
}
@@ -2760,7 +2809,7 @@ impl Buffer {
self.completion_triggers = self
.completion_triggers_per_language_server
.values()
- .flat_map(|triggers| triggers.into_iter().cloned())
+ .flat_map(|triggers| triggers.iter().cloned())
.collect();
} else {
self.completion_triggers_per_language_server
@@ -2822,18 +2871,18 @@ impl Buffer {
let mut edits: Vec<(Range<usize>, String)> = Vec::new();
let mut last_end = None;
for _ in 0..old_range_count {
- if last_end.map_or(false, |last_end| last_end >= self.len()) {
+ if last_end.is_some_and(|last_end| last_end >= self.len()) {
break;
}
let new_start = last_end.map_or(0, |last_end| last_end + 1);
let mut range = self.random_byte_range(new_start, rng);
- if rng.gen_bool(0.2) {
+ if rng.random_bool(0.2) {
mem::swap(&mut range.start, &mut range.end);
}
last_end = Some(range.end);
- let new_text_len = rng.gen_range(0..10);
+ let new_text_len = rng.random_range(0..10);
let mut new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect();
new_text = new_text.to_uppercase();
@@ -2991,9 +3040,9 @@ impl BufferSnapshot {
}
let mut error_ranges = Vec::<Range<Point>>::new();
- let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
- grammar.error_query.as_ref()
- });
+ let mut matches = self
+ .syntax
+ .matches(range, &self.text, |grammar| grammar.error_query.as_ref());
while let Some(mat) = matches.peek() {
let node = mat.captures[0].node;
let start = Point::from_ts_point(node.start_position());
@@ -3042,14 +3091,14 @@ impl BufferSnapshot {
if config
.decrease_indent_pattern
.as_ref()
- .map_or(false, |regex| regex.is_match(line))
+ .is_some_and(|regex| regex.is_match(line))
{
indent_change_rows.push((row, Ordering::Less));
}
if config
.increase_indent_pattern
.as_ref()
- .map_or(false, |regex| regex.is_match(line))
+ .is_some_and(|regex| regex.is_match(line))
{
indent_change_rows.push((row + 1, Ordering::Greater));
}
@@ -3065,7 +3114,7 @@ impl BufferSnapshot {
}
}
for rule in &config.decrease_indent_patterns {
- if rule.pattern.as_ref().map_or(false, |r| r.is_match(line)) {
+ if rule.pattern.as_ref().is_some_and(|r| r.is_match(line)) {
let row_start_column = self.indent_size_for_line(row).len;
let basis_row = rule
.valid_after
@@ -3278,8 +3327,7 @@ impl BufferSnapshot {
range: Range<D>,
) -> Option<SyntaxLayer<'_>> {
let range = range.to_offset(self);
- return self
- .syntax
+ self.syntax
.layers_for_range(range, &self.text, false)
.max_by(|a, b| {
if a.depth != b.depth {
@@ -3289,7 +3337,7 @@ impl BufferSnapshot {
} else {
a.node().end_byte().cmp(&b.node().end_byte()).reverse()
}
- });
+ })
}
/// Returns the main [`Language`].
@@ -3347,9 +3395,8 @@ impl BufferSnapshot {
}
}
- if let Some(range) = range {
- if smallest_range_and_depth.as_ref().map_or(
- true,
+ if let Some(range) = range
+ && smallest_range_and_depth.as_ref().is_none_or(
|(smallest_range, smallest_range_depth)| {
if layer.depth > *smallest_range_depth {
true
@@ -3359,13 +3406,13 @@ impl BufferSnapshot {
false
}
},
- ) {
- smallest_range_and_depth = Some((range, layer.depth));
- scope = Some(LanguageScope {
- language: layer.language.clone(),
- override_id: layer.override_id(offset, &self.text),
- });
- }
+ )
+ {
+ smallest_range_and_depth = Some((range, layer.depth));
+ scope = Some(LanguageScope {
+ language: layer.language.clone(),
+ override_id: layer.override_id(offset, &self.text),
+ });
}
}
@@ -3417,46 +3464,66 @@ impl BufferSnapshot {
}
/// Returns the closest syntax node enclosing the given range.
+ /// Positions a tree cursor at the leaf node that contains or touches the given range.
+ /// This is shared logic used by syntax navigation methods.
+ fn position_cursor_at_range(cursor: &mut tree_sitter::TreeCursor, range: &Range<usize>) {
+ // Descend to the first leaf that touches the start of the range.
+ //
+ // If the range is non-empty and the current node ends exactly at the start,
+ // move to the next sibling to find a node that extends beyond the start.
+ //
+ // If the range is empty and the current node starts after the range position,
+ // move to the previous sibling to find the node that contains the position.
+ while cursor.goto_first_child_for_byte(range.start).is_some() {
+ if !range.is_empty() && cursor.node().end_byte() == range.start {
+ cursor.goto_next_sibling();
+ }
+ if range.is_empty() && cursor.node().start_byte() > range.start {
+ cursor.goto_previous_sibling();
+ }
+ }
+ }
+
+ /// Moves the cursor to find a node that contains the given range.
+ /// Returns true if such a node is found, false otherwise.
+ /// This is shared logic used by syntax navigation methods.
+ fn find_containing_node(
+ cursor: &mut tree_sitter::TreeCursor,
+ range: &Range<usize>,
+ strict: bool,
+ ) -> bool {
+ loop {
+ let node_range = cursor.node().byte_range();
+
+ if node_range.start <= range.start
+ && node_range.end >= range.end
+ && (!strict || node_range.len() > range.len())
+ {
+ return true;
+ }
+ if !cursor.goto_parent() {
+ return false;
+ }
+ }
+ }
+
pub fn syntax_ancestor<'a, T: ToOffset>(
&'a self,
range: Range<T>,
) -> Option<tree_sitter::Node<'a>> {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut result: Option<tree_sitter::Node<'a>> = None;
- 'outer: for layer in self
+ for layer in self
.syntax
.layers_for_range(range.clone(), &self.text, true)
{
let mut cursor = layer.node().walk();
- // Descend to the first leaf that touches the start of the range.
- //
- // If the range is non-empty and the current node ends exactly at the start,
- // move to the next sibling to find a node that extends beyond the start.
- //
- // If the range is empty and the current node starts after the range position,
- // move to the previous sibling to find the node that contains the position.
- while cursor.goto_first_child_for_byte(range.start).is_some() {
- if !range.is_empty() && cursor.node().end_byte() == range.start {
- cursor.goto_next_sibling();
- }
- if range.is_empty() && cursor.node().start_byte() > range.start {
- cursor.goto_previous_sibling();
- }
- }
+ Self::position_cursor_at_range(&mut cursor, &range);
// Ascend to the smallest ancestor that strictly contains the range.
- loop {
- let node_range = cursor.node().byte_range();
- if node_range.start <= range.start
- && node_range.end >= range.end
- && node_range.len() > range.len()
- {
- break;
- }
- if !cursor.goto_parent() {
- continue 'outer;
- }
+ if !Self::find_containing_node(&mut cursor, &range, true) {
+ continue;
}
let left_node = cursor.node();
@@ -3481,19 +3548,125 @@ impl BufferSnapshot {
// If there is a candidate node on both sides of the (empty) range, then
// decide between the two by favoring a named node over an anonymous token.
// If both nodes are the same in that regard, favor the right one.
- if let Some(right_node) = right_node {
- if right_node.is_named() || !left_node.is_named() {
- layer_result = right_node;
+ if let Some(right_node) = right_node
+ && (right_node.is_named() || !left_node.is_named())
+ {
+ layer_result = right_node;
+ }
+ }
+
+ if let Some(previous_result) = &result
+ && previous_result.byte_range().len() < layer_result.byte_range().len()
+ {
+ continue;
+ }
+ result = Some(layer_result);
+ }
+
+ result
+ }
+
+ /// Find the previous sibling syntax node at the given range.
+ ///
+ /// This function locates the syntax node that precedes the node containing
+ /// the given range. It searches hierarchically by:
+ /// 1. Finding the node that contains the given range
+ /// 2. Looking for the previous sibling at the same tree level
+ /// 3. If no sibling is found, moving up to parent levels and searching for siblings
+ ///
+ /// Returns `None` if there is no previous sibling at any ancestor level.
+ pub fn syntax_prev_sibling<'a, T: ToOffset>(
+ &'a self,
+ range: Range<T>,
+ ) -> Option<tree_sitter::Node<'a>> {
+ let range = range.start.to_offset(self)..range.end.to_offset(self);
+ let mut result: Option<tree_sitter::Node<'a>> = None;
+
+ for layer in self
+ .syntax
+ .layers_for_range(range.clone(), &self.text, true)
+ {
+ let mut cursor = layer.node().walk();
+
+ Self::position_cursor_at_range(&mut cursor, &range);
+
+ // Find the node that contains the range
+ if !Self::find_containing_node(&mut cursor, &range, false) {
+ continue;
+ }
+
+ // Look for the previous sibling, moving up ancestor levels if needed
+ loop {
+ if cursor.goto_previous_sibling() {
+ let layer_result = cursor.node();
+
+ if let Some(previous_result) = &result {
+ if previous_result.byte_range().end < layer_result.byte_range().end {
+ continue;
+ }
}
+ result = Some(layer_result);
+ break;
+ }
+
+ // No sibling found at this level, try moving up to parent
+ if !cursor.goto_parent() {
+ break;
}
}
+ }
- if let Some(previous_result) = &result {
- if previous_result.byte_range().len() < layer_result.byte_range().len() {
- continue;
+ result
+ }
+
+ /// Find the next sibling syntax node at the given range.
+ ///
+ /// This function locates the syntax node that follows the node containing
+ /// the given range. It searches hierarchically by:
+ /// 1. Finding the node that contains the given range
+ /// 2. Looking for the next sibling at the same tree level
+ /// 3. If no sibling is found, moving up to parent levels and searching for siblings
+ ///
+ /// Returns `None` if there is no next sibling at any ancestor level.
+ pub fn syntax_next_sibling<'a, T: ToOffset>(
+ &'a self,
+ range: Range<T>,
+ ) -> Option<tree_sitter::Node<'a>> {
+ let range = range.start.to_offset(self)..range.end.to_offset(self);
+ let mut result: Option<tree_sitter::Node<'a>> = None;
+
+ for layer in self
+ .syntax
+ .layers_for_range(range.clone(), &self.text, true)
+ {
+ let mut cursor = layer.node().walk();
+
+ Self::position_cursor_at_range(&mut cursor, &range);
+
+ // Find the node that contains the range
+ if !Self::find_containing_node(&mut cursor, &range, false) {
+ continue;
+ }
+
+ // Look for the next sibling, moving up ancestor levels if needed
+ loop {
+ if cursor.goto_next_sibling() {
+ let layer_result = cursor.node();
+
+ if let Some(previous_result) = &result {
+ if previous_result.byte_range().start > layer_result.byte_range().start {
+ continue;
+ }
+ }
+ result = Some(layer_result);
+ break;
+ }
+
+ // No sibling found at this level, try moving up to parent
+ if !cursor.goto_parent() {
+ break;
}
}
- result = Some(layer_result);
}
result
@@ -3526,16 +3699,15 @@ impl BufferSnapshot {
}
}
- return Some(cursor.node());
+ Some(cursor.node())
}
/// Returns the outline for the buffer.
///
/// This method allows passing an optional [`SyntaxTheme`] to
/// syntax-highlight the returned symbols.
- pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
- self.outline_items_containing(0..self.len(), true, theme)
- .map(Outline::new)
+ pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Outline<Anchor> {
+ Outline::new(self.outline_items_containing(0..self.len(), true, theme))
}
/// Returns all the symbols that contain the given position.
@@ -3546,20 +3718,20 @@ impl BufferSnapshot {
&self,
position: T,
theme: Option<&SyntaxTheme>,
- ) -> Option<Vec<OutlineItem<Anchor>>> {
+ ) -> Vec<OutlineItem<Anchor>> {
let position = position.to_offset(self);
let mut items = self.outline_items_containing(
position.saturating_sub(1)..self.len().min(position + 1),
false,
theme,
- )?;
+ );
let mut prev_depth = None;
items.retain(|item| {
- let result = prev_depth.map_or(true, |prev_depth| item.depth > prev_depth);
+ let result = prev_depth.is_none_or(|prev_depth| item.depth > prev_depth);
prev_depth = Some(item.depth);
result
});
- Some(items)
+ items
}
pub fn outline_range_containing<T: ToOffset>(&self, range: Range<T>) -> Option<Range<Point>> {
@@ -3609,21 +3781,19 @@ impl BufferSnapshot {
range: Range<T>,
include_extra_context: bool,
theme: Option<&SyntaxTheme>,
- ) -> Option<Vec<OutlineItem<Anchor>>> {
+ ) -> Vec<OutlineItem<Anchor>> {
let range = range.to_offset(self);
let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
grammar.outline_config.as_ref().map(|c| &c.query)
});
- let configs = matches
- .grammars()
- .iter()
- .map(|g| g.outline_config.as_ref().unwrap())
- .collect::<Vec<_>>();
let mut items = Vec::new();
let mut annotation_row_ranges: Vec<Range<u32>> = Vec::new();
while let Some(mat) = matches.peek() {
- let config = &configs[mat.grammar_index];
+ let config = matches.grammars()[mat.grammar_index]
+ .outline_config
+ .as_ref()
+ .unwrap();
if let Some(item) =
self.next_outline_item(config, &mat, &range, include_extra_context, theme)
{
@@ -3702,7 +3872,7 @@ impl BufferSnapshot {
item_ends_stack.push(item.range.end);
}
- Some(anchor_items)
+ anchor_items
}
fn next_outline_item(
@@ -4062,11 +4232,11 @@ impl BufferSnapshot {
// Get the ranges of the innermost pair of brackets.
let mut result: Option<(Range<usize>, Range<usize>)> = None;
- for pair in self.enclosing_bracket_ranges(range.clone()) {
- if let Some(range_filter) = range_filter {
- if !range_filter(pair.open_range.clone(), pair.close_range.clone()) {
- continue;
- }
+ for pair in self.enclosing_bracket_ranges(range) {
+ if let Some(range_filter) = range_filter
+ && !range_filter(pair.open_range.clone(), pair.close_range.clone())
+ {
+ continue;
}
let len = pair.close_range.end - pair.open_range.start;
@@ -4235,7 +4405,7 @@ impl BufferSnapshot {
.map(|(range, name)| {
(
name.to_string(),
- self.text_for_range(range.clone()).collect::<String>(),
+ self.text_for_range(range).collect::<String>(),
)
})
.collect();
@@ -4432,7 +4602,7 @@ impl BufferSnapshot {
pub fn words_in_range(&self, query: WordsQuery) -> BTreeMap<String, Range<Anchor>> {
let query_str = query.fuzzy_contents;
- if query_str.map_or(false, |query| query.is_empty()) {
+ if query_str.is_some_and(|query| query.is_empty()) {
return BTreeMap::default();
}
@@ -4456,27 +4626,26 @@ impl BufferSnapshot {
current_word_start_ix = Some(ix);
}
- if let Some(query_chars) = &query_chars {
- if query_ix < query_len {
- if c.to_lowercase().eq(query_chars[query_ix].to_lowercase()) {
- query_ix += 1;
- }
- }
+ if let Some(query_chars) = &query_chars
+ && query_ix < query_len
+ && c.to_lowercase().eq(query_chars[query_ix].to_lowercase())
+ {
+ query_ix += 1;
}
continue;
- } else if let Some(word_start) = current_word_start_ix.take() {
- if query_ix == query_len {
- let word_range = self.anchor_before(word_start)..self.anchor_after(ix);
- let mut word_text = self.text_for_range(word_start..ix).peekable();
- let first_char = word_text
- .peek()
- .and_then(|first_chunk| first_chunk.chars().next());
- // Skip empty and "words" starting with digits as a heuristic to reduce useless completions
- if !query.skip_digits
- || first_char.map_or(true, |first_char| !first_char.is_digit(10))
- {
- words.insert(word_text.collect(), word_range);
- }
+ } else if let Some(word_start) = current_word_start_ix.take()
+ && query_ix == query_len
+ {
+ let word_range = self.anchor_before(word_start)..self.anchor_after(ix);
+ let mut word_text = self.text_for_range(word_start..ix).peekable();
+ let first_char = word_text
+ .peek()
+ .and_then(|first_chunk| first_chunk.chars().next());
+ // Skip empty and "words" starting with digits as a heuristic to reduce useless completions
+ if !query.skip_digits
+ || first_char.is_none_or(|first_char| !first_char.is_digit(10))
+ {
+ words.insert(word_text.collect(), word_range);
}
}
query_ix = 0;
@@ -4589,17 +4758,17 @@ impl<'a> BufferChunks<'a> {
highlights
.stack
.retain(|(end_offset, _)| *end_offset > range.start);
- if let Some(capture) = &highlights.next_capture {
- if range.start >= capture.node.start_byte() {
- let next_capture_end = capture.node.end_byte();
- if range.start < next_capture_end {
- highlights.stack.push((
- next_capture_end,
- highlights.highlight_maps[capture.grammar_index].get(capture.index),
- ));
- }
- highlights.next_capture.take();
+ if let Some(capture) = &highlights.next_capture
+ && range.start >= capture.node.start_byte()
+ {
+ let next_capture_end = capture.node.end_byte();
+ if range.start < next_capture_end {
+ highlights.stack.push((
+ next_capture_end,
+ highlights.highlight_maps[capture.grammar_index].get(capture.index),
+ ));
}
+ highlights.next_capture.take();
}
} else if let Some(snapshot) = self.buffer_snapshot {
let (captures, highlight_maps) = snapshot.get_highlights(self.range.clone());
@@ -67,6 +67,78 @@ fn test_line_endings(cx: &mut gpui::App) {
});
}
+#[gpui::test]
+fn test_set_line_ending(cx: &mut TestAppContext) {
+ let base = cx.new(|cx| Buffer::local("one\ntwo\nthree\n", cx));
+ let base_replica = cx.new(|cx| {
+ Buffer::from_proto(1, Capability::ReadWrite, base.read(cx).to_proto(cx), None).unwrap()
+ });
+ base.update(cx, |_buffer, cx| {
+ cx.subscribe(&base_replica, |this, _, event, cx| {
+ if let BufferEvent::Operation {
+ operation,
+ is_local: true,
+ } = event
+ {
+ this.apply_ops([operation.clone()], cx);
+ }
+ })
+ .detach();
+ });
+ base_replica.update(cx, |_buffer, cx| {
+ cx.subscribe(&base, |this, _, event, cx| {
+ if let BufferEvent::Operation {
+ operation,
+ is_local: true,
+ } = event
+ {
+ this.apply_ops([operation.clone()], cx);
+ }
+ })
+ .detach();
+ });
+
+ // Base
+ base_replica.read_with(cx, |buffer, _| {
+ assert_eq!(buffer.line_ending(), LineEnding::Unix);
+ });
+ base.update(cx, |buffer, cx| {
+ assert_eq!(buffer.line_ending(), LineEnding::Unix);
+ buffer.set_line_ending(LineEnding::Windows, cx);
+ assert_eq!(buffer.line_ending(), LineEnding::Windows);
+ });
+ base_replica.read_with(cx, |buffer, _| {
+ assert_eq!(buffer.line_ending(), LineEnding::Windows);
+ });
+ base.update(cx, |buffer, cx| {
+ buffer.set_line_ending(LineEnding::Unix, cx);
+ assert_eq!(buffer.line_ending(), LineEnding::Unix);
+ });
+ base_replica.read_with(cx, |buffer, _| {
+ assert_eq!(buffer.line_ending(), LineEnding::Unix);
+ });
+
+ // Replica
+ base.read_with(cx, |buffer, _| {
+ assert_eq!(buffer.line_ending(), LineEnding::Unix);
+ });
+ base_replica.update(cx, |buffer, cx| {
+ assert_eq!(buffer.line_ending(), LineEnding::Unix);
+ buffer.set_line_ending(LineEnding::Windows, cx);
+ assert_eq!(buffer.line_ending(), LineEnding::Windows);
+ });
+ base.read_with(cx, |buffer, _| {
+ assert_eq!(buffer.line_ending(), LineEnding::Windows);
+ });
+ base_replica.update(cx, |buffer, cx| {
+ buffer.set_line_ending(LineEnding::Unix, cx);
+ assert_eq!(buffer.line_ending(), LineEnding::Unix);
+ });
+ base.read_with(cx, |buffer, _| {
+ assert_eq!(buffer.line_ending(), LineEnding::Unix);
+ });
+}
+
#[gpui::test]
fn test_select_language(cx: &mut App) {
init_settings(cx, |_| {});
@@ -707,9 +779,7 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
- let outline = buffer
- .update(cx, |buffer, _| buffer.snapshot().outline(None))
- .unwrap();
+ let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None));
assert_eq!(
outline
@@ -791,9 +861,7 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
- let outline = buffer
- .update(cx, |buffer, _| buffer.snapshot().outline(None))
- .unwrap();
+ let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None));
assert_eq!(
outline
@@ -830,7 +898,7 @@ async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
// extra context nodes are included in the outline.
- let outline = snapshot.outline(None).unwrap();
+ let outline = snapshot.outline(None);
assert_eq!(
outline
.items
@@ -841,7 +909,7 @@ async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
);
// extra context nodes do not appear in breadcrumbs.
- let symbols = snapshot.symbols_containing(3, None).unwrap();
+ let symbols = snapshot.symbols_containing(3, None);
assert_eq!(
symbols
.iter()
@@ -873,9 +941,7 @@ fn test_outline_annotations(cx: &mut App) {
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
- let outline = buffer
- .update(cx, |buffer, _| buffer.snapshot().outline(None))
- .unwrap();
+ let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None));
assert_eq!(
outline
@@ -979,7 +1045,6 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
) -> Vec<(String, Range<Point>)> {
snapshot
.symbols_containing(position, None)
- .unwrap()
.into_iter()
.map(|item| {
(
@@ -1744,7 +1809,7 @@ fn test_autoindent_block_mode(cx: &mut App) {
buffer.edit(
[(Point::new(2, 8)..Point::new(2, 8), inserted_text)],
Some(AutoindentMode::Block {
- original_indent_columns: original_indent_columns.clone(),
+ original_indent_columns,
}),
cx,
);
@@ -1790,9 +1855,9 @@ fn test_autoindent_block_mode_with_newline(cx: &mut App) {
"#
.unindent();
buffer.edit(
- [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())],
+ [(Point::new(2, 0)..Point::new(2, 0), inserted_text)],
Some(AutoindentMode::Block {
- original_indent_columns: original_indent_columns.clone(),
+ original_indent_columns,
}),
cx,
);
@@ -1843,7 +1908,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut App) {
buffer.edit(
[(Point::new(2, 0)..Point::new(2, 0), inserted_text)],
Some(AutoindentMode::Block {
- original_indent_columns: original_indent_columns.clone(),
+ original_indent_columns,
}),
cx,
);
@@ -2030,7 +2095,7 @@ fn test_autoindent_with_injected_languages(cx: &mut App) {
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
language_registry.add(html_language.clone());
- language_registry.add(javascript_language.clone());
+ language_registry.add(javascript_language);
cx.new(|cx| {
let (text, ranges) = marked_text_ranges(
@@ -3013,7 +3078,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
- let base_text_len = rng.gen_range(0..10);
+ let base_text_len = rng.random_range(0..10);
let base_text = RandomCharIter::new(&mut rng)
.take(base_text_len)
.collect::<String>();
@@ -3022,7 +3087,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
let network = Arc::new(Mutex::new(Network::new(rng.clone())));
let base_buffer = cx.new(|cx| Buffer::local(base_text.as_str(), cx));
- for i in 0..rng.gen_range(min_peers..=max_peers) {
+ for i in 0..rng.random_range(min_peers..=max_peers) {
let buffer = cx.new(|cx| {
let state = base_buffer.read(cx).to_proto(cx);
let ops = cx
@@ -3035,7 +3100,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
.map(|op| proto::deserialize_operation(op).unwrap()),
cx,
);
- buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
+ buffer.set_group_interval(Duration::from_millis(rng.random_range(0..=200)));
let network = network.clone();
cx.subscribe(&cx.entity(), move |buffer, _, event, _| {
if let BufferEvent::Operation {
@@ -3066,11 +3131,11 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
let mut next_diagnostic_id = 0;
let mut active_selections = BTreeMap::default();
loop {
- let replica_index = rng.gen_range(0..replica_ids.len());
+ let replica_index = rng.random_range(0..replica_ids.len());
let replica_id = replica_ids[replica_index];
let buffer = &mut buffers[replica_index];
let mut new_buffer = None;
- match rng.gen_range(0..100) {
+ match rng.random_range(0..100) {
0..=29 if mutation_count != 0 => {
buffer.update(cx, |buffer, cx| {
buffer.start_transaction_at(now);
@@ -3082,13 +3147,13 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
}
30..=39 if mutation_count != 0 => {
buffer.update(cx, |buffer, cx| {
- if rng.gen_bool(0.2) {
+ if rng.random_bool(0.2) {
log::info!("peer {} clearing active selections", replica_id);
active_selections.remove(&replica_id);
buffer.remove_active_selections(cx);
} else {
let mut selections = Vec::new();
- for id in 0..rng.gen_range(1..=5) {
+ for id in 0..rng.random_range(1..=5) {
let range = buffer.random_byte_range(0, &mut rng);
selections.push(Selection {
id,
@@ -3111,7 +3176,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
mutation_count -= 1;
}
40..=49 if mutation_count != 0 && replica_id == 0 => {
- let entry_count = rng.gen_range(1..=5);
+ let entry_count = rng.random_range(1..=5);
buffer.update(cx, |buffer, cx| {
let diagnostics = DiagnosticSet::new(
(0..entry_count).map(|_| {
@@ -3166,7 +3231,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
new_buffer.replica_id(),
new_buffer.text()
);
- new_buffer.set_group_interval(Duration::from_millis(rng.gen_range(0..=200)));
+ new_buffer.set_group_interval(Duration::from_millis(rng.random_range(0..=200)));
let network = network.clone();
cx.subscribe(&cx.entity(), move |buffer, _, event, _| {
if let BufferEvent::Operation {
@@ -3238,7 +3303,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
_ => {}
}
- now += Duration::from_millis(rng.gen_range(0..=200));
+ now += Duration::from_millis(rng.random_range(0..=200));
buffers.extend(new_buffer);
for buffer in &buffers {
@@ -3320,23 +3385,23 @@ fn test_trailing_whitespace_ranges(mut rng: StdRng) {
// Generate a random multi-line string containing
// some lines with trailing whitespace.
let mut text = String::new();
- for _ in 0..rng.gen_range(0..16) {
- for _ in 0..rng.gen_range(0..36) {
- text.push(match rng.gen_range(0..10) {
+ for _ in 0..rng.random_range(0..16) {
+ for _ in 0..rng.random_range(0..36) {
+ text.push(match rng.random_range(0..10) {
0..=1 => ' ',
3 => '\t',
- _ => rng.gen_range('a'..='z'),
+ _ => rng.random_range('a'..='z'),
});
}
text.push('\n');
}
- match rng.gen_range(0..10) {
+ match rng.random_range(0..10) {
// sometimes remove the last newline
0..=1 => drop(text.pop()), //
// sometimes add extra newlines
- 2..=3 => text.push_str(&"\n".repeat(rng.gen_range(1..5))),
+ 2..=3 => text.push_str(&"\n".repeat(rng.random_range(1..5))),
_ => {}
}
@@ -3787,3 +3852,80 @@ fn init_settings(cx: &mut App, f: fn(&mut AllLanguageSettingsContent)) {
settings.update_user_settings::<AllLanguageSettings>(cx, f);
});
}
+
+#[gpui::test(iterations = 100)]
+fn test_random_chunk_bitmaps(cx: &mut App, mut rng: StdRng) {
+ use util::RandomCharIter;
+
+ // Generate random text
+ let len = rng.random_range(0..10000);
+ let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+
+ let buffer = cx.new(|cx| Buffer::local(text, cx));
+ let snapshot = buffer.read(cx).snapshot();
+
+ // Get all chunks and verify their bitmaps
+ let chunks = snapshot.chunks(0..snapshot.len(), false);
+
+ for chunk in chunks {
+ let chunk_text = chunk.text;
+ let chars_bitmap = chunk.chars;
+ let tabs_bitmap = chunk.tabs;
+
+ // Check empty chunks have empty bitmaps
+ if chunk_text.is_empty() {
+ assert_eq!(
+ chars_bitmap, 0,
+ "Empty chunk should have empty chars bitmap"
+ );
+ assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
+ continue;
+ }
+
+ // Verify that chunk text doesn't exceed 128 bytes
+ assert!(
+ chunk_text.len() <= 128,
+ "Chunk text length {} exceeds 128 bytes",
+ chunk_text.len()
+ );
+
+ // Verify chars bitmap
+ let char_indices = chunk_text
+ .char_indices()
+ .map(|(i, _)| i)
+ .collect::<Vec<_>>();
+
+ for byte_idx in 0..chunk_text.len() {
+ let should_have_bit = char_indices.contains(&byte_idx);
+ let has_bit = chars_bitmap & (1 << byte_idx) != 0;
+
+ if has_bit != should_have_bit {
+ eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
+ eprintln!("Char indices: {:?}", char_indices);
+ eprintln!("Chars bitmap: {:#b}", chars_bitmap);
+ }
+
+ assert_eq!(
+ has_bit, should_have_bit,
+ "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, should_have_bit, has_bit
+ );
+ }
+
+ // Verify tabs bitmap
+ for (byte_idx, byte) in chunk_text.bytes().enumerate() {
+ let is_tab = byte == b'\t';
+ let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
+
+ if has_bit != is_tab {
+ eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
+ eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
+ assert_eq!(
+ has_bit, is_tab,
+ "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, byte as char, is_tab, has_bit
+ );
+ }
+ }
+ }
+}
@@ -44,6 +44,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use serde_json::Value;
use settings::WorktreeId;
use smol::future::FutureExt as _;
+use std::num::NonZeroU32;
use std::{
any::Any,
ffi::OsStr,
@@ -59,7 +60,6 @@ use std::{
atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
},
};
-use std::{num::NonZeroU32, sync::OnceLock};
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
use task::RunnableTag;
pub use task_context::{ContextLocation, ContextProvider, RunnableRange};
@@ -67,7 +67,10 @@ pub use text_diff::{
DiffOptions, apply_diff_patch, line_diff, text_diff, text_diff_with_options, unified_diff,
};
use theme::SyntaxTheme;
-pub use toolchain::{LanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister};
+pub use toolchain::{
+ LanguageToolchainStore, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister,
+ ToolchainMetadata, ToolchainScope,
+};
use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime};
use util::serde::default_true;
@@ -119,8 +122,8 @@ where
func(cursor.deref_mut())
}
-static NEXT_LANGUAGE_ID: LazyLock<AtomicUsize> = LazyLock::new(Default::default);
-static NEXT_GRAMMAR_ID: LazyLock<AtomicUsize> = LazyLock::new(Default::default);
+static NEXT_LANGUAGE_ID: AtomicUsize = AtomicUsize::new(0);
+static NEXT_GRAMMAR_ID: AtomicUsize = AtomicUsize::new(0);
static WASM_ENGINE: LazyLock<wasmtime::Engine> = LazyLock::new(|| {
wasmtime::Engine::new(&wasmtime::Config::new()).expect("Failed to create Wasmtime engine")
});
@@ -165,7 +168,6 @@ pub struct CachedLspAdapter {
pub adapter: Arc<dyn LspAdapter>,
pub reinstall_attempt_count: AtomicU64,
cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
- manifest_name: OnceLock<Option<ManifestName>>,
}
impl Debug for CachedLspAdapter {
@@ -201,18 +203,17 @@ impl CachedLspAdapter {
adapter,
cached_binary: Default::default(),
reinstall_attempt_count: AtomicU64::new(0),
- manifest_name: Default::default(),
})
}
pub fn name(&self) -> LanguageServerName {
- self.adapter.name().clone()
+ self.adapter.name()
}
pub async fn get_language_server_command(
self: Arc<Self>,
delegate: Arc<dyn LspAdapterDelegate>,
- toolchains: Arc<dyn LanguageToolchainStore>,
+ toolchains: Option<Toolchain>,
binary_options: LanguageServerBinaryOptions,
cx: &mut AsyncApp,
) -> Result<LanguageServerBinary> {
@@ -281,21 +282,6 @@ impl CachedLspAdapter {
.cloned()
.unwrap_or_else(|| language_name.lsp_id())
}
-
- pub fn manifest_name(&self) -> Option<ManifestName> {
- self.manifest_name
- .get_or_init(|| self.adapter.manifest_name())
- .clone()
- }
-}
-
-/// Determines what gets sent out as a workspace folders content
-#[derive(Clone, Copy, Debug, PartialEq)]
-pub enum WorkspaceFoldersContent {
- /// Send out a single entry with the root of the workspace.
- WorktreeRoot,
- /// Send out a list of subproject roots.
- SubprojectRoots,
}
/// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application
@@ -327,7 +313,7 @@ pub trait LspAdapter: 'static + Send + Sync {
fn get_language_server_command<'a>(
self: Arc<Self>,
delegate: Arc<dyn LspAdapterDelegate>,
- toolchains: Arc<dyn LanguageToolchainStore>,
+ toolchains: Option<Toolchain>,
binary_options: LanguageServerBinaryOptions,
mut cached_binary: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
cx: &'a mut AsyncApp,
@@ -344,9 +330,9 @@ pub trait LspAdapter: 'static + Send + Sync {
// We only want to cache when we fall back to the global one,
// because we don't want to download and overwrite our global one
// for each worktree we might have open.
- if binary_options.allow_path_lookup {
- if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await {
- log::info!(
+ if binary_options.allow_path_lookup
+ && let Some(binary) = self.check_if_user_installed(delegate.as_ref(), toolchains, cx).await {
+ log::debug!(
"found user-installed language server for {}. path: {:?}, arguments: {:?}",
self.name().0,
binary.path,
@@ -354,7 +340,6 @@ pub trait LspAdapter: 'static + Send + Sync {
);
return Ok(binary);
}
- }
anyhow::ensure!(binary_options.allow_binary_download, "downloading language servers disabled");
@@ -402,7 +387,7 @@ pub trait LspAdapter: 'static + Send + Sync {
async fn check_if_user_installed(
&self,
_: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
None
@@ -411,6 +396,7 @@ pub trait LspAdapter: 'static + Send + Sync {
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
+ cx: &AsyncApp,
) -> Result<Box<dyn 'static + Send + Any>>;
fn will_fetch_server(
@@ -535,7 +521,7 @@ pub trait LspAdapter: 'static + Send + Sync {
self: Arc<Self>,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_cx: &mut AsyncApp,
) -> Result<Value> {
Ok(serde_json::json!({}))
@@ -555,7 +541,6 @@ pub trait LspAdapter: 'static + Send + Sync {
_target_language_server_id: LanguageServerName,
_: &dyn Fs,
_: &Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
_cx: &mut AsyncApp,
) -> Result<Option<Value>> {
Ok(None)
@@ -587,17 +572,6 @@ pub trait LspAdapter: 'static + Send + Sync {
Ok(original)
}
- /// Determines whether a language server supports workspace folders.
- ///
- /// And does not trip over itself in the process.
- fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
- WorkspaceFoldersContent::SubprojectRoots
- }
-
- fn manifest_name(&self) -> Option<ManifestName> {
- None
- }
-
/// Method only implemented by the default JSON language server adapter.
/// Used to provide dynamic reloading of the JSON schemas used to
/// provide autocompletion and diagnostics in Zed setting and keybind
@@ -616,6 +590,11 @@ pub trait LspAdapter: 'static + Send + Sync {
"Not implemented for this adapter. This method should only be called on the default JSON language server adapter"
);
}
+
+ /// True for the extension adapter and false otherwise.
+ fn is_extension(&self) -> bool {
+ false
+ }
}
async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>(
@@ -629,18 +608,18 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>
}
let name = adapter.name();
- log::info!("fetching latest version of language server {:?}", name.0);
+ log::debug!("fetching latest version of language server {:?}", name.0);
delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate);
let latest_version = adapter
- .fetch_latest_server_version(delegate.as_ref())
+ .fetch_latest_server_version(delegate.as_ref(), cx)
.await?;
if let Some(binary) = adapter
.check_if_version_installed(latest_version.as_ref(), &container_dir, delegate.as_ref())
.await
{
- log::info!("language server {:?} is already installed", name.0);
+ log::debug!("language server {:?} is already installed", name.0);
delegate.update_status(name.clone(), BinaryStatus::None);
Ok(binary)
} else {
@@ -748,6 +727,9 @@ pub struct LanguageConfig {
/// How to soft-wrap long lines of text.
#[serde(default)]
pub soft_wrap: Option<SoftWrap>,
+ /// When set, selections can be wrapped using prefix/suffix pairs on both sides.
+ #[serde(default)]
+ pub wrap_characters: Option<WrapCharactersConfig>,
/// The name of a Prettier parser that will be used for this language when no file path is available.
/// If there's a parser name in the language settings, that will be used instead.
#[serde(default)]
@@ -951,6 +933,7 @@ impl Default for LanguageConfig {
hard_tabs: None,
tab_size: None,
soft_wrap: None,
+ wrap_characters: None,
prettier_parser_name: None,
hidden: false,
jsx_tag_auto_close: None,
@@ -960,6 +943,18 @@ impl Default for LanguageConfig {
}
}
+#[derive(Clone, Debug, Deserialize, JsonSchema)]
+pub struct WrapCharactersConfig {
+ /// Opening token split into a prefix and suffix. The first caret goes
+ /// after the prefix (i.e., between prefix and suffix).
+ pub start_prefix: String,
+ pub start_suffix: String,
+ /// Closing token split into a prefix and suffix. The second caret goes
+ /// after the prefix (i.e., between prefix and suffix).
+ pub end_prefix: String,
+ pub end_suffix: String,
+}
+
fn auto_indent_using_last_non_empty_line_default() -> bool {
true
}
@@ -991,11 +986,11 @@ where
fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Regex>, D::Error> {
let sources = Vec::<String>::deserialize(d)?;
- let mut regexes = Vec::new();
- for source in sources {
- regexes.push(regex::Regex::new(&source).map_err(de::Error::custom)?);
- }
- Ok(regexes)
+ sources
+ .into_iter()
+ .map(|source| regex::Regex::new(&source))
+ .collect::<Result<_, _>>()
+ .map_err(de::Error::custom)
}
fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema {
@@ -1061,12 +1056,10 @@ impl<'de> Deserialize<'de> for BracketPairConfig {
D: Deserializer<'de>,
{
let result = Vec::<BracketPairContent>::deserialize(deserializer)?;
- let mut brackets = Vec::with_capacity(result.len());
- let mut disabled_scopes_by_bracket_ix = Vec::with_capacity(result.len());
- for entry in result {
- brackets.push(entry.bracket_pair);
- disabled_scopes_by_bracket_ix.push(entry.not_in);
- }
+ let (brackets, disabled_scopes_by_bracket_ix) = result
+ .into_iter()
+ .map(|entry| (entry.bracket_pair, entry.not_in))
+ .unzip();
Ok(BracketPairConfig {
pairs: brackets,
@@ -1108,6 +1101,7 @@ pub struct Language {
pub(crate) grammar: Option<Arc<Grammar>>,
pub(crate) context_provider: Option<Arc<dyn ContextProvider>>,
pub(crate) toolchain: Option<Arc<dyn ToolchainLister>>,
+ pub(crate) manifest_name: Option<ManifestName>,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
@@ -1263,6 +1257,7 @@ struct InjectionPatternConfig {
combined: bool,
}
+#[derive(Debug)]
struct BracketsConfig {
query: Query,
open_capture_ix: u32,
@@ -1318,6 +1313,7 @@ impl Language {
}),
context_provider: None,
toolchain: None,
+ manifest_name: None,
}
}
@@ -1331,6 +1327,10 @@ impl Language {
self
}
+ pub fn with_manifest(mut self, name: Option<ManifestName>) -> Self {
+ self.manifest_name = name;
+ self
+ }
pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
if let Some(query) = queries.highlights {
self = self
@@ -1400,16 +1400,14 @@ impl Language {
let grammar = self.grammar_mut().context("cannot mutate grammar")?;
let query = Query::new(&grammar.ts_language, source)?;
- let mut extra_captures = Vec::with_capacity(query.capture_names().len());
-
- for name in query.capture_names().iter() {
- let kind = if *name == "run" {
- RunnableCapture::Run
- } else {
- RunnableCapture::Named(name.to_string().into())
- };
- extra_captures.push(kind);
- }
+ let extra_captures: Vec<_> = query
+ .capture_names()
+ .iter()
+ .map(|&name| match name {
+ "run" => RunnableCapture::Run,
+ name => RunnableCapture::Named(name.to_string().into()),
+ })
+ .collect();
grammar.runnable_config = Some(RunnableConfig {
extra_captures,
@@ -1539,9 +1537,8 @@ impl Language {
.map(|ix| {
let mut config = BracketsPatternConfig::default();
for setting in query.property_settings(ix) {
- match setting.key.as_ref() {
- "newline.only" => config.newline_only = true,
- _ => {}
+ if setting.key.as_ref() == "newline.only" {
+ config.newline_only = true
}
}
config
@@ -1764,6 +1761,9 @@ impl Language {
pub fn name(&self) -> LanguageName {
self.config.name.clone()
}
+ pub fn manifest(&self) -> Option<&ManifestName> {
+ self.manifest_name.as_ref()
+ }
pub fn code_fence_block_name(&self) -> Arc<str> {
self.config
@@ -1798,10 +1798,10 @@ impl Language {
BufferChunks::new(text, range, Some((captures, highlight_maps)), false, None)
{
let end_offset = offset + chunk.text.len();
- if let Some(highlight_id) = chunk.syntax_highlight_id {
- if !highlight_id.is_default() {
- result.push((offset..end_offset, highlight_id));
- }
+ if let Some(highlight_id) = chunk.syntax_highlight_id
+ && !highlight_id.is_default()
+ {
+ result.push((offset..end_offset, highlight_id));
}
offset = end_offset;
}
@@ -1818,11 +1818,11 @@ impl Language {
}
pub fn set_theme(&self, theme: &SyntaxTheme) {
- if let Some(grammar) = self.grammar.as_ref() {
- if let Some(highlights_query) = &grammar.highlights_query {
- *grammar.highlight_map.lock() =
- HighlightMap::new(highlights_query.capture_names(), theme);
- }
+ if let Some(grammar) = self.grammar.as_ref()
+ && let Some(highlights_query) = &grammar.highlights_query
+ {
+ *grammar.highlight_map.lock() =
+ HighlightMap::new(highlights_query.capture_names(), theme);
}
}
@@ -1852,7 +1852,7 @@ impl Language {
impl LanguageScope {
pub fn path_suffixes(&self) -> &[String] {
- &self.language.path_suffixes()
+ self.language.path_suffixes()
}
pub fn language_name(&self) -> LanguageName {
@@ -1942,11 +1942,11 @@ impl LanguageScope {
.enumerate()
.map(move |(ix, bracket)| {
let mut is_enabled = true;
- if let Some(next_disabled_ix) = disabled_ids.first() {
- if ix == *next_disabled_ix as usize {
- disabled_ids = &disabled_ids[1..];
- is_enabled = false;
- }
+ if let Some(next_disabled_ix) = disabled_ids.first()
+ && ix == *next_disabled_ix as usize
+ {
+ disabled_ids = &disabled_ids[1..];
+ is_enabled = false;
}
(bracket, is_enabled)
})
@@ -2209,7 +2209,7 @@ impl LspAdapter for FakeLspAdapter {
async fn check_if_user_installed(
&self,
_: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
Some(self.language_server_binary.clone())
@@ -2218,7 +2218,7 @@ impl LspAdapter for FakeLspAdapter {
fn get_language_server_command<'a>(
self: Arc<Self>,
_: Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: LanguageServerBinaryOptions,
_: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
_: &'a mut AsyncApp,
@@ -2229,6 +2229,7 @@ impl LspAdapter for FakeLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Send + Any>> {
unreachable!();
}
@@ -2274,6 +2275,10 @@ impl LspAdapter for FakeLspAdapter {
let label_for_completion = self.label_for_completion.as_ref()?;
label_for_completion(item, language)
}
+
+ fn is_extension(&self) -> bool {
+ false
+ }
}
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {
@@ -1,6 +1,6 @@
use crate::{
CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher,
- LanguageServerName, LspAdapter, PLAIN_TEXT, ToolchainLister,
+ LanguageServerName, LspAdapter, ManifestName, PLAIN_TEXT, ToolchainLister,
language_settings::{
AllLanguageSettingsContent, LanguageSettingsContent, all_language_settings,
},
@@ -49,7 +49,7 @@ impl LanguageName {
pub fn from_proto(s: String) -> Self {
Self(SharedString::from(s))
}
- pub fn to_proto(self) -> String {
+ pub fn to_proto(&self) -> String {
self.0.to_string()
}
pub fn lsp_id(&self) -> String {
@@ -172,6 +172,7 @@ pub struct AvailableLanguage {
hidden: bool,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
loaded: bool,
+ manifest_name: Option<ManifestName>,
}
impl AvailableLanguage {
@@ -259,6 +260,7 @@ pub struct LoadedLanguage {
pub queries: LanguageQueries,
pub context_provider: Option<Arc<dyn ContextProvider>>,
pub toolchain_provider: Option<Arc<dyn ToolchainLister>>,
+ pub manifest_name: Option<ManifestName>,
}
impl LanguageRegistry {
@@ -349,12 +351,14 @@ impl LanguageRegistry {
config.grammar.clone(),
config.matcher.clone(),
config.hidden,
+ None,
Arc::new(move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: Default::default(),
toolchain_provider: None,
context_provider: None,
+ manifest_name: None,
})
}),
)
@@ -370,14 +374,23 @@ impl LanguageRegistry {
pub fn register_available_lsp_adapter(
&self,
name: LanguageServerName,
- load: impl Fn() -> Arc<dyn LspAdapter> + 'static + Send + Sync,
+ adapter: Arc<dyn LspAdapter>,
) {
- self.state.write().available_lsp_adapters.insert(
+ let mut state = self.state.write();
+
+ if adapter.is_extension()
+ && let Some(existing_adapter) = state.all_lsp_adapters.get(&name)
+ && !existing_adapter.adapter.is_extension()
+ {
+ log::warn!(
+ "not registering extension-provided language server {name:?}, since a builtin language server exists with that name",
+ );
+ return;
+ }
+
+ state.available_lsp_adapters.insert(
name,
- Arc::new(move || {
- let lsp_adapter = load();
- CachedLspAdapter::new(lsp_adapter)
- }),
+ Arc::new(move || CachedLspAdapter::new(adapter.clone())),
);
}
@@ -392,13 +405,21 @@ impl LanguageRegistry {
Some(load_lsp_adapter())
}
- pub fn register_lsp_adapter(
- &self,
- language_name: LanguageName,
- adapter: Arc<dyn LspAdapter>,
- ) -> Arc<CachedLspAdapter> {
- let cached = CachedLspAdapter::new(adapter);
+ pub fn register_lsp_adapter(&self, language_name: LanguageName, adapter: Arc<dyn LspAdapter>) {
let mut state = self.state.write();
+
+ if adapter.is_extension()
+ && let Some(existing_adapter) = state.all_lsp_adapters.get(&adapter.name())
+ && !existing_adapter.adapter.is_extension()
+ {
+ log::warn!(
+ "not registering extension-provided language server {:?} for language {language_name:?}, since a builtin language server exists with that name",
+ adapter.name(),
+ );
+ return;
+ }
+
+ let cached = CachedLspAdapter::new(adapter);
state
.lsp_adapters
.entry(language_name)
@@ -407,8 +428,6 @@ impl LanguageRegistry {
state
.all_lsp_adapters
.insert(cached.name.clone(), cached.clone());
-
- cached
}
/// Register a fake language server and adapter
@@ -428,7 +447,7 @@ impl LanguageRegistry {
let mut state = self.state.write();
state
.lsp_adapters
- .entry(language_name.clone())
+ .entry(language_name)
.or_default()
.push(adapter.clone());
state.all_lsp_adapters.insert(adapter.name(), adapter);
@@ -450,7 +469,7 @@ impl LanguageRegistry {
let cached_adapter = CachedLspAdapter::new(Arc::new(adapter));
state
.lsp_adapters
- .entry(language_name.clone())
+ .entry(language_name)
.or_default()
.push(cached_adapter.clone());
state
@@ -487,6 +506,7 @@ impl LanguageRegistry {
grammar_name: Option<Arc<str>>,
matcher: LanguageMatcher,
hidden: bool,
+ manifest_name: Option<ManifestName>,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) {
let state = &mut *self.state.write();
@@ -496,6 +516,7 @@ impl LanguageRegistry {
existing_language.grammar = grammar_name;
existing_language.matcher = matcher;
existing_language.load = load;
+ existing_language.manifest_name = manifest_name;
return;
}
}
@@ -508,6 +529,7 @@ impl LanguageRegistry {
load,
hidden,
loaded: false,
+ manifest_name,
});
state.version += 1;
state.reload_count += 1;
@@ -575,6 +597,7 @@ impl LanguageRegistry {
grammar: language.config.grammar.clone(),
matcher: language.config.matcher.clone(),
hidden: language.config.hidden,
+ manifest_name: None,
load: Arc::new(|| Err(anyhow!("already loaded"))),
loaded: true,
});
@@ -765,7 +788,7 @@ impl LanguageRegistry {
};
let content_matches = || {
- config.first_line_pattern.as_ref().map_or(false, |pattern| {
+ config.first_line_pattern.as_ref().is_some_and(|pattern| {
content
.as_ref()
.is_some_and(|content| pattern.is_match(content))
@@ -914,10 +937,12 @@ impl LanguageRegistry {
Language::new_with_id(id, loaded_language.config, grammar)
.with_context_provider(loaded_language.context_provider)
.with_toolchain_lister(loaded_language.toolchain_provider)
+ .with_manifest(loaded_language.manifest_name)
.with_queries(loaded_language.queries)
} else {
Ok(Language::new_with_id(id, loaded_language.config, None)
.with_context_provider(loaded_language.context_provider)
+ .with_manifest(loaded_language.manifest_name)
.with_toolchain_lister(loaded_language.toolchain_provider))
}
}
@@ -1092,7 +1117,7 @@ impl LanguageRegistry {
use gpui::AppContext as _;
let mut state = self.state.write();
- let fake_entry = state.fake_server_entries.get_mut(&name)?;
+ let fake_entry = state.fake_server_entries.get_mut(name)?;
let (server, mut fake_server) = lsp::FakeLanguageServer::new(
server_id,
binary,
@@ -1157,8 +1182,7 @@ impl LanguageRegistryState {
soft_wrap: language.config.soft_wrap,
auto_indent_on_paste: language.config.auto_indent_on_paste,
..Default::default()
- }
- .clone(),
+ },
);
self.languages.push(language);
self.version += 1;
@@ -5,10 +5,10 @@ use anyhow::Result;
use collections::{FxHashMap, HashMap, HashSet};
use ec4rs::{
Properties as EditorconfigProperties,
- property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs},
+ property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs},
};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
-use gpui::{App, Modifiers};
+use gpui::{App, Modifiers, SharedString};
use itertools::{Either, Itertools};
use schemars::{JsonSchema, json_schema};
use serde::{
@@ -17,7 +17,8 @@ use serde::{
};
use settings::{
- ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore,
+ ParameterizedJsonSchema, Settings, SettingsKey, SettingsLocation, SettingsSources,
+ SettingsStore, SettingsUi,
};
use shellexpand;
use std::{borrow::Cow, num::NonZeroU32, path::Path, slice, sync::Arc};
@@ -122,6 +123,8 @@ pub struct LanguageSettings {
pub edit_predictions_disabled_in: Vec<String>,
/// Whether to show tabs and spaces in the editor.
pub show_whitespaces: ShowWhitespaceSetting,
+ /// Visible characters used to render whitespace when show_whitespaces is enabled.
+ pub whitespace_map: WhitespaceMap,
/// Whether to start a new line with a comment when a previous line is a comment as well.
pub extend_comment_on_newline: bool,
/// Inlay hint related settings.
@@ -133,6 +136,8 @@ pub struct LanguageSettings {
/// Whether to use additional LSP queries to format (and amend) the code after
/// every "trigger" symbol input, defined by LSP server capabilities.
pub use_on_type_format: bool,
+ /// Whether indentation should be adjusted based on the context whilst typing.
+ pub auto_indent: bool,
/// Whether indentation of pasted content should be adjusted based on the context.
pub auto_indent_on_paste: bool,
/// Controls how the editor handles the autoclosed characters.
@@ -185,8 +190,8 @@ impl LanguageSettings {
let rest = available_language_servers
.iter()
.filter(|&available_language_server| {
- !disabled_language_servers.contains(&available_language_server)
- && !enabled_language_servers.contains(&available_language_server)
+ !disabled_language_servers.contains(available_language_server)
+ && !enabled_language_servers.contains(available_language_server)
})
.cloned()
.collect::<Vec<_>>();
@@ -197,7 +202,7 @@ impl LanguageSettings {
if language_server.0.as_ref() == Self::REST_OF_LANGUAGE_SERVERS {
rest.clone()
} else {
- vec![language_server.clone()]
+ vec![language_server]
}
})
.collect::<Vec<_>>()
@@ -205,7 +210,9 @@ impl LanguageSettings {
}
/// The provider that supplies edit predictions.
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+ Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub enum EditPredictionProvider {
None,
@@ -228,13 +235,14 @@ impl EditPredictionProvider {
/// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot)
/// or [Supermaven](https://supermaven.com).
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug, Default, SettingsUi)]
pub struct EditPredictionSettings {
/// The provider that supplies edit predictions.
pub provider: EditPredictionProvider,
/// A list of globs representing files that edit predictions should be disabled for.
/// This list adds to a pre-existing, sensible default set of globs.
/// Any additional ones you add are combined with them.
+ #[settings_ui(skip)]
pub disabled_globs: Vec<DisabledGlob>,
/// Configures how edit predictions are displayed in the buffer.
pub mode: EditPredictionsMode,
@@ -251,7 +259,7 @@ impl EditPredictionSettings {
!self.disabled_globs.iter().any(|glob| {
if glob.is_absolute {
file.as_local()
- .map_or(false, |local| glob.matcher.is_match(local.abs_path(cx)))
+ .is_some_and(|local| glob.matcher.is_match(local.abs_path(cx)))
} else {
glob.matcher.is_match(file.path())
}
@@ -266,7 +274,9 @@ pub struct DisabledGlob {
}
/// The mode in which edit predictions should be displayed.
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+ Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub enum EditPredictionsMode {
/// If provider supports it, display inline when holding modifier key (e.g., alt).
@@ -279,18 +289,24 @@ pub enum EditPredictionsMode {
Eager,
}
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug, Default, SettingsUi)]
pub struct CopilotSettings {
/// HTTP/HTTPS proxy to use for Copilot.
+ #[settings_ui(skip)]
pub proxy: Option<String>,
/// Disable certificate verification for proxy (not recommended).
pub proxy_no_verify: Option<bool>,
/// Enterprise URI for Copilot.
+ #[settings_ui(skip)]
pub enterprise_uri: Option<String>,
}
/// The settings for all languages.
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+ Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey,
+)]
+#[settings_key(None)]
+#[settings_ui(group = "Default Language Settings")]
pub struct AllLanguageSettingsContent {
/// The settings for enabling/disabling features.
#[serde(default)]
@@ -307,6 +323,7 @@ pub struct AllLanguageSettingsContent {
/// Settings for associating file extensions and filenames
/// with languages.
#[serde(default)]
+ #[settings_ui(skip)]
pub file_types: HashMap<Arc<str>, Vec<String>>,
}
@@ -315,6 +332,37 @@ pub struct AllLanguageSettingsContent {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct LanguageToSettingsMap(pub HashMap<LanguageName, LanguageSettingsContent>);
+impl SettingsUi for LanguageToSettingsMap {
+ fn settings_ui_item() -> settings::SettingsUiItem {
+ settings::SettingsUiItem::DynamicMap(settings::SettingsUiItemDynamicMap {
+ item: LanguageSettingsContent::settings_ui_item,
+ defaults_path: &[],
+ determine_items: |settings_value, cx| {
+ use settings::SettingsUiEntryMetaData;
+
+ // todo(settings_ui): We should be using a global LanguageRegistry, but it's not implemented yet
+ _ = cx;
+
+ let Some(settings_language_map) = settings_value.as_object() else {
+ return Vec::new();
+ };
+ let mut languages = Vec::with_capacity(settings_language_map.len());
+
+ for language_name in settings_language_map.keys().map(gpui::SharedString::from) {
+ languages.push(SettingsUiEntryMetaData {
+ title: language_name.clone(),
+ path: language_name,
+ // todo(settings_ui): Implement documentation for each language
+ // ideally based on the language's official docs from extension or builtin info
+ documentation: None,
+ });
+ }
+ return languages;
+ },
+ })
+ }
+}
+
inventory::submit! {
ParameterizedJsonSchema {
add_and_get_ref: |generator, params, _cx| {
@@ -339,7 +387,7 @@ inventory::submit! {
}
/// Controls how completions are processed for this language.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub struct CompletionSettings {
/// Controls how words are completed.
@@ -348,6 +396,12 @@ pub struct CompletionSettings {
/// Default: `fallback`
#[serde(default = "default_words_completion_mode")]
pub words: WordsCompletionMode,
+ /// How many characters has to be in the completions query to automatically show the words-based completions.
+ /// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
+ ///
+ /// Default: 3
+ #[serde(default = "default_3")]
+ pub words_min_length: usize,
/// Whether to fetch LSP completions or not.
///
/// Default: true
@@ -357,7 +411,7 @@ pub struct CompletionSettings {
/// When set to 0, waits indefinitely.
///
/// Default: 0
- #[serde(default = "default_lsp_fetch_timeout_ms")]
+ #[serde(default)]
pub lsp_fetch_timeout_ms: u64,
/// Controls how LSP completions are inserted.
///
@@ -403,17 +457,19 @@ fn default_lsp_insert_mode() -> LspInsertMode {
LspInsertMode::ReplaceSuffix
}
-fn default_lsp_fetch_timeout_ms() -> u64 {
- 0
+fn default_3() -> usize {
+ 3
}
/// The settings for a particular language.
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)]
+#[settings_ui(group = "Default")]
pub struct LanguageSettingsContent {
/// How many columns a tab should occupy.
///
/// Default: 4
#[serde(default)]
+ #[settings_ui(skip)]
pub tab_size: Option<NonZeroU32>,
/// Whether to indent lines using tab characters, as opposed to multiple
/// spaces.
@@ -444,6 +500,7 @@ pub struct LanguageSettingsContent {
///
/// Default: []
#[serde(default)]
+ #[settings_ui(skip)]
pub wrap_guides: Option<Vec<usize>>,
/// Indent guide related settings.
#[serde(default)]
@@ -469,6 +526,7 @@ pub struct LanguageSettingsContent {
///
/// Default: auto
#[serde(default)]
+ #[settings_ui(skip)]
pub formatter: Option<SelectedFormatter>,
/// Zed's Prettier integration settings.
/// Allows to enable/disable formatting with Prettier
@@ -494,6 +552,7 @@ pub struct LanguageSettingsContent {
///
/// Default: ["..."]
#[serde(default)]
+ #[settings_ui(skip)]
pub language_servers: Option<Vec<String>>,
/// Controls where the `editor::Rewrap` action is allowed for this language.
///
@@ -516,10 +575,16 @@ pub struct LanguageSettingsContent {
///
/// Default: []
#[serde(default)]
+ #[settings_ui(skip)]
pub edit_predictions_disabled_in: Option<Vec<String>>,
/// Whether to show tabs and spaces in the editor.
#[serde(default)]
pub show_whitespaces: Option<ShowWhitespaceSetting>,
+ /// Visible characters used to render whitespace when show_whitespaces is enabled.
+ ///
+ /// Default: "•" for spaces, "→" for tabs.
+ #[serde(default)]
+ pub whitespace_map: Option<WhitespaceMap>,
/// Whether to start a new line with a comment when a previous line is a comment as well.
///
/// Default: true
@@ -555,12 +620,17 @@ pub struct LanguageSettingsContent {
/// These are not run if formatting is off.
///
/// Default: {} (or {"source.organizeImports": true} for Go).
+ #[settings_ui(skip)]
pub code_actions_on_format: Option<HashMap<String, bool>>,
/// Whether to perform linked edits of associated ranges, if the language server supports it.
/// For example, when editing opening <html> tag, the contents of the closing </html> tag will be edited as well.
///
/// Default: true
pub linked_edits: Option<bool>,
+ /// Whether indentation should be adjusted based on the context whilst typing.
+ ///
+ /// Default: true
+ pub auto_indent: Option<bool>,
/// Whether indentation of pasted content should be adjusted based on the context.
///
/// Default: true
@@ -584,11 +654,14 @@ pub struct LanguageSettingsContent {
/// Preferred debuggers for this language.
///
/// Default: []
+ #[settings_ui(skip)]
pub debuggers: Option<Vec<String>>,
}
/// The behavior of `editor::Rewrap`.
-#[derive(Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(
+ Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub enum RewrapBehavior {
/// Only rewrap within comments.
@@ -601,12 +674,13 @@ pub enum RewrapBehavior {
}
/// The contents of the edit prediction settings.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)]
pub struct EditPredictionSettingsContent {
/// A list of globs representing files that edit predictions should be disabled for.
/// This list adds to a pre-existing, sensible default set of globs.
/// Any additional ones you add are combined with them.
#[serde(default)]
+ #[settings_ui(skip)]
pub disabled_globs: Option<Vec<String>>,
/// The mode used to display edit predictions in the buffer.
/// Provider support required.
@@ -621,12 +695,13 @@ pub struct EditPredictionSettingsContent {
pub enabled_in_text_threads: bool,
}
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)]
pub struct CopilotSettingsContent {
/// HTTP/HTTPS proxy to use for Copilot.
///
/// Default: none
#[serde(default)]
+ #[settings_ui(skip)]
pub proxy: Option<String>,
/// Disable certificate verification for the proxy (not recommended).
///
@@ -637,19 +712,21 @@ pub struct CopilotSettingsContent {
///
/// Default: none
#[serde(default)]
+ #[settings_ui(skip)]
pub enterprise_uri: Option<String>,
}
/// The settings for enabling/disabling features.
-#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
+#[settings_ui(group = "Features")]
pub struct FeaturesContent {
/// Determines which edit prediction provider to use.
pub edit_prediction_provider: Option<EditPredictionProvider>,
}
/// Controls the soft-wrapping behavior in the editor.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum SoftWrap {
/// Prefer a single line generally, unless an overly long line is encountered.
@@ -666,7 +743,7 @@ pub enum SoftWrap {
}
/// Controls the behavior of formatting files when they are saved.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, SettingsUi)]
pub enum FormatOnSave {
/// Files should be formatted on save.
On,
@@ -765,7 +842,7 @@ impl<'de> Deserialize<'de> for FormatOnSave {
}
/// Controls how whitespace should be displayedin the editor.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum ShowWhitespaceSetting {
/// Draw whitespace only for the selected text.
@@ -785,8 +862,30 @@ pub enum ShowWhitespaceSetting {
Trailing,
}
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)]
+pub struct WhitespaceMap {
+ #[serde(default)]
+ pub space: Option<String>,
+ #[serde(default)]
+ pub tab: Option<String>,
+}
+
+impl WhitespaceMap {
+ pub fn space(&self) -> SharedString {
+ self.space
+ .as_ref()
+ .map_or_else(|| SharedString::from("•"), |s| SharedString::from(s))
+ }
+
+ pub fn tab(&self) -> SharedString {
+ self.tab
+ .as_ref()
+ .map_or_else(|| SharedString::from("→"), |s| SharedString::from(s))
+ }
+}
+
/// Controls which formatter should be used when formatting code.
-#[derive(Clone, Debug, Default, PartialEq, Eq)]
+#[derive(Clone, Debug, Default, PartialEq, Eq, SettingsUi)]
pub enum SelectedFormatter {
/// Format files using Zed's Prettier integration (if applicable),
/// or falling back to formatting via language server.
@@ -882,11 +981,17 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
}
/// Controls which formatters should be used when formatting code.
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[serde(untagged)]
pub enum FormatterList {
Single(Formatter),
- Vec(Vec<Formatter>),
+ Vec(#[settings_ui(skip)] Vec<Formatter>),
+}
+
+impl Default for FormatterList {
+ fn default() -> Self {
+ Self::Single(Formatter::default())
+ }
}
impl AsRef<[Formatter]> for FormatterList {
@@ -899,26 +1004,34 @@ impl AsRef<[Formatter]> for FormatterList {
}
/// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration.
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum Formatter {
/// Format code using the current language server.
- LanguageServer { name: Option<String> },
+ LanguageServer {
+ #[settings_ui(skip)]
+ name: Option<String>,
+ },
/// Format code using Zed's Prettier integration.
+ #[default]
Prettier,
/// Format code using an external command.
External {
/// The external program to run.
+ #[settings_ui(skip)]
command: Arc<str>,
/// The arguments to pass to the program.
+ #[settings_ui(skip)]
arguments: Option<Arc<[String]>>,
},
/// Files should be formatted using code actions executed by language servers.
- CodeActions(HashMap<String, bool>),
+ CodeActions(#[settings_ui(skip)] HashMap<String, bool>),
}
/// The settings for indent guides.
-#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+ Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi,
+)]
pub struct IndentGuideSettings {
/// Whether to display indent guides in the editor.
///
@@ -980,7 +1093,7 @@ pub enum IndentGuideBackgroundColoring {
}
/// The settings for inlay hints.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
pub struct InlayHintSettings {
/// Global switch to toggle hints on and off.
///
@@ -1047,7 +1160,7 @@ fn scroll_debounce_ms() -> u64 {
}
/// The task settings for a particular language.
-#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema)]
+#[derive(Debug, Clone, Deserialize, PartialEq, Serialize, JsonSchema, SettingsUi)]
pub struct LanguageTaskConfig {
/// Extra task variables to set for a particular language.
#[serde(default)]
@@ -1125,6 +1238,10 @@ impl AllLanguageSettings {
}
fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
+ let preferred_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
+ MaxLineLen::Value(u) => Some(u as u32),
+ MaxLineLen::Off => None,
+ });
let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
IndentSize::Value(u) => NonZeroU32::new(u as u32),
IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
@@ -1152,6 +1269,7 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr
*target = value;
}
}
+ merge(&mut settings.preferred_line_length, preferred_line_length);
merge(&mut settings.tab_size, tab_size);
merge(&mut settings.hard_tabs, hard_tabs);
merge(
@@ -1196,8 +1314,6 @@ impl InlayHintKind {
}
impl settings::Settings for AllLanguageSettings {
- const KEY: Option<&'static str> = None;
-
type FileContent = AllLanguageSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
@@ -1457,6 +1573,7 @@ impl settings::Settings for AllLanguageSettings {
} else {
d.completions = Some(CompletionSettings {
words: mode,
+ words_min_length: 3,
lsp: true,
lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::ReplaceSuffix,
@@ -1517,6 +1634,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
merge(&mut settings.use_autoclose, src.use_autoclose);
merge(&mut settings.use_auto_surround, src.use_auto_surround);
merge(&mut settings.use_on_type_format, src.use_on_type_format);
+ merge(&mut settings.auto_indent, src.auto_indent);
merge(&mut settings.auto_indent_on_paste, src.auto_indent_on_paste);
merge(
&mut settings.always_treat_brackets_as_autoclosed,
@@ -1566,6 +1684,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
src.edit_predictions_disabled_in.clone(),
);
merge(&mut settings.show_whitespaces, src.show_whitespaces);
+ merge(&mut settings.whitespace_map, src.whitespace_map.clone());
merge(
&mut settings.extend_comment_on_newline,
src.extend_comment_on_newline,
@@ -1585,7 +1704,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
/// Allows to enable/disable formatting with Prettier
/// and configure default Prettier, used when no project-level Prettier installation is found.
/// Prettier formatting is disabled by default.
-#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
pub struct PrettierSettings {
/// Enables or disables formatting with Prettier for a given language.
#[serde(default)]
@@ -1598,15 +1717,17 @@ pub struct PrettierSettings {
/// Forces Prettier integration to use specific plugins when formatting files with the language.
/// The default Prettier will be installed with these plugins.
#[serde(default)]
+ #[settings_ui(skip)]
pub plugins: HashSet<String>,
/// Default Prettier options, in the format as in package.json section for Prettier.
/// If project installs Prettier via its package.json, these options will be ignored.
#[serde(flatten)]
+ #[settings_ui(skip)]
pub options: HashMap<String, serde_json::Value>,
}
-#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi)]
pub struct JsxTagAutoCloseSettings {
/// Enables or disables auto-closing of JSX tags.
#[serde(default)]
@@ -1786,7 +1907,7 @@ mod tests {
assert!(!settings.enabled_for_file(&dot_env_file, &cx));
// Test tilde expansion
- let home = shellexpand::tilde("~").into_owned().to_string();
+ let home = shellexpand::tilde("~").into_owned();
let home_file = make_test_file(&[&home, "test.rs"]);
let settings = build_settings(&["~/test.rs"]);
assert!(!settings.enabled_for_file(&home_file, &cx));
@@ -12,6 +12,12 @@ impl Borrow<SharedString> for ManifestName {
}
}
+impl Borrow<str> for ManifestName {
+ fn borrow(&self) -> &str {
+ &self.0
+ }
+}
+
impl From<SharedString> for ManifestName {
fn from(value: SharedString) -> Self {
Self(value)
@@ -86,10 +86,19 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
proto::operation::UpdateCompletionTriggers {
replica_id: lamport_timestamp.replica_id as u32,
lamport_timestamp: lamport_timestamp.value,
- triggers: triggers.iter().cloned().collect(),
+ triggers: triggers.clone(),
language_server_id: server_id.to_proto(),
},
),
+
+ crate::Operation::UpdateLineEnding {
+ line_ending,
+ lamport_timestamp,
+ } => proto::operation::Variant::UpdateLineEnding(proto::operation::UpdateLineEnding {
+ replica_id: lamport_timestamp.replica_id as u32,
+ lamport_timestamp: lamport_timestamp.value,
+ line_ending: serialize_line_ending(*line_ending) as i32,
+ }),
}),
}
}
@@ -341,6 +350,18 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
server_id: LanguageServerId::from_proto(message.language_server_id),
}
}
+ proto::operation::Variant::UpdateLineEnding(message) => {
+ crate::Operation::UpdateLineEnding {
+ lamport_timestamp: clock::Lamport {
+ replica_id: message.replica_id as ReplicaId,
+ value: message.lamport_timestamp,
+ },
+ line_ending: deserialize_line_ending(
+ proto::LineEnding::from_i32(message.line_ending)
+ .context("missing line_ending")?,
+ ),
+ }
+ }
},
)
}
@@ -385,12 +406,10 @@ pub fn deserialize_undo_map_entry(
/// Deserializes selections from the RPC representation.
pub fn deserialize_selections(selections: Vec<proto::Selection>) -> Arc<[Selection<Anchor>]> {
- Arc::from(
- selections
- .into_iter()
- .filter_map(deserialize_selection)
- .collect::<Vec<_>>(),
- )
+ selections
+ .into_iter()
+ .filter_map(deserialize_selection)
+ .collect()
}
/// Deserializes a [`Selection`] from the RPC representation.
@@ -433,7 +452,7 @@ pub fn deserialize_diagnostics(
code: diagnostic.code.map(lsp::NumberOrString::from_string),
code_description: diagnostic
.code_description
- .and_then(|s| lsp::Url::parse(&s).ok()),
+ .and_then(|s| lsp::Uri::from_str(&s).ok()),
is_primary: diagnostic.is_primary,
is_disk_based: diagnostic.is_disk_based,
is_unnecessary: diagnostic.is_unnecessary,
@@ -498,6 +517,10 @@ pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option<c
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
+ proto::operation::Variant::UpdateLineEnding(op) => {
+ replica_id = op.replica_id;
+ value = op.lamport_timestamp;
+ }
}
Some(clock::Lamport {
@@ -414,42 +414,42 @@ impl SyntaxSnapshot {
.collect::<Vec<_>>();
self.reparse_with_ranges(text, root_language.clone(), edit_ranges, registry.as_ref());
- if let Some(registry) = registry {
- if registry.version() != self.language_registry_version {
- let mut resolved_injection_ranges = Vec::new();
- let mut cursor = self
- .layers
- .filter::<_, ()>(text, |summary| summary.contains_unknown_injections);
- cursor.next();
- while let Some(layer) = cursor.item() {
- let SyntaxLayerContent::Pending { language_name } = &layer.content else {
- unreachable!()
- };
- if registry
- .language_for_name_or_extension(language_name)
- .now_or_never()
- .and_then(|language| language.ok())
- .is_some()
- {
- let range = layer.range.to_offset(text);
- log::trace!("reparse range {range:?} for language {language_name:?}");
- resolved_injection_ranges.push(range);
- }
-
- cursor.next();
- }
- drop(cursor);
-
- if !resolved_injection_ranges.is_empty() {
- self.reparse_with_ranges(
- text,
- root_language,
- resolved_injection_ranges,
- Some(®istry),
- );
+ if let Some(registry) = registry
+ && registry.version() != self.language_registry_version
+ {
+ let mut resolved_injection_ranges = Vec::new();
+ let mut cursor = self
+ .layers
+ .filter::<_, ()>(text, |summary| summary.contains_unknown_injections);
+ cursor.next();
+ while let Some(layer) = cursor.item() {
+ let SyntaxLayerContent::Pending { language_name } = &layer.content else {
+ unreachable!()
+ };
+ if registry
+ .language_for_name_or_extension(language_name)
+ .now_or_never()
+ .and_then(|language| language.ok())
+ .is_some()
+ {
+ let range = layer.range.to_offset(text);
+ log::trace!("reparse range {range:?} for language {language_name:?}");
+ resolved_injection_ranges.push(range);
}
- self.language_registry_version = registry.version();
+
+ cursor.next();
}
+ drop(cursor);
+
+ if !resolved_injection_ranges.is_empty() {
+ self.reparse_with_ranges(
+ text,
+ root_language,
+ resolved_injection_ranges,
+ Some(®istry),
+ );
+ }
+ self.language_registry_version = registry.version();
}
self.update_count += 1;
@@ -832,7 +832,7 @@ impl SyntaxSnapshot {
query: fn(&Grammar) -> Option<&Query>,
) -> SyntaxMapCaptures<'a> {
SyntaxMapCaptures::new(
- range.clone(),
+ range,
text,
[SyntaxLayer {
language,
@@ -1065,10 +1065,10 @@ impl<'a> SyntaxMapCaptures<'a> {
pub fn set_byte_range(&mut self, range: Range<usize>) {
for layer in &mut self.layers {
layer.captures.set_byte_range(range.clone());
- if let Some(capture) = &layer.next_capture {
- if capture.node.end_byte() > range.start {
- continue;
- }
+ if let Some(capture) = &layer.next_capture
+ && capture.node.end_byte() > range.start
+ {
+ continue;
}
layer.advance();
}
@@ -1277,11 +1277,11 @@ fn join_ranges(
(None, None) => break,
};
- if let Some(last) = result.last_mut() {
- if range.start <= last.end {
- last.end = last.end.max(range.end);
- continue;
- }
+ if let Some(last) = result.last_mut()
+ && range.start <= last.end
+ {
+ last.end = last.end.max(range.end);
+ continue;
}
result.push(range);
}
@@ -1297,7 +1297,7 @@ fn parse_text(
) -> anyhow::Result<Tree> {
with_parser(|parser| {
let mut chunks = text.chunks_in_range(start_byte..text.len());
- parser.set_included_ranges(&ranges)?;
+ parser.set_included_ranges(ranges)?;
parser.set_language(&grammar.ts_language)?;
parser
.parse_with_options(
@@ -1330,14 +1330,13 @@ fn get_injections(
// if there currently no matches for that injection.
combined_injection_ranges.clear();
for pattern in &config.patterns {
- if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined) {
- if let Some(language) = language_registry
+ if let (Some(language_name), true) = (pattern.language.as_ref(), pattern.combined)
+ && let Some(language) = language_registry
.language_for_name_or_extension(language_name)
.now_or_never()
.and_then(|language| language.ok())
- {
- combined_injection_ranges.insert(language.id, (language, Vec::new()));
- }
+ {
+ combined_injection_ranges.insert(language.id, (language, Vec::new()));
}
}
@@ -1357,10 +1356,11 @@ fn get_injections(
content_ranges.first().unwrap().start_byte..content_ranges.last().unwrap().end_byte;
// Avoid duplicate matches if two changed ranges intersect the same injection.
- if let Some((prev_pattern_ix, prev_range)) = &prev_match {
- if mat.pattern_index == *prev_pattern_ix && content_range == *prev_range {
- continue;
- }
+ if let Some((prev_pattern_ix, prev_range)) = &prev_match
+ && mat.pattern_index == *prev_pattern_ix
+ && content_range == *prev_range
+ {
+ continue;
}
prev_match = Some((mat.pattern_index, content_range.clone()));
@@ -1630,10 +1630,8 @@ impl<'a> SyntaxLayer<'a> {
if offset < range.start || offset > range.end {
continue;
}
- } else {
- if offset <= range.start || offset >= range.end {
- continue;
- }
+ } else if offset <= range.start || offset >= range.end {
+ continue;
}
if let Some((_, smallest_range)) = &smallest_match {
@@ -58,8 +58,7 @@ fn test_splice_included_ranges() {
assert_eq!(change, 0..1);
// does not create overlapping ranges
- let (new_ranges, change) =
- splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]);
+ let (new_ranges, change) = splice_included_ranges(ranges, &[0..18], &[ts_range(20..32)]);
assert_eq!(
new_ranges,
&[ts_range(20..32), ts_range(50..60), ts_range(80..90)]
@@ -104,7 +103,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) {
);
let mut syntax_map = SyntaxMap::new(&buffer);
- syntax_map.set_language_registry(registry.clone());
+ syntax_map.set_language_registry(registry);
syntax_map.reparse(language.clone(), &buffer);
assert_layers_for_range(
@@ -165,7 +164,7 @@ fn test_syntax_map_layers_for_range(cx: &mut App) {
// Put the vec! macro back, adding back the syntactic layer.
buffer.undo();
syntax_map.interpolate(&buffer);
- syntax_map.reparse(language.clone(), &buffer);
+ syntax_map.reparse(language, &buffer);
assert_layers_for_range(
&syntax_map,
@@ -252,8 +251,8 @@ fn test_dynamic_language_injection(cx: &mut App) {
assert!(syntax_map.contains_unknown_injections());
registry.add(Arc::new(html_lang()));
- syntax_map.reparse(markdown.clone(), &buffer);
- syntax_map.reparse(markdown_inline.clone(), &buffer);
+ syntax_map.reparse(markdown, &buffer);
+ syntax_map.reparse(markdown_inline, &buffer);
assert_layers_for_range(
&syntax_map,
&buffer,
@@ -862,7 +861,7 @@ fn test_syntax_map_languages_loading_with_erb(cx: &mut App) {
log::info!("editing");
buffer.edit_via_marked_text(&text);
syntax_map.interpolate(&buffer);
- syntax_map.reparse(language.clone(), &buffer);
+ syntax_map.reparse(language, &buffer);
assert_capture_ranges(
&syntax_map,
@@ -986,7 +985,7 @@ fn test_random_edits(
syntax_map.reparse(language.clone(), &buffer);
let mut reference_syntax_map = SyntaxMap::new(&buffer);
- reference_syntax_map.set_language_registry(registry.clone());
+ reference_syntax_map.set_language_registry(registry);
log::info!("initial text:\n{}", buffer.text());
@@ -88,11 +88,11 @@ pub fn text_diff_with_options(
let new_offset = new_byte_range.start;
hunk_input.clear();
hunk_input.update_before(tokenize(
- &old_text[old_byte_range.clone()],
+ &old_text[old_byte_range],
options.language_scope.clone(),
));
hunk_input.update_after(tokenize(
- &new_text[new_byte_range.clone()],
+ &new_text[new_byte_range],
options.language_scope.clone(),
));
diff_internal(&hunk_input, |old_byte_range, new_byte_range, _, _| {
@@ -103,7 +103,7 @@ pub fn text_diff_with_options(
let replacement_text = if new_byte_range.is_empty() {
empty.clone()
} else {
- new_text[new_byte_range.clone()].into()
+ new_text[new_byte_range].into()
};
edits.push((old_byte_range, replacement_text));
});
@@ -111,9 +111,9 @@ pub fn text_diff_with_options(
let replacement_text = if new_byte_range.is_empty() {
empty.clone()
} else {
- new_text[new_byte_range.clone()].into()
+ new_text[new_byte_range].into()
};
- edits.push((old_byte_range.clone(), replacement_text));
+ edits.push((old_byte_range, replacement_text));
}
},
);
@@ -154,19 +154,19 @@ fn diff_internal(
input,
|old_tokens: Range<u32>, new_tokens: Range<u32>| {
old_offset += token_len(
- &input,
+ input,
&input.before[old_token_ix as usize..old_tokens.start as usize],
);
new_offset += token_len(
- &input,
+ input,
&input.after[new_token_ix as usize..new_tokens.start as usize],
);
let old_len = token_len(
- &input,
+ input,
&input.before[old_tokens.start as usize..old_tokens.end as usize],
);
let new_len = token_len(
- &input,
+ input,
&input.after[new_tokens.start as usize..new_tokens.end as usize],
);
let old_byte_range = old_offset..old_offset + old_len;
@@ -186,14 +186,14 @@ fn tokenize(text: &str, language_scope: Option<LanguageScope>) -> impl Iterator<
let mut prev = None;
let mut start_ix = 0;
iter::from_fn(move || {
- while let Some((ix, c)) = chars.next() {
+ for (ix, c) in chars.by_ref() {
let mut token = None;
let kind = classifier.kind(c);
- if let Some((prev_char, prev_kind)) = prev {
- if kind != prev_kind || (kind == CharKind::Punctuation && c != prev_char) {
- token = Some(&text[start_ix..ix]);
- start_ix = ix;
- }
+ if let Some((prev_char, prev_kind)) = prev
+ && (kind != prev_kind || (kind == CharKind::Punctuation && c != prev_char))
+ {
+ token = Some(&text[start_ix..ix]);
+ start_ix = ix;
}
prev = Some((c, kind));
if token.is_some() {
@@ -11,13 +11,15 @@ use std::{
use async_trait::async_trait;
use collections::HashMap;
+use fs::Fs;
use gpui::{AsyncApp, SharedString};
use settings::WorktreeId;
+use task::ShellKind;
use crate::{LanguageName, ManifestName};
/// Represents a single toolchain.
-#[derive(Clone, Debug)]
+#[derive(Clone, Eq, Debug)]
pub struct Toolchain {
/// User-facing label
pub name: SharedString,
@@ -27,30 +29,104 @@ pub struct Toolchain {
pub as_json: serde_json::Value,
}
+/// Declares a scope of a toolchain added by user.
+///
+/// When the user adds a toolchain, we give them an option to see that toolchain in:
+/// - All of their projects
+/// - A project they're currently in.
+/// - Only in the subproject they're currently in.
+#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
+pub enum ToolchainScope {
+ Subproject(WorktreeId, Arc<Path>),
+ Project,
+ /// Available in all projects on this box. It wouldn't make sense to show suggestions across machines.
+ Global,
+}
+
+impl ToolchainScope {
+ pub fn label(&self) -> &'static str {
+ match self {
+ ToolchainScope::Subproject(_, _) => "Subproject",
+ ToolchainScope::Project => "Project",
+ ToolchainScope::Global => "Global",
+ }
+ }
+
+ pub fn description(&self) -> &'static str {
+ match self {
+ ToolchainScope::Subproject(_, _) => {
+ "Available only in the subproject you're currently in."
+ }
+ ToolchainScope::Project => "Available in all locations in your current project.",
+ ToolchainScope::Global => "Available in all of your projects on this machine.",
+ }
+ }
+}
+
+impl std::hash::Hash for Toolchain {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ let Self {
+ name,
+ path,
+ language_name,
+ as_json: _,
+ } = self;
+ name.hash(state);
+ path.hash(state);
+ language_name.hash(state);
+ }
+}
+
impl PartialEq for Toolchain {
fn eq(&self, other: &Self) -> bool {
+ let Self {
+ name,
+ path,
+ language_name,
+ as_json: _,
+ } = self;
// Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced.
// Thus, there could be multiple entries that look the same in the UI.
- (&self.name, &self.path, &self.language_name).eq(&(
- &other.name,
- &other.path,
- &other.language_name,
- ))
+ (name, path, language_name).eq(&(&other.name, &other.path, &other.language_name))
}
}
#[async_trait]
-pub trait ToolchainLister: Send + Sync {
+pub trait ToolchainLister: Send + Sync + 'static {
+ /// List all available toolchains for a given path.
async fn list(
&self,
worktree_root: PathBuf,
- subroot_relative_path: Option<Arc<Path>>,
+ subroot_relative_path: Arc<Path>,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList;
- // Returns a term which we should use in UI to refer to a toolchain.
- fn term(&self) -> SharedString;
- /// Returns the name of the manifest file for this toolchain.
- fn manifest_name(&self) -> ManifestName;
+
+ /// Given a user-created toolchain, resolve lister-specific details.
+ /// Put another way: fill in the details of the toolchain so the user does not have to.
+ async fn resolve(
+ &self,
+ path: PathBuf,
+ project_env: Option<HashMap<String, String>>,
+ ) -> anyhow::Result<Toolchain>;
+
+ async fn activation_script(
+ &self,
+ toolchain: &Toolchain,
+ shell: ShellKind,
+ fs: &dyn Fs,
+ ) -> Vec<String>;
+ /// Returns various "static" bits of information about this toolchain lister. This function should be pure.
+ fn meta(&self) -> ToolchainMetadata;
+}
+
+#[derive(Clone, PartialEq, Eq, Hash)]
+pub struct ToolchainMetadata {
+ /// Returns a term which we should use in UI to refer to toolchains produced by a given `[ToolchainLister]`.
+ pub term: SharedString,
+ /// A user-facing placeholder describing the semantic meaning of a path to a new toolchain.
+ pub new_toolchain_placeholder: SharedString,
+ /// The name of the manifest file for this toolchain.
+ pub manifest_name: ManifestName,
}
#[async_trait(?Send)]
@@ -64,8 +140,31 @@ pub trait LanguageToolchainStore: Send + Sync + 'static {
) -> Option<Toolchain>;
}
+pub trait LocalLanguageToolchainStore: Send + Sync + 'static {
+ fn active_toolchain(
+ self: Arc<Self>,
+ worktree_id: WorktreeId,
+ relative_path: &Arc<Path>,
+ language_name: LanguageName,
+ cx: &mut AsyncApp,
+ ) -> Option<Toolchain>;
+}
+
+#[async_trait(?Send)]
+impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
+ async fn active_toolchain(
+ self: Arc<Self>,
+ worktree_id: WorktreeId,
+ relative_path: Arc<Path>,
+ language_name: LanguageName,
+ cx: &mut AsyncApp,
+ ) -> Option<Toolchain> {
+ self.active_toolchain(worktree_id, &relative_path, language_name, cx)
+ }
+}
+
type DefaultIndex = usize;
-#[derive(Default, Clone)]
+#[derive(Default, Clone, Debug)]
pub struct ToolchainList {
pub toolchains: Vec<Toolchain>,
pub default: Option<DefaultIndex>,
@@ -12,8 +12,8 @@ use fs::Fs;
use futures::{Future, FutureExt, future::join_all};
use gpui::{App, AppContext, AsyncApp, Task};
use language::{
- BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore,
- LspAdapter, LspAdapterDelegate,
+ BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LspAdapter, LspAdapterDelegate,
+ Toolchain,
};
use lsp::{
CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName,
@@ -159,7 +159,7 @@ impl LspAdapter for ExtensionLspAdapter {
fn get_language_server_command<'a>(
self: Arc<Self>,
delegate: Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: LanguageServerBinaryOptions,
_: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
_: &'a mut AsyncApp,
@@ -204,6 +204,7 @@ impl LspAdapter for ExtensionLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Send + Any>> {
unreachable!("get_language_server_command is overridden")
}
@@ -288,7 +289,7 @@ impl LspAdapter for ExtensionLspAdapter {
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_cx: &mut AsyncApp,
) -> Result<Value> {
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
@@ -336,7 +337,7 @@ impl LspAdapter for ExtensionLspAdapter {
target_language_server_id: LanguageServerName,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+
_cx: &mut AsyncApp,
) -> Result<Option<serde_json::Value>> {
let delegate = Arc::new(WorktreeDelegateAdapter(delegate.clone())) as _;
@@ -397,6 +398,10 @@ impl LspAdapter for ExtensionLspAdapter {
Ok(labels_from_extension(labels, language))
}
+
+ fn is_extension(&self) -> bool {
+ true
+ }
}
fn labels_from_extension(
@@ -52,7 +52,7 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy {
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
) {
self.language_registry
- .register_language(language, grammar, matcher, hidden, load);
+ .register_language(language, grammar, matcher, hidden, None, load);
}
fn remove_languages(
@@ -61,6 +61,6 @@ impl ExtensionLanguageProxy for LanguageServerRegistryProxy {
grammars_to_remove: &[Arc<str>],
) {
self.language_registry
- .remove_languages(&languages_to_remove, &grammars_to_remove);
+ .remove_languages(languages_to_remove, grammars_to_remove);
}
}
@@ -17,6 +17,7 @@ test-support = []
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
+open_router.workspace = true
anyhow.workspace = true
base64.workspace = true
client.workspace = true
@@ -1,14 +1,19 @@
use crate::{
- AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
- LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
- LanguageModelToolChoice,
+ AuthenticateError, ConfigurationViewTargetAgent, LanguageModel, LanguageModelCompletionError,
+ LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
+ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
+ LanguageModelRequest, LanguageModelToolChoice,
};
-use futures::{FutureExt, StreamExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
+use anyhow::anyhow;
+use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream};
use gpui::{AnyView, App, AsyncApp, Entity, Task, Window};
use http_client::Result;
use parking_lot::Mutex;
-use std::sync::Arc;
+use smol::stream::StreamExt;
+use std::sync::{
+ Arc,
+ atomic::{AtomicBool, Ordering::SeqCst},
+};
#[derive(Clone)]
pub struct FakeLanguageModelProvider {
@@ -62,7 +67,12 @@ impl LanguageModelProvider for FakeLanguageModelProvider {
Task::ready(Ok(()))
}
- fn configuration_view(&self, _window: &mut Window, _: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: ConfigurationViewTargetAgent,
+ _window: &mut Window,
+ _: &mut App,
+ ) -> AnyView {
unimplemented!()
}
@@ -95,9 +105,12 @@ pub struct FakeLanguageModel {
current_completion_txs: Mutex<
Vec<(
LanguageModelRequest,
- mpsc::UnboundedSender<LanguageModelCompletionEvent>,
+ mpsc::UnboundedSender<
+ Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
+ >,
)>,
>,
+ forbid_requests: AtomicBool,
}
impl Default for FakeLanguageModel {
@@ -106,11 +119,20 @@ impl Default for FakeLanguageModel {
provider_id: LanguageModelProviderId::from("fake".to_string()),
provider_name: LanguageModelProviderName::from("Fake".to_string()),
current_completion_txs: Mutex::new(Vec::new()),
+ forbid_requests: AtomicBool::new(false),
}
}
}
impl FakeLanguageModel {
+ pub fn allow_requests(&self) {
+ self.forbid_requests.store(false, SeqCst);
+ }
+
+ pub fn forbid_requests(&self) {
+ self.forbid_requests.store(true, SeqCst);
+ }
+
pub fn pending_completions(&self) -> Vec<LanguageModelRequest> {
self.current_completion_txs
.lock()
@@ -145,7 +167,21 @@ impl FakeLanguageModel {
.find(|(req, _)| req == request)
.map(|(_, tx)| tx)
.unwrap();
- tx.unbounded_send(event.into()).unwrap();
+ tx.unbounded_send(Ok(event.into())).unwrap();
+ }
+
+ pub fn send_completion_stream_error(
+ &self,
+ request: &LanguageModelRequest,
+ error: impl Into<LanguageModelCompletionError>,
+ ) {
+ let current_completion_txs = self.current_completion_txs.lock();
+ let tx = current_completion_txs
+ .iter()
+ .find(|(req, _)| req == request)
+ .map(|(_, tx)| tx)
+ .unwrap();
+ tx.unbounded_send(Err(error.into())).unwrap();
}
pub fn end_completion_stream(&self, request: &LanguageModelRequest) {
@@ -165,6 +201,13 @@ impl FakeLanguageModel {
self.send_completion_stream_event(self.pending_completions().last().unwrap(), event);
}
+ pub fn send_last_completion_stream_error(
+ &self,
+ error: impl Into<LanguageModelCompletionError>,
+ ) {
+ self.send_completion_stream_error(self.pending_completions().last().unwrap(), error);
+ }
+
pub fn end_last_completion_stream(&self) {
self.end_completion_stream(self.pending_completions().last().unwrap());
}
@@ -222,9 +265,18 @@ impl LanguageModel for FakeLanguageModel {
LanguageModelCompletionError,
>,
> {
- let (tx, rx) = mpsc::unbounded();
- self.current_completion_txs.lock().push((request, tx));
- async move { Ok(rx.map(Ok).boxed()) }.boxed()
+ if self.forbid_requests.load(SeqCst) {
+ async move {
+ Err(LanguageModelCompletionError::Other(anyhow!(
+ "requests are forbidden"
+ )))
+ }
+ .boxed()
+ } else {
+ let (tx, rx) = mpsc::unbounded();
+ self.current_completion_txs.lock().push((request, tx));
+ async move { Ok(rx.boxed()) }.boxed()
+ }
}
fn as_fake(&self) -> &Self {
@@ -14,9 +14,10 @@ use client::Client;
use cloud_llm_client::{CompletionMode, CompletionRequestStatus};
use futures::FutureExt;
use futures::{StreamExt, future::BoxFuture, stream::BoxStream};
-use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window};
+use gpui::{AnyView, App, AsyncApp, SharedString, Task, Window};
use http_client::{StatusCode, http};
use icons::IconName;
+use open_router::OpenRouterError;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
@@ -54,7 +55,7 @@ pub const ZED_CLOUD_PROVIDER_NAME: LanguageModelProviderName =
pub fn init(client: Arc<Client>, cx: &mut App) {
init_settings(cx);
- RefreshLlmTokenListener::register(client.clone(), cx);
+ RefreshLlmTokenListener::register(client, cx);
}
pub fn init_settings(cx: &mut App) {
@@ -300,7 +301,7 @@ impl From<AnthropicError> for LanguageModelCompletionError {
},
AnthropicError::ServerOverloaded { retry_after } => Self::ServerOverloaded {
provider,
- retry_after: retry_after,
+ retry_after,
},
AnthropicError::ApiError(api_error) => api_error.into(),
}
@@ -347,6 +348,72 @@ impl From<anthropic::ApiError> for LanguageModelCompletionError {
}
}
+impl From<OpenRouterError> for LanguageModelCompletionError {
+ fn from(error: OpenRouterError) -> Self {
+ let provider = LanguageModelProviderName::new("OpenRouter");
+ match error {
+ OpenRouterError::SerializeRequest(error) => Self::SerializeRequest { provider, error },
+ OpenRouterError::BuildRequestBody(error) => Self::BuildRequestBody { provider, error },
+ OpenRouterError::HttpSend(error) => Self::HttpSend { provider, error },
+ OpenRouterError::DeserializeResponse(error) => {
+ Self::DeserializeResponse { provider, error }
+ }
+ OpenRouterError::ReadResponse(error) => Self::ApiReadResponseError { provider, error },
+ OpenRouterError::RateLimit { retry_after } => Self::RateLimitExceeded {
+ provider,
+ retry_after: Some(retry_after),
+ },
+ OpenRouterError::ServerOverloaded { retry_after } => Self::ServerOverloaded {
+ provider,
+ retry_after,
+ },
+ OpenRouterError::ApiError(api_error) => api_error.into(),
+ }
+ }
+}
+
+impl From<open_router::ApiError> for LanguageModelCompletionError {
+ fn from(error: open_router::ApiError) -> Self {
+ use open_router::ApiErrorCode::*;
+ let provider = LanguageModelProviderName::new("OpenRouter");
+ match error.code {
+ InvalidRequestError => Self::BadRequestFormat {
+ provider,
+ message: error.message,
+ },
+ AuthenticationError => Self::AuthenticationError {
+ provider,
+ message: error.message,
+ },
+ PaymentRequiredError => Self::AuthenticationError {
+ provider,
+ message: format!("Payment required: {}", error.message),
+ },
+ PermissionError => Self::PermissionError {
+ provider,
+ message: error.message,
+ },
+ RequestTimedOut => Self::HttpResponseError {
+ provider,
+ status_code: StatusCode::REQUEST_TIMEOUT,
+ message: error.message,
+ },
+ RateLimitError => Self::RateLimitExceeded {
+ provider,
+ retry_after: None,
+ },
+ ApiError => Self::ApiInternalServerError {
+ provider,
+ message: error.message,
+ },
+ OverloadedError => Self::ServerOverloaded {
+ provider,
+ retry_after: None,
+ },
+ }
+ }
+}
+
/// Indicates the format used to define the input schema for a language model tool.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
pub enum LanguageModelToolSchemaFormat {
@@ -538,7 +605,7 @@ pub trait LanguageModel: Send + Sync {
if let Some(first_event) = events.next().await {
match first_event {
Ok(LanguageModelCompletionEvent::StartMessage { message_id: id }) => {
- message_id = Some(id.clone());
+ message_id = Some(id);
}
Ok(LanguageModelCompletionEvent::Text(text)) => {
first_item_text = Some(text);
@@ -634,20 +701,22 @@ pub trait LanguageModelProvider: 'static {
}
fn is_authenticated(&self, cx: &App) -> bool;
fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>>;
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView;
- fn must_accept_terms(&self, _cx: &App) -> bool {
- false
- }
- fn render_accept_terms(
+ fn configuration_view(
&self,
- _view: LanguageModelProviderTosView,
- _cx: &mut App,
- ) -> Option<AnyElement> {
- None
- }
+ target_agent: ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView;
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>>;
}
+#[derive(Default, Clone)]
+pub enum ConfigurationViewTargetAgent {
+ #[default]
+ ZedAgent,
+ Other(SharedString),
+}
+
#[derive(PartialEq, Eq)]
pub enum LanguageModelProviderTosView {
/// When there are some past interactions in the Agent Panel.
@@ -4,7 +4,7 @@ use std::sync::Arc;
use anyhow::Result;
use client::Client;
use cloud_api_types::websocket_protocol::MessageToClient;
-use cloud_llm_client::Plan;
+use cloud_llm_client::{Plan, PlanV1};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, ReadGlobal as _};
use smol::lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard};
use thiserror::Error;
@@ -29,19 +29,34 @@ pub struct ModelRequestLimitReachedError {
impl fmt::Display for ModelRequestLimitReachedError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let message = match self.plan {
- Plan::ZedFree => "Model request limit reached. Upgrade to Zed Pro for more requests.",
- Plan::ZedPro => {
+ Plan::V1(PlanV1::ZedFree) => {
+ "Model request limit reached. Upgrade to Zed Pro for more requests."
+ }
+ Plan::V1(PlanV1::ZedPro) => {
"Model request limit reached. Upgrade to usage-based billing for more requests."
}
- Plan::ZedProTrial => {
+ Plan::V1(PlanV1::ZedProTrial) => {
"Model request limit reached. Upgrade to Zed Pro for more requests."
}
+ Plan::V2(_) => "Model request limit reached.",
};
write!(f, "{message}")
}
}
+#[derive(Error, Debug)]
+pub struct ToolUseLimitReachedError;
+
+impl fmt::Display for ToolUseLimitReachedError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "Consecutive tool use limit reached. Enable Burn Mode for unlimited tool use."
+ )
+ }
+}
+
#[derive(Clone, Default)]
pub struct LlmApiToken(Arc<RwLock<Option<String>>>);
@@ -70,7 +85,7 @@ impl LlmApiToken {
let response = client.cloud_client().create_llm_token(system_id).await?;
*lock = Some(response.token.0.clone());
- Ok(response.token.0.clone())
+ Ok(response.token.0)
}
}
@@ -21,13 +21,10 @@ impl Global for GlobalLanguageModelRegistry {}
pub enum ConfigurationError {
#[error("Configure at least one LLM provider to start using the panel.")]
NoProvider,
- #[error("LLM Provider is not configured or does not support the configured model.")]
+ #[error("LLM provider is not configured or does not support the configured model.")]
ModelNotFound,
#[error("{} LLM provider is not configured.", .0.name().0)]
ProviderNotAuthenticated(Arc<dyn LanguageModelProvider>),
- #[error("Using the {} LLM provider requires accepting the Terms of Service.",
- .0.name().0)]
- ProviderPendingTermsAcceptance(Arc<dyn LanguageModelProvider>),
}
impl std::fmt::Debug for ConfigurationError {
@@ -38,9 +35,6 @@ impl std::fmt::Debug for ConfigurationError {
Self::ProviderNotAuthenticated(provider) => {
write!(f, "ProviderNotAuthenticated({})", provider.id())
}
- Self::ProviderPendingTermsAcceptance(provider) => {
- write!(f, "ProviderPendingTermsAcceptance({})", provider.id())
- }
}
}
}
@@ -107,7 +101,7 @@ pub enum Event {
InlineAssistantModelChanged,
CommitMessageModelChanged,
ThreadSummaryModelChanged,
- ProviderStateChanged,
+ ProviderStateChanged(LanguageModelProviderId),
AddedProvider(LanguageModelProviderId),
RemovedProvider(LanguageModelProviderId),
}
@@ -148,8 +142,11 @@ impl LanguageModelRegistry {
) {
let id = provider.id();
- let subscription = provider.subscribe(cx, |_, cx| {
- cx.emit(Event::ProviderStateChanged);
+ let subscription = provider.subscribe(cx, {
+ let id = id.clone();
+ move |_, cx| {
+ cx.emit(Event::ProviderStateChanged(id.clone()));
+ }
});
if let Some(subscription) = subscription {
subscription.detach();
@@ -197,12 +194,6 @@ impl LanguageModelRegistry {
return Some(ConfigurationError::ProviderNotAuthenticated(model.provider));
}
- if model.provider.must_accept_terms(cx) {
- return Some(ConfigurationError::ProviderPendingTermsAcceptance(
- model.provider,
- ));
- }
-
None
}
@@ -217,6 +208,7 @@ impl LanguageModelRegistry {
) -> impl Iterator<Item = Arc<dyn LanguageModel>> + 'a {
self.providers
.values()
+ .filter(|provider| provider.is_authenticated(cx))
.flat_map(|provider| provider.provided_models(cx))
}
@@ -220,42 +220,39 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent {
// Accept wrapped text format: { "type": "text", "text": "..." }
if let (Some(type_value), Some(text_value)) =
- (get_field(&obj, "type"), get_field(&obj, "text"))
+ (get_field(obj, "type"), get_field(obj, "text"))
+ && let Some(type_str) = type_value.as_str()
+ && type_str.to_lowercase() == "text"
+ && let Some(text) = text_value.as_str()
{
- if let Some(type_str) = type_value.as_str() {
- if type_str.to_lowercase() == "text" {
- if let Some(text) = text_value.as_str() {
- return Ok(Self::Text(Arc::from(text)));
- }
- }
- }
+ return Ok(Self::Text(Arc::from(text)));
}
// Check for wrapped Text variant: { "text": "..." }
- if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "text") {
- if obj.len() == 1 {
- // Only one field, and it's "text" (case-insensitive)
- if let Some(text) = value.as_str() {
- return Ok(Self::Text(Arc::from(text)));
- }
+ if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "text")
+ && obj.len() == 1
+ {
+ // Only one field, and it's "text" (case-insensitive)
+ if let Some(text) = value.as_str() {
+ return Ok(Self::Text(Arc::from(text)));
}
}
// Check for wrapped Image variant: { "image": { "source": "...", "size": ... } }
- if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "image") {
- if obj.len() == 1 {
- // Only one field, and it's "image" (case-insensitive)
- // Try to parse the nested image object
- if let Some(image_obj) = value.as_object() {
- if let Some(image) = LanguageModelImage::from_json(image_obj) {
- return Ok(Self::Image(image));
- }
- }
+ if let Some((_key, value)) = obj.iter().find(|(k, _)| k.to_lowercase() == "image")
+ && obj.len() == 1
+ {
+ // Only one field, and it's "image" (case-insensitive)
+ // Try to parse the nested image object
+ if let Some(image_obj) = value.as_object()
+ && let Some(image) = LanguageModelImage::from_json(image_obj)
+ {
+ return Ok(Self::Image(image));
}
}
// Try as direct Image (object with "source" and "size" fields)
- if let Some(image) = LanguageModelImage::from_json(&obj) {
+ if let Some(image) = LanguageModelImage::from_json(obj) {
return Ok(Self::Image(image));
}
}
@@ -272,7 +269,7 @@ impl<'de> Deserialize<'de> for LanguageModelToolResultContent {
impl LanguageModelToolResultContent {
pub fn to_str(&self) -> Option<&str> {
match self {
- Self::Text(text) => Some(&text),
+ Self::Text(text) => Some(text),
Self::Image(_) => None,
}
}
@@ -19,7 +19,7 @@ impl Role {
}
}
- pub fn to_proto(&self) -> proto::LanguageModelRole {
+ pub fn to_proto(self) -> proto::LanguageModelRole {
match self {
Role::User => proto::LanguageModelRole::LanguageModelUser,
Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant,
@@ -104,7 +104,7 @@ fn register_language_model_providers(
cx: &mut Context<LanguageModelRegistry>,
) {
registry.register_provider(
- CloudLanguageModelProvider::new(user_store.clone(), client.clone(), cx),
+ CloudLanguageModelProvider::new(user_store, client.clone(), cx),
cx,
);
@@ -15,11 +15,11 @@ use gpui::{
};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, LanguageModel, LanguageModelCacheConfiguration,
- LanguageModelCompletionError, LanguageModelId, LanguageModelName, LanguageModelProvider,
- LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
- LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent, MessageContent,
- RateLimiter, Role,
+ AuthenticateError, ConfigurationViewTargetAgent, LanguageModel,
+ LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelId,
+ LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
+ LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
+ LanguageModelToolResultContent, MessageContent, RateLimiter, Role,
};
use language_model::{LanguageModelCompletionEvent, LanguageModelToolUse, StopReason};
use schemars::JsonSchema;
@@ -114,7 +114,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .delete_credentials(&api_url, &cx)
+ .delete_credentials(&api_url, cx)
.await
.ok();
this.update(cx, |this, cx| {
@@ -133,7 +133,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+ .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
.await
.ok();
@@ -153,29 +153,14 @@ impl State {
return Task::ready(Ok(()));
}
- let credentials_provider = <dyn CredentialsProvider>::global(cx);
- let api_url = AllLanguageModelSettings::get_global(cx)
- .anthropic
- .api_url
- .clone();
+ let key = AnthropicLanguageModelProvider::api_key(cx);
cx.spawn(async move |this, cx| {
- let (api_key, from_env) = if let Ok(api_key) = std::env::var(ANTHROPIC_API_KEY_VAR) {
- (api_key, true)
- } else {
- let (_, api_key) = credentials_provider
- .read_credentials(&api_url, &cx)
- .await?
- .ok_or(AuthenticateError::CredentialsNotFound)?;
- (
- String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?,
- false,
- )
- };
+ let key = key.await?;
this.update(cx, |this, cx| {
- this.api_key = Some(api_key);
- this.api_key_from_env = from_env;
+ this.api_key = Some(key.key);
+ this.api_key_from_env = key.from_env;
cx.notify();
})?;
@@ -184,6 +169,11 @@ impl State {
}
}
+pub struct ApiKey {
+ pub key: String,
+ pub from_env: bool,
+}
+
impl AnthropicLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
let state = cx.new(|cx| State {
@@ -206,6 +196,33 @@ impl AnthropicLanguageModelProvider {
request_limiter: RateLimiter::new(4),
})
}
+
+ pub fn api_key(cx: &mut App) -> Task<Result<ApiKey, AuthenticateError>> {
+ let credentials_provider = <dyn CredentialsProvider>::global(cx);
+ let api_url = AllLanguageModelSettings::get_global(cx)
+ .anthropic
+ .api_url
+ .clone();
+
+ if let Ok(key) = std::env::var(ANTHROPIC_API_KEY_VAR) {
+ Task::ready(Ok(ApiKey {
+ key,
+ from_env: true,
+ }))
+ } else {
+ cx.spawn(async move |cx| {
+ let (_, api_key) = credentials_provider
+ .read_credentials(&api_url, cx)
+ .await?
+ .ok_or(AuthenticateError::CredentialsNotFound)?;
+
+ Ok(ApiKey {
+ key: String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?,
+ from_env: false,
+ })
+ })
+ }
+ }
}
impl LanguageModelProviderState for AnthropicLanguageModelProvider {
@@ -299,8 +316,13 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
- cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
+ fn configuration_view(
+ &self,
+ target_agent: ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
+ cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx))
.into()
}
@@ -402,14 +424,21 @@ impl AnthropicModel {
return futures::future::ready(Err(anyhow!("App state dropped").into())).boxed();
};
+ let beta_headers = self.model.beta_headers();
+
async move {
let Some(api_key) = api_key else {
return Err(LanguageModelCompletionError::NoApiKey {
provider: PROVIDER_NAME,
});
};
- let request =
- anthropic::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
+ let request = anthropic::stream_completion(
+ http_client.as_ref(),
+ &api_url,
+ &api_key,
+ request,
+ beta_headers,
+ );
request.await.map_err(Into::into)
}
.boxed()
@@ -532,7 +561,7 @@ pub fn into_anthropic(
.into_iter()
.filter_map(|content| match content {
MessageContent::Text(text) => {
- let text = if text.chars().last().map_or(false, |c| c.is_whitespace()) {
+ let text = if text.chars().last().is_some_and(|c| c.is_whitespace()) {
text.trim_end().to_string()
} else {
text
@@ -611,11 +640,11 @@ pub fn into_anthropic(
Role::Assistant => anthropic::Role::Assistant,
Role::System => unreachable!("System role should never occur here"),
};
- if let Some(last_message) = new_messages.last_mut() {
- if last_message.role == anthropic_role {
- last_message.content.extend(anthropic_message_content);
- continue;
- }
+ if let Some(last_message) = new_messages.last_mut()
+ && last_message.role == anthropic_role
+ {
+ last_message.content.extend(anthropic_message_content);
+ continue;
}
// Mark the last segment of the message as cached
@@ -791,7 +820,7 @@ impl AnthropicEventMapper {
))];
}
}
- return vec![];
+ vec![]
}
},
Event::ContentBlockStop { index } => {
@@ -902,12 +931,18 @@ struct ConfigurationView {
api_key_editor: Entity<Editor>,
state: gpui::Entity<State>,
load_credentials_task: Option<Task<()>>,
+ target_agent: ConfigurationViewTargetAgent,
}
impl ConfigurationView {
const PLACEHOLDER_TEXT: &'static str = "sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
- fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+ fn new(
+ state: gpui::Entity<State>,
+ target_agent: ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
cx.observe(&state, |_, _, cx| {
cx.notify();
})
@@ -934,11 +969,12 @@ impl ConfigurationView {
Self {
api_key_editor: cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text(Self::PLACEHOLDER_TEXT, cx);
+ editor.set_placeholder_text(Self::PLACEHOLDER_TEXT, window, cx);
editor
}),
state,
load_credentials_task,
+ target_agent,
}
}
@@ -1012,7 +1048,10 @@ impl Render for ConfigurationView {
v_flex()
.size_full()
.on_action(cx.listener(Self::save_api_key))
- .child(Label::new("To use Zed's agent with Anthropic, you need to add an API key. Follow these steps:"))
+ .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent {
+ ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic".into(),
+ ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
+ })))
.child(
List::new()
.child(
@@ -1023,7 +1062,7 @@ impl Render for ConfigurationView {
)
)
.child(
- InstructionListItem::text_only("Paste your API key below and hit enter to start using the assistant")
+ InstructionListItem::text_only("Paste your API key below and hit enter to start using the agent")
)
)
.child(
@@ -150,7 +150,7 @@ impl State {
let credentials_provider = <dyn CredentialsProvider>::global(cx);
cx.spawn(async move |this, cx| {
credentials_provider
- .delete_credentials(AMAZON_AWS_URL, &cx)
+ .delete_credentials(AMAZON_AWS_URL, cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -174,7 +174,7 @@ impl State {
AMAZON_AWS_URL,
"Bearer",
&serde_json::to_vec(&credentials)?,
- &cx,
+ cx,
)
.await?;
this.update(cx, |this, cx| {
@@ -206,7 +206,7 @@ impl State {
(credentials, true)
} else {
let (_, credentials) = credentials_provider
- .read_credentials(AMAZON_AWS_URL, &cx)
+ .read_credentials(AMAZON_AWS_URL, cx)
.await?
.ok_or_else(|| AuthenticateError::CredentialsNotFound)?;
(
@@ -348,7 +348,12 @@ impl LanguageModelProvider for BedrockLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
@@ -407,10 +412,10 @@ impl BedrockModel {
.region(Region::new(region))
.timeout_config(TimeoutConfig::disabled());
- if let Some(endpoint_url) = endpoint {
- if !endpoint_url.is_empty() {
- config_builder = config_builder.endpoint_url(endpoint_url);
- }
+ if let Some(endpoint_url) = endpoint
+ && !endpoint_url.is_empty()
+ {
+ config_builder = config_builder.endpoint_url(endpoint_url);
}
match auth_method {
@@ -460,7 +465,7 @@ impl BedrockModel {
Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>>,
> {
let Ok(runtime_client) = self
- .get_or_init_client(&cx)
+ .get_or_init_client(cx)
.cloned()
.context("Bedrock client not initialized")
else {
@@ -723,11 +728,11 @@ pub fn into_bedrock(
Role::Assistant => bedrock::BedrockRole::Assistant,
Role::System => unreachable!("System role should never occur here"),
};
- if let Some(last_message) = new_messages.last_mut() {
- if last_message.role == bedrock_role {
- last_message.content.extend(bedrock_message_content);
- continue;
- }
+ if let Some(last_message) = new_messages.last_mut()
+ && last_message.role == bedrock_role
+ {
+ last_message.content.extend(bedrock_message_content);
+ continue;
}
new_messages.push(
BedrockMessage::builder()
@@ -912,7 +917,7 @@ pub fn map_to_language_model_completion_events(
Some(ContentBlockDelta::ReasoningContent(thinking)) => match thinking {
ReasoningContentBlockDelta::Text(thoughts) => {
Some(Ok(LanguageModelCompletionEvent::Thinking {
- text: thoughts.clone(),
+ text: thoughts,
signature: None,
}))
}
@@ -963,7 +968,7 @@ pub fn map_to_language_model_completion_events(
id: tool_use.id.into(),
name: tool_use.name.into(),
is_input_complete: true,
- raw_input: tool_use.input_json.clone(),
+ raw_input: tool_use.input_json,
input,
},
))
@@ -1048,22 +1053,22 @@ impl ConfigurationView {
Self {
access_key_id_editor: cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text(Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT, cx);
+ editor.set_placeholder_text(Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT, window, cx);
editor
}),
secret_access_key_editor: cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text(Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT, cx);
+ editor.set_placeholder_text(Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT, window, cx);
editor
}),
session_token_editor: cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text(Self::PLACEHOLDER_SESSION_TOKEN_TEXT, cx);
+ editor.set_placeholder_text(Self::PLACEHOLDER_SESSION_TOKEN_TEXT, window, cx);
editor
}),
region_editor: cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text(Self::PLACEHOLDER_REGION, cx);
+ editor.set_placeholder_text(Self::PLACEHOLDER_REGION, window, cx);
editor
}),
state,
@@ -1081,21 +1086,18 @@ impl ConfigurationView {
.access_key_id_editor
.read(cx)
.text(cx)
- .to_string()
.trim()
.to_string();
let secret_access_key = self
.secret_access_key_editor
.read(cx)
.text(cx)
- .to_string()
.trim()
.to_string();
let session_token = self
.session_token_editor
.read(cx)
.text(cx)
- .to_string()
.trim()
.to_string();
let session_token = if session_token.is_empty() {
@@ -1103,13 +1105,7 @@ impl ConfigurationView {
} else {
Some(session_token)
};
- let region = self
- .region_editor
- .read(cx)
- .text(cx)
- .to_string()
- .trim()
- .to_string();
+ let region = self.region_editor.read(cx).text(cx).trim().to_string();
let region = if region.is_empty() {
"us-east-1".to_string()
} else {
@@ -7,8 +7,9 @@ use cloud_llm_client::{
CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody,
CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse,
EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE, Plan,
- SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
- TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME,
+ PlanV1, PlanV2, SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME,
+ SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME, TOOL_USE_LIMIT_REACHED_HEADER_NAME,
+ ZED_VERSION_HEADER_NAME,
};
use futures::{
AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream,
@@ -23,9 +24,9 @@ use language_model::{
AuthenticateError, LanguageModel, LanguageModelCacheConfiguration,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
- LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest,
- LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken,
- ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener,
+ LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
+ LanguageModelToolSchemaFormat, LlmApiToken, ModelRequestLimitReachedError,
+ PaymentRequiredError, RateLimiter, RefreshLlmTokenListener,
};
use release_channel::AppVersion;
use schemars::JsonSchema;
@@ -118,7 +119,6 @@ pub struct State {
llm_api_token: LlmApiToken,
user_store: Entity<UserStore>,
status: client::Status,
- accept_terms_of_service_task: Option<Task<Result<()>>>,
models: Vec<Arc<cloud_llm_client::LanguageModel>>,
default_model: Option<Arc<cloud_llm_client::LanguageModel>>,
default_fast_model: Option<Arc<cloud_llm_client::LanguageModel>>,
@@ -140,9 +140,8 @@ impl State {
Self {
client: client.clone(),
llm_api_token: LlmApiToken::default(),
- user_store: user_store.clone(),
+ user_store,
status,
- accept_terms_of_service_task: None,
models: Vec::new(),
default_model: None,
default_fast_model: None,
@@ -193,28 +192,10 @@ impl State {
fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(async move |state, cx| {
- client.sign_in_with_optional_connect(true, &cx).await?;
+ client.sign_in_with_optional_connect(true, cx).await?;
state.update(cx, |_, cx| cx.notify())
})
}
-
- fn has_accepted_terms_of_service(&self, cx: &App) -> bool {
- self.user_store.read(cx).has_accepted_terms_of_service()
- }
-
- fn accept_terms_of_service(&mut self, cx: &mut Context<Self>) {
- let user_store = self.user_store.clone();
- self.accept_terms_of_service_task = Some(cx.spawn(async move |this, cx| {
- let _ = user_store
- .update(cx, |store, cx| store.accept_terms_of_service(cx))?
- .await;
- this.update(cx, |this, cx| {
- this.accept_terms_of_service_task = None;
- cx.notify()
- })
- }));
- }
-
fn update_models(&mut self, response: ListModelsResponse, cx: &mut Context<Self>) {
let mut models = Vec::new();
@@ -270,7 +251,7 @@ impl State {
if response.status().is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
- return Ok(serde_json::from_str(&body)?);
+ Ok(serde_json::from_str(&body)?)
} else {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
@@ -307,7 +288,7 @@ impl CloudLanguageModelProvider {
Self {
client,
- state: state.clone(),
+ state,
_maintain_client_status: maintain_client_status,
}
}
@@ -320,7 +301,7 @@ impl CloudLanguageModelProvider {
Arc::new(CloudLanguageModel {
id: LanguageModelId(SharedString::from(model.id.0.clone())),
model,
- llm_api_token: llm_api_token.clone(),
+ llm_api_token,
client: self.client.clone(),
request_limiter: RateLimiter::new(4),
})
@@ -384,40 +365,21 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
fn is_authenticated(&self, cx: &App) -> bool {
let state = self.state.read(cx);
- !state.is_signed_out(cx) && state.has_accepted_terms_of_service(cx)
+ !state.is_signed_out(cx)
}
fn authenticate(&self, _cx: &mut App) -> Task<Result<(), AuthenticateError>> {
Task::ready(Ok(()))
}
- fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView {
- cx.new(|_| ConfigurationView::new(self.state.clone()))
- .into()
- }
-
- fn must_accept_terms(&self, cx: &App) -> bool {
- !self.state.read(cx).has_accepted_terms_of_service(cx)
- }
-
- fn render_accept_terms(
+ fn configuration_view(
&self,
- view: LanguageModelProviderTosView,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ _: &mut Window,
cx: &mut App,
- ) -> Option<AnyElement> {
- let state = self.state.read(cx);
- if state.has_accepted_terms_of_service(cx) {
- return None;
- }
- Some(
- render_accept_terms(view, state.accept_terms_of_service_task.is_some(), {
- let state = self.state.clone();
- move |_window, cx| {
- state.update(cx, |state, cx| state.accept_terms_of_service(cx));
- }
- })
- .into_any_element(),
- )
+ ) -> AnyView {
+ cx.new(|_| ConfigurationView::new(self.state.clone()))
+ .into()
}
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
@@ -425,83 +387,6 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
}
}
-fn render_accept_terms(
- view_kind: LanguageModelProviderTosView,
- accept_terms_of_service_in_progress: bool,
- accept_terms_callback: impl Fn(&mut Window, &mut App) + 'static,
-) -> impl IntoElement {
- let thread_fresh_start = matches!(view_kind, LanguageModelProviderTosView::ThreadFreshStart);
- let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadEmptyState);
-
- let terms_button = Button::new("terms_of_service", "Terms of Service")
- .style(ButtonStyle::Subtle)
- .icon(IconName::ArrowUpRight)
- .icon_color(Color::Muted)
- .icon_size(IconSize::Small)
- .when(thread_empty_state, |this| this.label_size(LabelSize::Small))
- .on_click(move |_, _window, cx| cx.open_url("https://zed.dev/terms-of-service"));
-
- let button_container = h_flex().child(
- Button::new("accept_terms", "I accept the Terms of Service")
- .when(!thread_empty_state, |this| {
- this.full_width()
- .style(ButtonStyle::Tinted(TintColor::Accent))
- .icon(IconName::Check)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::Small)
- })
- .when(thread_empty_state, |this| {
- this.style(ButtonStyle::Tinted(TintColor::Warning))
- .label_size(LabelSize::Small)
- })
- .disabled(accept_terms_of_service_in_progress)
- .on_click(move |_, window, cx| (accept_terms_callback)(window, cx)),
- );
-
- if thread_empty_state {
- h_flex()
- .w_full()
- .flex_wrap()
- .justify_between()
- .child(
- h_flex()
- .child(
- Label::new("To start using Zed AI, please read and accept the")
- .size(LabelSize::Small),
- )
- .child(terms_button),
- )
- .child(button_container)
- } else {
- v_flex()
- .w_full()
- .gap_2()
- .child(
- h_flex()
- .flex_wrap()
- .when(thread_fresh_start, |this| this.justify_center())
- .child(Label::new(
- "To start using Zed AI, please read and accept the",
- ))
- .child(terms_button),
- )
- .child({
- match view_kind {
- LanguageModelProviderTosView::TextThreadPopup => {
- button_container.w_full().justify_end()
- }
- LanguageModelProviderTosView::Configuration => {
- button_container.w_full().justify_start()
- }
- LanguageModelProviderTosView::ThreadFreshStart => {
- button_container.w_full().justify_center()
- }
- LanguageModelProviderTosView::ThreadEmptyState => div().w_0(),
- }
- })
- }
-}
-
pub struct CloudLanguageModel {
id: LanguageModelId,
model: Arc<cloud_llm_client::LanguageModel>,
@@ -592,15 +477,14 @@ impl CloudLanguageModel {
.headers()
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
.and_then(|resource| resource.to_str().ok())
- {
- if let Some(plan) = response
+ && let Some(plan) = response
.headers()
.get(CURRENT_PLAN_HEADER_NAME)
.and_then(|plan| plan.to_str().ok())
- .and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok())
- {
- return Err(anyhow!(ModelRequestLimitReachedError { plan }));
- }
+ .and_then(|plan| cloud_llm_client::PlanV1::from_str(plan).ok())
+ .map(Plan::V1)
+ {
+ return Err(anyhow!(ModelRequestLimitReachedError { plan }));
}
} else if status == StatusCode::PAYMENT_REQUIRED {
return Err(anyhow!(PaymentRequiredError));
@@ -657,29 +541,29 @@ where
impl From<ApiError> for LanguageModelCompletionError {
fn from(error: ApiError) -> Self {
- if let Ok(cloud_error) = serde_json::from_str::<CloudApiError>(&error.body) {
- if cloud_error.code.starts_with("upstream_http_") {
- let status = if let Some(status) = cloud_error.upstream_status {
- status
- } else if cloud_error.code.ends_with("_error") {
- error.status
- } else {
- // If there's a status code in the code string (e.g. "upstream_http_429")
- // then use that; otherwise, see if the JSON contains a status code.
- cloud_error
- .code
- .strip_prefix("upstream_http_")
- .and_then(|code_str| code_str.parse::<u16>().ok())
- .and_then(|code| StatusCode::from_u16(code).ok())
- .unwrap_or(error.status)
- };
+ if let Ok(cloud_error) = serde_json::from_str::<CloudApiError>(&error.body)
+ && cloud_error.code.starts_with("upstream_http_")
+ {
+ let status = if let Some(status) = cloud_error.upstream_status {
+ status
+ } else if cloud_error.code.ends_with("_error") {
+ error.status
+ } else {
+ // If there's a status code in the code string (e.g. "upstream_http_429")
+ // then use that; otherwise, see if the JSON contains a status code.
+ cloud_error
+ .code
+ .strip_prefix("upstream_http_")
+ .and_then(|code_str| code_str.parse::<u16>().ok())
+ .and_then(|code| StatusCode::from_u16(code).ok())
+ .unwrap_or(error.status)
+ };
- return LanguageModelCompletionError::UpstreamProviderError {
- message: cloud_error.message,
- status,
- retry_after: cloud_error.retry_after.map(Duration::from_secs_f64),
- };
- }
+ return LanguageModelCompletionError::UpstreamProviderError {
+ message: cloud_error.message,
+ status,
+ retry_after: cloud_error.retry_after.map(Duration::from_secs_f64),
+ };
}
let retry_after = None;
@@ -941,6 +825,7 @@ impl LanguageModel for CloudLanguageModel {
request,
model.id(),
model.supports_parallel_tool_calls(),
+ model.supports_prompt_cache_key(),
None,
None,
);
@@ -1103,10 +988,7 @@ struct ZedAiConfiguration {
plan: Option<Plan>,
subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
eligible_for_trial: bool,
- has_accepted_terms_of_service: bool,
account_too_young: bool,
- accept_terms_of_service_in_progress: bool,
- accept_terms_of_service_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
}
@@ -1114,15 +996,17 @@ impl RenderOnce for ZedAiConfiguration {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let young_account_banner = YoungAccountBanner;
- let is_pro = self.plan == Some(Plan::ZedPro);
+ let is_pro = self.plan.is_some_and(|plan| {
+ matches!(plan, Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro))
+ });
let subscription_text = match (self.plan, self.subscription_period) {
- (Some(Plan::ZedPro), Some(_)) => {
+ (Some(Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro)), Some(_)) => {
"You have access to Zed's hosted models through your Pro subscription."
}
- (Some(Plan::ZedProTrial), Some(_)) => {
+ (Some(Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial)), Some(_)) => {
"You have access to Zed's hosted models through your Pro trial."
}
- (Some(Plan::ZedFree), Some(_)) => {
+ (Some(Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree)), Some(_)) => {
"You have basic access to Zed's hosted models through the Free plan."
}
_ => {
@@ -1172,58 +1056,30 @@ impl RenderOnce for ZedAiConfiguration {
);
}
- v_flex()
- .gap_2()
- .w_full()
- .when(!self.has_accepted_terms_of_service, |this| {
- this.child(render_accept_terms(
- LanguageModelProviderTosView::Configuration,
- self.accept_terms_of_service_in_progress,
- {
- let callback = self.accept_terms_of_service_callback.clone();
- move |window, cx| (callback)(window, cx)
- },
- ))
- })
- .map(|this| {
- if self.has_accepted_terms_of_service && self.account_too_young {
- this.child(young_account_banner).child(
- Button::new("upgrade", "Upgrade to Pro")
- .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
- .full_width()
- .on_click(|_, _, cx| {
- cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
- }),
- )
- } else if self.has_accepted_terms_of_service {
- this.text_sm()
- .child(subscription_text)
- .child(manage_subscription_buttons)
- } else {
- this
- }
- })
- .when(self.has_accepted_terms_of_service, |this| this)
+ v_flex().gap_2().w_full().map(|this| {
+ if self.account_too_young {
+ this.child(young_account_banner).child(
+ Button::new("upgrade", "Upgrade to Pro")
+ .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
+ .full_width()
+ .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))),
+ )
+ } else {
+ this.text_sm()
+ .child(subscription_text)
+ .child(manage_subscription_buttons)
+ }
+ })
}
}
struct ConfigurationView {
state: Entity<State>,
- accept_terms_of_service_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
}
impl ConfigurationView {
fn new(state: Entity<State>) -> Self {
- let accept_terms_of_service_callback = Arc::new({
- let state = state.clone();
- move |_window: &mut Window, cx: &mut App| {
- state.update(cx, |state, cx| {
- state.accept_terms_of_service(cx);
- });
- }
- });
-
let sign_in_callback = Arc::new({
let state = state.clone();
move |_window: &mut Window, cx: &mut App| {
@@ -1235,7 +1091,6 @@ impl ConfigurationView {
Self {
state,
- accept_terms_of_service_callback,
sign_in_callback,
}
}
@@ -1251,10 +1106,7 @@ impl Render for ConfigurationView {
plan: user_store.plan(),
subscription_period: user_store.subscription_period(),
eligible_for_trial: user_store.trial_started_at().is_none(),
- has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx),
account_too_young: user_store.account_too_young(),
- accept_terms_of_service_in_progress: state.accept_terms_of_service_task.is_some(),
- accept_terms_of_service_callback: self.accept_terms_of_service_callback.clone(),
sign_in_callback: self.sign_in_callback.clone(),
}
}
@@ -1279,7 +1131,6 @@ impl Component for ZedAiConfiguration {
plan: Option<Plan>,
eligible_for_trial: bool,
account_too_young: bool,
- has_accepted_terms_of_service: bool,
) -> AnyElement {
ZedAiConfiguration {
is_connected,
@@ -1288,10 +1139,7 @@ impl Component for ZedAiConfiguration {
.is_some()
.then(|| (Utc::now(), Utc::now() + chrono::Duration::days(7))),
eligible_for_trial,
- has_accepted_terms_of_service,
account_too_young,
- accept_terms_of_service_in_progress: false,
- accept_terms_of_service_callback: Arc::new(|_, _| {}),
sign_in_callback: Arc::new(|_, _| {}),
}
.into_any_element()
@@ -1302,33 +1150,30 @@ impl Component for ZedAiConfiguration {
.p_4()
.gap_4()
.children(vec![
- single_example(
- "Not connected",
- configuration(false, None, false, false, true),
- ),
+ single_example("Not connected", configuration(false, None, false, false)),
single_example(
"Accept Terms of Service",
- configuration(true, None, true, false, false),
+ configuration(true, None, true, false),
),
single_example(
"No Plan - Not eligible for trial",
- configuration(true, None, false, false, true),
+ configuration(true, None, false, false),
),
single_example(
"No Plan - Eligible for trial",
- configuration(true, None, true, false, true),
+ configuration(true, None, true, false),
),
single_example(
"Free Plan",
- configuration(true, Some(Plan::ZedFree), true, false, true),
+ configuration(true, Some(Plan::V1(PlanV1::ZedFree)), true, false),
),
single_example(
"Zed Pro Trial Plan",
- configuration(true, Some(Plan::ZedProTrial), true, false, true),
+ configuration(true, Some(Plan::V1(PlanV1::ZedProTrial)), true, false),
),
single_example(
"Zed Pro Plan",
- configuration(true, Some(Plan::ZedPro), true, false, true),
+ configuration(true, Some(Plan::V1(PlanV1::ZedPro)), true, false),
),
])
.into_any_element(),
@@ -14,10 +14,7 @@ use copilot::{Copilot, Status};
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use futures::{FutureExt, Stream, StreamExt};
-use gpui::{
- Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, Render, Subscription, Task,
- Transformation, percentage, svg,
-};
+use gpui::{Action, AnyView, App, AsyncApp, Entity, Render, Subscription, Task, svg};
use language::language_settings::all_language_settings;
use language_model::{
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
@@ -28,14 +25,9 @@ use language_model::{
StopReason, TokenUsage,
};
use settings::SettingsStore;
-use std::time::Duration;
-use ui::prelude::*;
+use ui::{CommonAnimationExt, prelude::*};
use util::debug_panic;
-use super::anthropic::count_anthropic_tokens;
-use super::google::count_google_tokens;
-use super::open_ai::count_open_ai_tokens;
-
const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("copilot_chat");
const PROVIDER_NAME: LanguageModelProviderName =
LanguageModelProviderName::new("GitHub Copilot Chat");
@@ -176,7 +168,12 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
Task::ready(Err(err.into()))
}
- fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ _: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
let state = self.state.clone();
cx.new(|cx| ConfigurationView::new(state, cx)).into()
}
@@ -188,6 +185,25 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
}
}
+fn collect_tiktoken_messages(
+ request: LanguageModelRequest,
+) -> Vec<tiktoken_rs::ChatCompletionRequestMessage> {
+ request
+ .messages
+ .into_iter()
+ .map(|message| tiktoken_rs::ChatCompletionRequestMessage {
+ role: match message.role {
+ Role::User => "user".into(),
+ Role::Assistant => "assistant".into(),
+ Role::System => "system".into(),
+ },
+ content: Some(message.string_contents()),
+ name: None,
+ function_call: None,
+ })
+ .collect::<Vec<_>>()
+}
+
pub struct CopilotChatLanguageModel {
model: CopilotChatModel,
request_limiter: RateLimiter,
@@ -223,7 +239,9 @@ impl LanguageModel for CopilotChatLanguageModel {
ModelVendor::OpenAI | ModelVendor::Anthropic => {
LanguageModelToolSchemaFormat::JsonSchema
}
- ModelVendor::Google => LanguageModelToolSchemaFormat::JsonSchemaSubset,
+ ModelVendor::Google | ModelVendor::XAI | ModelVendor::Unknown => {
+ LanguageModelToolSchemaFormat::JsonSchemaSubset
+ }
}
}
@@ -248,14 +266,20 @@ impl LanguageModel for CopilotChatLanguageModel {
request: LanguageModelRequest,
cx: &App,
) -> BoxFuture<'static, Result<u64>> {
- match self.model.vendor() {
- ModelVendor::Anthropic => count_anthropic_tokens(request, cx),
- ModelVendor::Google => count_google_tokens(request, cx),
- ModelVendor::OpenAI => {
- let model = open_ai::Model::from_id(self.model.id()).unwrap_or_default();
- count_open_ai_tokens(request, model, cx)
- }
- }
+ let model = self.model.clone();
+ cx.background_spawn(async move {
+ let messages = collect_tiktoken_messages(request);
+ // Copilot uses OpenAI tiktoken tokenizer for all it's model irrespective of the underlying provider(vendor).
+ let tokenizer_model = match model.tokenizer() {
+ Some("o200k_base") => "gpt-4o",
+ Some("cl100k_base") => "gpt-4",
+ _ => "gpt-4o",
+ };
+
+ tiktoken_rs::num_tokens_from_messages(tokenizer_model, &messages)
+ .map(|tokens| tokens as u64)
+ })
+ .boxed()
}
fn stream_completion(
@@ -470,7 +494,6 @@ fn into_copilot_chat(
}
}
- let mut tool_called = false;
let mut messages: Vec<ChatMessage> = Vec::new();
for message in request_messages {
match message.role {
@@ -540,7 +563,6 @@ fn into_copilot_chat(
let mut tool_calls = Vec::new();
for content in &message.content {
if let MessageContent::ToolUse(tool_use) = content {
- tool_called = true;
tool_calls.push(ToolCall {
id: tool_use.id.to_string(),
content: copilot::copilot_chat::ToolCallContent::Function {
@@ -585,7 +607,7 @@ fn into_copilot_chat(
}
}
- let mut tools = request
+ let tools = request
.tools
.iter()
.map(|tool| Tool::Function {
@@ -597,22 +619,6 @@ fn into_copilot_chat(
})
.collect::<Vec<_>>();
- // The API will return a Bad Request (with no error message) when tools
- // were used previously in the conversation but no tools are provided as
- // part of this request. Inserting a dummy tool seems to circumvent this
- // error.
- if tool_called && tools.is_empty() {
- tools.push(Tool::Function {
- function: copilot::copilot_chat::Function {
- name: "noop".to_string(),
- description: "No operation".to_string(),
- parameters: serde_json::json!({
- "type": "object"
- }),
- },
- });
- }
-
Ok(CopilotChatRequest {
intent: true,
n: 1,
@@ -677,11 +683,7 @@ impl Render for ConfigurationView {
}),
)
} else {
- let loading_icon = Icon::new(IconName::ArrowCircle).with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(4)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- );
+ let loading_icon = Icon::new(IconName::ArrowCircle).with_rotate_animation(4);
const ERROR_LABEL: &str = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different Assistant provider.";
@@ -77,7 +77,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .delete_credentials(&api_url, &cx)
+ .delete_credentials(&api_url, cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -96,7 +96,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+ .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
.await?;
this.update(cx, |this, cx| {
this.api_key = Some(api_key);
@@ -120,7 +120,7 @@ impl State {
(api_key, true)
} else {
let (_, api_key) = credentials_provider
- .read_credentials(&api_url, &cx)
+ .read_credentials(&api_url, cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(
@@ -229,7 +229,12 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
@@ -570,7 +575,7 @@ impl ConfigurationView {
fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("sk-00000000000000000000000000000000", cx);
+ editor.set_placeholder_text("sk-00000000000000000000000000000000", window, cx);
editor
});
@@ -12,9 +12,9 @@ use gpui::{
};
use http_client::HttpClient;
use language_model::{
- AuthenticateError, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelToolChoice, LanguageModelToolSchemaFormat, LanguageModelToolUse,
- LanguageModelToolUseId, MessageContent, StopReason,
+ AuthenticateError, ConfigurationViewTargetAgent, LanguageModelCompletionError,
+ LanguageModelCompletionEvent, LanguageModelToolChoice, LanguageModelToolSchemaFormat,
+ LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason,
};
use language_model::{
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
@@ -37,6 +37,8 @@ use util::ResultExt;
use crate::AllLanguageModelSettings;
use crate::ui::InstructionListItem;
+use super::anthropic::ApiKey;
+
const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID;
const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME;
@@ -110,7 +112,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .delete_credentials(&api_url, &cx)
+ .delete_credentials(&api_url, cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -129,7 +131,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+ .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
.await?;
this.update(cx, |this, cx| {
this.api_key = Some(api_key);
@@ -156,7 +158,7 @@ impl State {
(api_key, true)
} else {
let (_, api_key) = credentials_provider
- .read_credentials(&api_url, &cx)
+ .read_credentials(&api_url, cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(
@@ -198,6 +200,33 @@ impl GoogleLanguageModelProvider {
request_limiter: RateLimiter::new(4),
})
}
+
+ pub fn api_key(cx: &mut App) -> Task<Result<ApiKey>> {
+ let credentials_provider = <dyn CredentialsProvider>::global(cx);
+ let api_url = AllLanguageModelSettings::get_global(cx)
+ .google
+ .api_url
+ .clone();
+
+ if let Ok(key) = std::env::var(GEMINI_API_KEY_VAR) {
+ Task::ready(Ok(ApiKey {
+ key,
+ from_env: true,
+ }))
+ } else {
+ cx.spawn(async move |cx| {
+ let (_, api_key) = credentials_provider
+ .read_credentials(&api_url, cx)
+ .await?
+ .ok_or(AuthenticateError::CredentialsNotFound)?;
+
+ Ok(ApiKey {
+ key: String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?,
+ from_env: false,
+ })
+ })
+ }
+ }
}
impl LanguageModelProviderState for GoogleLanguageModelProvider {
@@ -277,8 +306,13 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
- cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
+ fn configuration_view(
+ &self,
+ target_agent: language_model::ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
+ cx.new(|cx| ConfigurationView::new(self.state.clone(), target_agent, window, cx))
.into()
}
@@ -382,7 +416,7 @@ impl LanguageModel for GoogleLanguageModel {
cx: &App,
) -> BoxFuture<'static, Result<u64>> {
let model_id = self.model.request_id().to_string();
- let request = into_google(request, model_id.clone(), self.model.mode());
+ let request = into_google(request, model_id, self.model.mode());
let http_client = self.http_client.clone();
let api_key = self.state.read(cx).api_key.clone();
@@ -525,7 +559,7 @@ pub fn into_google(
let system_instructions = if request
.messages
.first()
- .map_or(false, |msg| matches!(msg.role, Role::System))
+ .is_some_and(|msg| matches!(msg.role, Role::System))
{
let message = request.messages.remove(0);
Some(SystemInstruction {
@@ -572,7 +606,7 @@ pub fn into_google(
top_k: None,
}),
safety_settings: None,
- tools: (request.tools.len() > 0).then(|| {
+ tools: (!request.tools.is_empty()).then(|| {
vec![google_ai::Tool {
function_declarations: request
.tools
@@ -771,11 +805,17 @@ fn convert_usage(usage: &UsageMetadata) -> language_model::TokenUsage {
struct ConfigurationView {
api_key_editor: Entity<Editor>,
state: gpui::Entity<State>,
+ target_agent: language_model::ConfigurationViewTargetAgent,
load_credentials_task: Option<Task<()>>,
}
impl ConfigurationView {
- fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+ fn new(
+ state: gpui::Entity<State>,
+ target_agent: language_model::ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
cx.observe(&state, |_, _, cx| {
cx.notify();
})
@@ -802,9 +842,10 @@ impl ConfigurationView {
Self {
api_key_editor: cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("AIzaSy...", cx);
+ editor.set_placeholder_text("AIzaSy...", window, cx);
editor
}),
+ target_agent,
state,
load_credentials_task,
}
@@ -880,7 +921,10 @@ impl Render for ConfigurationView {
v_flex()
.size_full()
.on_action(cx.listener(Self::save_api_key))
- .child(Label::new("To use Zed's agent with Google AI, you need to add an API key. Follow these steps:"))
+ .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent {
+ ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI".into(),
+ ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
+ })))
.child(
List::new()
.child(InstructionListItem::new(
@@ -210,7 +210,7 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
.map(|model| {
Arc::new(LmStudioLanguageModel {
id: LanguageModelId::from(model.name.clone()),
- model: model.clone(),
+ model,
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
}) as Arc<dyn LanguageModel>
@@ -226,7 +226,12 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, _window: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ _window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
let state = self.state.clone();
cx.new(|cx| ConfigurationView::new(state, cx)).into()
}
@@ -76,7 +76,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .delete_credentials(&api_url, &cx)
+ .delete_credentials(&api_url, cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -95,7 +95,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+ .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
.await?;
this.update(cx, |this, cx| {
this.api_key = Some(api_key);
@@ -119,7 +119,7 @@ impl State {
(api_key, true)
} else {
let (_, api_key) = credentials_provider
- .read_credentials(&api_url, &cx)
+ .read_credentials(&api_url, cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(
@@ -243,7 +243,12 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
@@ -739,7 +744,7 @@ impl ConfigurationView {
fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor.set_placeholder_text("0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2", cx);
+ editor.set_placeholder_text("0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2", window, cx);
editor
});
@@ -11,8 +11,8 @@ use language_model::{
LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
};
use ollama::{
- ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionTool,
- OllamaToolCall, get_models, show_model, stream_chat_completion,
+ ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionCall,
+ OllamaFunctionTool, OllamaToolCall, get_models, show_model, stream_chat_completion,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -237,7 +237,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
.map(|model| {
Arc::new(OllamaLanguageModel {
id: LanguageModelId::from(model.name.clone()),
- model: model.clone(),
+ model,
http_client: self.http_client.clone(),
request_limiter: RateLimiter::new(4),
}) as Arc<dyn LanguageModel>
@@ -255,7 +255,12 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
let state = self.state.clone();
cx.new(|cx| ConfigurationView::new(state, window, cx))
.into()
@@ -277,59 +282,85 @@ impl OllamaLanguageModel {
fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
let supports_vision = self.model.supports_vision.unwrap_or(false);
- ChatRequest {
- model: self.model.name.clone(),
- messages: request
- .messages
- .into_iter()
- .map(|msg| {
- let images = if supports_vision {
- msg.content
- .iter()
- .filter_map(|content| match content {
- MessageContent::Image(image) => Some(image.source.to_string()),
- _ => None,
- })
- .collect::<Vec<String>>()
- } else {
- vec![]
- };
-
- match msg.role {
- Role::User => ChatMessage::User {
+ let mut messages = Vec::with_capacity(request.messages.len());
+
+ for mut msg in request.messages.into_iter() {
+ let images = if supports_vision {
+ msg.content
+ .iter()
+ .filter_map(|content| match content {
+ MessageContent::Image(image) => Some(image.source.to_string()),
+ _ => None,
+ })
+ .collect::<Vec<String>>()
+ } else {
+ vec![]
+ };
+
+ match msg.role {
+ Role::User => {
+ for tool_result in msg
+ .content
+ .extract_if(.., |x| matches!(x, MessageContent::ToolResult(..)))
+ {
+ match tool_result {
+ MessageContent::ToolResult(tool_result) => {
+ messages.push(ChatMessage::Tool {
+ tool_name: tool_result.tool_name.to_string(),
+ content: tool_result.content.to_str().unwrap_or("").to_string(),
+ })
+ }
+ _ => unreachable!("Only tool result should be extracted"),
+ }
+ }
+ if !msg.content.is_empty() {
+ messages.push(ChatMessage::User {
content: msg.string_contents(),
images: if images.is_empty() {
None
} else {
Some(images)
},
- },
- Role::Assistant => {
- let content = msg.string_contents();
- let thinking =
- msg.content.into_iter().find_map(|content| match content {
- MessageContent::Thinking { text, .. } if !text.is_empty() => {
- Some(text)
- }
- _ => None,
- });
- ChatMessage::Assistant {
- content,
- tool_calls: None,
- images: if images.is_empty() {
- None
- } else {
- Some(images)
- },
- thinking,
+ })
+ }
+ }
+ Role::Assistant => {
+ let content = msg.string_contents();
+ let mut thinking = None;
+ let mut tool_calls = Vec::new();
+ for content in msg.content.into_iter() {
+ match content {
+ MessageContent::Thinking { text, .. } if !text.is_empty() => {
+ thinking = Some(text)
+ }
+ MessageContent::ToolUse(tool_use) => {
+ tool_calls.push(OllamaToolCall::Function(OllamaFunctionCall {
+ name: tool_use.name.to_string(),
+ arguments: tool_use.input,
+ }));
}
+ _ => (),
}
- Role::System => ChatMessage::System {
- content: msg.string_contents(),
- },
}
- })
- .collect(),
+ messages.push(ChatMessage::Assistant {
+ content,
+ tool_calls: Some(tool_calls),
+ images: if images.is_empty() {
+ None
+ } else {
+ Some(images)
+ },
+ thinking,
+ })
+ }
+ Role::System => messages.push(ChatMessage::System {
+ content: msg.string_contents(),
+ }),
+ }
+ }
+ ChatRequest {
+ model: self.model.name.clone(),
+ messages,
keep_alive: self.model.keep_alive.clone().unwrap_or_default(),
stream: true,
options: Some(ChatOptions {
@@ -342,7 +373,11 @@ impl OllamaLanguageModel {
.model
.supports_thinking
.map(|supports_thinking| supports_thinking && request.thinking_allowed),
- tools: request.tools.into_iter().map(tool_into_ollama).collect(),
+ tools: if self.model.supports_tools.unwrap_or(false) {
+ request.tools.into_iter().map(tool_into_ollama).collect()
+ } else {
+ vec![]
+ },
}
}
}
@@ -474,6 +509,9 @@ fn map_to_language_model_completion_events(
ChatMessage::System { content } => {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
+ ChatMessage::Tool { content, .. } => {
+ events.push(Ok(LanguageModelCompletionEvent::Text(content)));
+ }
ChatMessage::Assistant {
content,
tool_calls,
@@ -56,13 +56,13 @@ pub struct OpenAiLanguageModelProvider {
pub struct State {
api_key: Option<String>,
api_key_from_env: bool,
+ last_api_url: String,
_subscription: Subscription,
}
const OPENAI_API_KEY_VAR: &str = "OPENAI_API_KEY";
impl State {
- //
fn is_authenticated(&self) -> bool {
self.api_key.is_some()
}
@@ -75,7 +75,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .delete_credentials(&api_url, &cx)
+ .delete_credentials(&api_url, cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -94,7 +94,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+ .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -104,11 +104,7 @@ impl State {
})
}
- fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
- if self.is_authenticated() {
- return Task::ready(Ok(()));
- }
-
+ fn get_api_key(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.openai
@@ -119,7 +115,7 @@ impl State {
(api_key, true)
} else {
let (_, api_key) = credentials_provider
- .read_credentials(&api_url, &cx)
+ .read_credentials(&api_url, cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(
@@ -136,14 +132,52 @@ impl State {
Ok(())
})
}
+
+ fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
+ if self.is_authenticated() {
+ return Task::ready(Ok(()));
+ }
+
+ self.get_api_key(cx)
+ }
}
impl OpenAiLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
+ let initial_api_url = AllLanguageModelSettings::get_global(cx)
+ .openai
+ .api_url
+ .clone();
+
let state = cx.new(|cx| State {
api_key: None,
api_key_from_env: false,
- _subscription: cx.observe_global::<SettingsStore>(|_this: &mut State, cx| {
+ last_api_url: initial_api_url.clone(),
+ _subscription: cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
+ let current_api_url = AllLanguageModelSettings::get_global(cx)
+ .openai
+ .api_url
+ .clone();
+
+ if this.last_api_url != current_api_url {
+ this.last_api_url = current_api_url;
+ if !this.api_key_from_env {
+ this.api_key = None;
+ let spawn_task = cx.spawn(async move |handle, cx| {
+ if let Ok(task) = handle.update(cx, |this, cx| this.get_api_key(cx)) {
+ if let Err(_) = task.await {
+ handle
+ .update(cx, |this, _| {
+ this.api_key = None;
+ this.api_key_from_env = false;
+ })
+ .ok();
+ }
+ }
+ });
+ spawn_task.detach();
+ }
+ }
cx.notify();
}),
});
@@ -233,7 +267,12 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
@@ -370,6 +409,7 @@ impl LanguageModel for OpenAiLanguageModel {
request,
self.model.id(),
self.model.supports_parallel_tool_calls(),
+ self.model.supports_prompt_cache_key(),
self.max_output_tokens(),
self.model.reasoning_effort(),
);
@@ -386,6 +426,7 @@ pub fn into_open_ai(
request: LanguageModelRequest,
model_id: &str,
supports_parallel_tool_calls: bool,
+ supports_prompt_cache_key: bool,
max_output_tokens: Option<u64>,
reasoning_effort: Option<ReasoningEffort>,
) -> open_ai::Request {
@@ -397,7 +438,7 @@ pub fn into_open_ai(
match content {
MessageContent::Text(text) | MessageContent::Thinking { text, .. } => {
add_message_content_part(
- open_ai::MessagePart::Text { text: text },
+ open_ai::MessagePart::Text { text },
message.role,
&mut messages,
)
@@ -477,7 +518,11 @@ pub fn into_open_ai(
} else {
None
},
- prompt_cache_key: request.thread_id,
+ prompt_cache_key: if supports_prompt_cache_key {
+ request.thread_id
+ } else {
+ None
+ },
tools: request
.tools
.into_iter()
@@ -575,7 +620,9 @@ impl OpenAiEventMapper {
};
if let Some(content) = choice.delta.content.clone() {
- events.push(Ok(LanguageModelCompletionEvent::Text(content)));
+ if !content.is_empty() {
+ events.push(Ok(LanguageModelCompletionEvent::Text(content)));
+ }
}
if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {
@@ -9,7 +9,7 @@ use language_model::{
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
- LanguageModelToolChoice, RateLimiter,
+ LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
};
use menu;
use open_ai::{ResponseStreamEvent, stream_completion};
@@ -38,6 +38,27 @@ pub struct AvailableModel {
pub max_tokens: u64,
pub max_output_tokens: Option<u64>,
pub max_completion_tokens: Option<u64>,
+ #[serde(default)]
+ pub capabilities: ModelCapabilities,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct ModelCapabilities {
+ pub tools: bool,
+ pub images: bool,
+ pub parallel_tool_calls: bool,
+ pub prompt_cache_key: bool,
+}
+
+impl Default for ModelCapabilities {
+ fn default() -> Self {
+ Self {
+ tools: true,
+ images: false,
+ parallel_tool_calls: false,
+ prompt_cache_key: false,
+ }
+ }
}
pub struct OpenAiCompatibleLanguageModelProvider {
@@ -66,7 +87,7 @@ impl State {
let api_url = self.settings.api_url.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .delete_credentials(&api_url, &cx)
+ .delete_credentials(&api_url, cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -82,7 +103,7 @@ impl State {
let api_url = self.settings.api_url.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+ .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -92,11 +113,7 @@ impl State {
})
}
- fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
- if self.is_authenticated() {
- return Task::ready(Ok(()));
- }
-
+ fn get_api_key(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let env_var_name = self.env_var_name.clone();
let api_url = self.settings.api_url.clone();
@@ -105,7 +122,7 @@ impl State {
(api_key, true)
} else {
let (_, api_key) = credentials_provider
- .read_credentials(&api_url, &cx)
+ .read_credentials(&api_url, cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(
@@ -122,6 +139,14 @@ impl State {
Ok(())
})
}
+
+ fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
+ if self.is_authenticated() {
+ return Task::ready(Ok(()));
+ }
+
+ self.get_api_key(cx)
+ }
}
impl OpenAiCompatibleLanguageModelProvider {
@@ -139,11 +164,27 @@ impl OpenAiCompatibleLanguageModelProvider {
api_key: None,
api_key_from_env: false,
_subscription: cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
- let Some(settings) = resolve_settings(&this.id, cx) else {
+ let Some(settings) = resolve_settings(&this.id, cx).cloned() else {
return;
};
- if &this.settings != settings {
- this.settings = settings.clone();
+ if &this.settings != &settings {
+ if settings.api_url != this.settings.api_url && !this.api_key_from_env {
+ let spawn_task = cx.spawn(async move |handle, cx| {
+ if let Ok(task) = handle.update(cx, |this, cx| this.get_api_key(cx)) {
+ if let Err(_) = task.await {
+ handle
+ .update(cx, |this, _| {
+ this.api_key = None;
+ this.api_key_from_env = false;
+ })
+ .ok();
+ }
+ }
+ });
+ spawn_task.detach();
+ }
+
+ this.settings = settings;
cx.notify();
}
}),
@@ -222,7 +263,12 @@ impl LanguageModelProvider for OpenAiCompatibleLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
@@ -293,17 +339,21 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
}
fn supports_tools(&self) -> bool {
- true
+ self.model.capabilities.tools
+ }
+
+ fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
+ LanguageModelToolSchemaFormat::JsonSchemaSubset
}
fn supports_images(&self) -> bool {
- false
+ self.model.capabilities.images
}
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool {
match choice {
- LanguageModelToolChoice::Auto => true,
- LanguageModelToolChoice::Any => true,
+ LanguageModelToolChoice::Auto => self.model.capabilities.tools,
+ LanguageModelToolChoice::Any => self.model.capabilities.tools,
LanguageModelToolChoice::None => true,
}
}
@@ -358,7 +408,8 @@ impl LanguageModel for OpenAiCompatibleLanguageModel {
let request = into_open_ai(
request,
&self.model.name,
- true,
+ self.model.capabilities.parallel_tool_calls,
+ self.model.capabilities.prompt_cache_key,
self.max_output_tokens(),
None,
);
@@ -92,7 +92,7 @@ pub struct State {
api_key_from_env: bool,
http_client: Arc<dyn HttpClient>,
available_models: Vec<open_router::Model>,
- fetch_models_task: Option<Task<Result<()>>>,
+ fetch_models_task: Option<Task<Result<(), LanguageModelCompletionError>>>,
settings: OpenRouterSettings,
_subscription: Subscription,
}
@@ -112,7 +112,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .delete_credentials(&api_url, &cx)
+ .delete_credentials(&api_url, cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -131,7 +131,7 @@ impl State {
.clone();
cx.spawn(async move |this, cx| {
credentials_provider
- .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+ .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -152,20 +152,21 @@ impl State {
.open_router
.api_url
.clone();
+
cx.spawn(async move |this, cx| {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(OPENROUTER_API_KEY_VAR) {
(api_key, true)
} else {
let (_, api_key) = credentials_provider
- .read_credentials(&api_url, &cx)
+ .read_credentials(&api_url, cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(
- String::from_utf8(api_key)
- .context(format!("invalid {} API key", PROVIDER_NAME))?,
+ String::from_utf8(api_key).context("invalid {PROVIDER_NAME} API key")?,
false,
)
};
+
this.update(cx, |this, cx| {
this.api_key = Some(api_key);
this.api_key_from_env = from_env;
@@ -177,18 +178,35 @@ impl State {
})
}
- fn fetch_models(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
+ fn fetch_models(
+ &mut self,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<(), LanguageModelCompletionError>> {
let settings = &AllLanguageModelSettings::get_global(cx).open_router;
let http_client = self.http_client.clone();
let api_url = settings.api_url.clone();
-
+ let Some(api_key) = self.api_key.clone() else {
+ return Task::ready(Err(LanguageModelCompletionError::NoApiKey {
+ provider: PROVIDER_NAME,
+ }));
+ };
cx.spawn(async move |this, cx| {
- let models = list_models(http_client.as_ref(), &api_url).await?;
+ let models = list_models(http_client.as_ref(), &api_url, &api_key)
+ .await
+ .map_err(|e| {
+ LanguageModelCompletionError::Other(anyhow::anyhow!(
+ "OpenRouter error: {:?}",
+ e
+ ))
+ })?;
this.update(cx, |this, cx| {
this.available_models = models;
cx.notify();
})
+ .map_err(|e| LanguageModelCompletionError::Other(e))?;
+
+ Ok(())
})
}
@@ -306,7 +324,12 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
@@ -329,27 +352,37 @@ impl OpenRouterLanguageModel {
&self,
request: open_router::Request,
cx: &AsyncApp,
- ) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<ResponseStreamEvent>>>>
- {
+ ) -> BoxFuture<
+ 'static,
+ Result<
+ futures::stream::BoxStream<
+ 'static,
+ Result<ResponseStreamEvent, open_router::OpenRouterError>,
+ >,
+ LanguageModelCompletionError,
+ >,
+ > {
let http_client = self.http_client.clone();
let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| {
let settings = &AllLanguageModelSettings::get_global(cx).open_router;
(state.api_key.clone(), settings.api_url.clone())
}) else {
- return futures::future::ready(Err(anyhow!(
- "App state dropped: Unable to read API key or API URL from the application state"
- )))
+ return futures::future::ready(Err(LanguageModelCompletionError::Other(anyhow!(
+ "App state dropped"
+ ))))
.boxed();
};
- let future = self.request_limiter.stream(async move {
- let api_key = api_key.ok_or_else(|| anyhow!("Missing OpenRouter API Key"))?;
+ async move {
+ let Some(api_key) = api_key else {
+ return Err(LanguageModelCompletionError::NoApiKey {
+ provider: PROVIDER_NAME,
+ });
+ };
let request = stream_completion(http_client.as_ref(), &api_url, &api_key, request);
- let response = request.await?;
- Ok(response)
- });
-
- async move { Ok(future.await?.boxed()) }.boxed()
+ request.await.map_err(Into::into)
+ }
+ .boxed()
}
}
@@ -376,7 +409,7 @@ impl LanguageModel for OpenRouterLanguageModel {
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
let model_id = self.model.id().trim().to_lowercase();
- if model_id.contains("gemini") || model_id.contains("grok-4") {
+ if model_id.contains("gemini") || model_id.contains("grok") {
LanguageModelToolSchemaFormat::JsonSchemaSubset
} else {
LanguageModelToolSchemaFormat::JsonSchema
@@ -430,12 +463,12 @@ impl LanguageModel for OpenRouterLanguageModel {
>,
> {
let request = into_open_router(request, &self.model, self.max_output_tokens());
- let completions = self.stream_completion(request, cx);
- async move {
- let mapper = OpenRouterEventMapper::new();
- Ok(mapper.map_stream(completions.await?).boxed())
- }
- .boxed()
+ let request = self.stream_completion(request, cx);
+ let future = self.request_limiter.stream(async move {
+ let response = request.await?;
+ Ok(OpenRouterEventMapper::new().map_stream(response))
+ });
+ async move { Ok(future.await?.boxed()) }.boxed()
}
}
@@ -603,13 +636,17 @@ impl OpenRouterEventMapper {
pub fn map_stream(
mut self,
- events: Pin<Box<dyn Send + Stream<Item = Result<ResponseStreamEvent>>>>,
+ events: Pin<
+ Box<
+ dyn Send + Stream<Item = Result<ResponseStreamEvent, open_router::OpenRouterError>>,
+ >,
+ >,
) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
{
events.flat_map(move |event| {
futures::stream::iter(match event {
Ok(event) => self.map_event(event),
- Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
+ Err(error) => vec![Err(error.into())],
})
})
}
@@ -750,8 +787,11 @@ impl ConfigurationView {
fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let api_key_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
- editor
- .set_placeholder_text("sk_or_000000000000000000000000000000000000000000000000", cx);
+ editor.set_placeholder_text(
+ "sk_or_000000000000000000000000000000000000000000000000",
+ window,
+ cx,
+ );
editor
});
@@ -71,7 +71,7 @@ impl State {
};
cx.spawn(async move |this, cx| {
credentials_provider
- .delete_credentials(&api_url, &cx)
+ .delete_credentials(&api_url, cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -92,7 +92,7 @@ impl State {
};
cx.spawn(async move |this, cx| {
credentials_provider
- .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+ .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -119,7 +119,7 @@ impl State {
(api_key, true)
} else {
let (_, api_key) = credentials_provider
- .read_credentials(&api_url, &cx)
+ .read_credentials(&api_url, cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(
@@ -230,7 +230,12 @@ impl LanguageModelProvider for VercelLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
@@ -355,6 +360,7 @@ impl LanguageModel for VercelLanguageModel {
request,
self.model.id(),
self.model.supports_parallel_tool_calls(),
+ self.model.supports_prompt_cache_key(),
self.max_output_tokens(),
None,
);
@@ -71,7 +71,7 @@ impl State {
};
cx.spawn(async move |this, cx| {
credentials_provider
- .delete_credentials(&api_url, &cx)
+ .delete_credentials(&api_url, cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -92,7 +92,7 @@ impl State {
};
cx.spawn(async move |this, cx| {
credentials_provider
- .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx)
+ .write_credentials(&api_url, "Bearer", api_key.as_bytes(), cx)
.await
.log_err();
this.update(cx, |this, cx| {
@@ -119,7 +119,7 @@ impl State {
(api_key, true)
} else {
let (_, api_key) = credentials_provider
- .read_credentials(&api_url, &cx)
+ .read_credentials(&api_url, cx)
.await?
.ok_or(AuthenticateError::CredentialsNotFound)?;
(
@@ -230,7 +230,12 @@ impl LanguageModelProvider for XAiLanguageModelProvider {
self.state.update(cx, |state, cx| state.authenticate(cx))
}
- fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
+ fn configuration_view(
+ &self,
+ _target_agent: language_model::ConfigurationViewTargetAgent,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyView {
cx.new(|cx| ConfigurationView::new(self.state.clone(), window, cx))
.into()
}
@@ -314,7 +319,7 @@ impl LanguageModel for XAiLanguageModel {
}
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
let model_id = self.model.id().trim().to_lowercase();
- if model_id.eq(x_ai::Model::Grok4.id()) {
+ if model_id.eq(x_ai::Model::Grok4.id()) || model_id.eq(x_ai::Model::GrokCodeFast1.id()) {
LanguageModelToolSchemaFormat::JsonSchemaSubset
} else {
LanguageModelToolSchemaFormat::JsonSchema
@@ -359,6 +364,7 @@ impl LanguageModel for XAiLanguageModel {
request,
self.model.id(),
self.model.supports_parallel_tool_calls(),
+ self.model.supports_prompt_cache_key(),
self.max_output_tokens(),
None,
);
@@ -5,7 +5,7 @@ use collections::HashMap;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings::{Settings, SettingsSources};
+use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use crate::provider::{
self,
@@ -46,7 +46,10 @@ pub struct AllLanguageModelSettings {
pub zed_dot_dev: ZedDotDevSettings,
}
-#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[derive(
+ Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, SettingsUi, SettingsKey,
+)]
+#[settings_key(key = "language_models")]
pub struct AllLanguageModelSettingsContent {
pub anthropic: Option<AnthropicSettingsContent>,
pub bedrock: Option<AmazonBedrockSettingsContent>,
@@ -145,8 +148,6 @@ pub struct OpenRouterSettingsContent {
}
impl settings::Settings for AllLanguageModelSettings {
- const KEY: Option<&'static str> = Some("language_models");
-
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
type FileContent = AllLanguageModelSettingsContent;
@@ -37,7 +37,7 @@ impl IntoElement for InstructionListItem {
let item_content = if let (Some(button_label), Some(button_link)) =
(self.button_label, self.button_link)
{
- let link = button_link.clone();
+ let link = button_link;
let unique_id = SharedString::from(format!("{}-button", self.label));
h_flex()
@@ -0,0 +1,30 @@
+[package]
+name = "language_onboarding"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/python.rs"
+
+[features]
+default = []
+
+[dependencies]
+db.workspace = true
+editor.workspace = true
+gpui.workspace = true
+project.workspace = true
+ui.workspace = true
+workspace.workspace = true
+workspace-hack.workspace = true
+
+# Uncomment other workspace dependencies as needed
+# assistant.workspace = true
+# client.workspace = true
+# project.workspace = true
+# settings.workspace = true
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,95 @@
+use db::kvp::Dismissable;
+use editor::Editor;
+use gpui::{Context, EventEmitter, Subscription};
+use ui::{Banner, FluentBuilder as _, prelude::*};
+use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace};
+
+pub struct BasedPyrightBanner {
+ dismissed: bool,
+ have_basedpyright: bool,
+ _subscriptions: [Subscription; 1],
+}
+
+impl Dismissable for BasedPyrightBanner {
+ const KEY: &str = "basedpyright-banner";
+}
+
+impl BasedPyrightBanner {
+ pub fn new(workspace: &Workspace, cx: &mut Context<Self>) -> Self {
+ let subscription = cx.subscribe(workspace.project(), |this, _, event, _| {
+ if let project::Event::LanguageServerAdded(_, name, _) = event
+ && name == "basedpyright"
+ {
+ this.have_basedpyright = true;
+ }
+ });
+ let dismissed = Self::dismissed();
+ Self {
+ dismissed,
+ have_basedpyright: false,
+ _subscriptions: [subscription],
+ }
+ }
+}
+
+impl EventEmitter<ToolbarItemEvent> for BasedPyrightBanner {}
+
+impl Render for BasedPyrightBanner {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .id("basedpyright-banner")
+ .when(!self.dismissed && self.have_basedpyright, |el| {
+ el.child(
+ Banner::new()
+ .child(
+ v_flex()
+ .gap_0p5()
+ .child(Label::new("Basedpyright is now the only default language server for Python").mt_0p5())
+ .child(Label::new("We have disabled PyRight and pylsp by default. They can be re-enabled in your settings.").size(LabelSize::Small).color(Color::Muted))
+ )
+ .action_slot(
+ h_flex()
+ .gap_0p5()
+ .child(
+ Button::new("learn-more", "Learn More")
+ .icon(IconName::ArrowUpRight)
+ .label_size(LabelSize::Small)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .on_click(|_, _, cx| {
+ cx.open_url("https://zed.dev/docs/languages/python")
+ }),
+ )
+ .child(IconButton::new("dismiss", IconName::Close).icon_size(IconSize::Small).on_click(
+ cx.listener(|this, _, _, cx| {
+ this.dismissed = true;
+ Self::set_dismissed(true, cx);
+ cx.notify();
+ }),
+ ))
+ )
+ .into_any_element(),
+ )
+ })
+ }
+}
+
+impl ToolbarItemView for BasedPyrightBanner {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn workspace::ItemHandle>,
+ _window: &mut ui::Window,
+ cx: &mut Context<Self>,
+ ) -> ToolbarItemLocation {
+ if let Some(item) = active_pane_item
+ && let Some(editor) = item.act_as::<Editor>(cx)
+ && let Some(path) = editor.update(cx, |editor, cx| editor.target_file_abs_path(cx))
+ && let Some(file_name) = path.file_name()
+ && file_name.as_encoded_bytes().ends_with(".py".as_bytes())
+ {
+ return ToolbarItemLocation::Secondary;
+ }
+
+ ToolbarItemLocation::Hidden
+ }
+}
@@ -28,10 +28,10 @@ impl ActiveBufferLanguage {
self.active_language = Some(None);
let editor = editor.read(cx);
- if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
- if let Some(language) = buffer.read(cx).language() {
- self.active_language = Some(Some(language.name()));
- }
+ if let Some((_, buffer, _)) = editor.active_excerpt(cx)
+ && let Some(language) = buffer.read(cx).language()
+ {
+ self.active_language = Some(Some(language.name()));
}
cx.notify();
@@ -283,7 +283,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let mat = &self.matches[ix];
+ let mat = &self.matches.get(ix)?;
let (label, language_icon) = self.language_data_for_match(mat, cx);
Some(
ListItem::new(ix)
@@ -16,6 +16,7 @@ doctest = false
anyhow.workspace = true
client.workspace = true
collections.workspace = true
+command_palette_hooks.workspace = true
copilot.workspace = true
editor.workspace = true
futures.workspace = true
@@ -24,6 +25,7 @@ itertools.workspace = true
language.workspace = true
lsp.workspace = true
project.workspace = true
+proto.workspace = true
serde_json.workspace = true
settings.workspace = true
theme.workspace = true
@@ -4,7 +4,6 @@ use gpui::{
};
use itertools::Itertools;
use serde_json::json;
-use settings::get_key_equivalents;
use ui::{Button, ButtonStyle};
use ui::{
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
@@ -71,12 +70,10 @@ impl KeyContextView {
} else {
None
}
+ } else if this.action_matches(&e.action, binding.action()) {
+ Some(true)
} else {
- if this.action_matches(&e.action, binding.action()) {
- Some(true)
- } else {
- Some(false)
- }
+ Some(false)
};
let predicate = if let Some(predicate) = binding.predicate() {
format!("{}", predicate)
@@ -98,9 +95,7 @@ impl KeyContextView {
cx.notify();
});
let sub2 = cx.observe_pending_input(window, |this, window, cx| {
- this.pending_keystrokes = window
- .pending_input_keystrokes()
- .map(|k| k.iter().cloned().collect());
+ this.pending_keystrokes = window.pending_input_keystrokes().map(|k| k.to_vec());
if this.pending_keystrokes.is_some() {
this.last_keystrokes.take();
}
@@ -173,7 +168,8 @@ impl Item for KeyContextView {
impl Render for KeyContextView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
use itertools::Itertools;
- let key_equivalents = get_key_equivalents(cx.keyboard_layout().id());
+
+ let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
v_flex()
.id("key-context-view")
.overflow_scroll()
@@ -1,20 +1,20 @@
mod key_context_view;
-mod lsp_log;
-pub mod lsp_tool;
+pub mod lsp_button;
+pub mod lsp_log_view;
mod syntax_tree_view;
#[cfg(test)]
-mod lsp_log_tests;
+mod lsp_log_view_tests;
use gpui::{App, AppContext, Entity};
-pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView};
+pub use lsp_log_view::LspLogView;
pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
use ui::{Context, Window};
use workspace::{Item, ItemHandle, SplitDirection, Workspace};
pub fn init(cx: &mut App) {
- lsp_log::init(cx);
+ lsp_log_view::init(false, cx);
syntax_tree_view::init(cx);
key_context_view::init(cx);
}
@@ -11,7 +11,10 @@ use editor::{Editor, EditorEvent};
use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
use language::{BinaryStatus, BufferId, ServerHealth};
use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
-use project::{LspStore, LspStoreEvent, Worktree, project_settings::ProjectSettings};
+use project::{
+ LspStore, LspStoreEvent, Worktree, lsp_store::log_store::GlobalLogStore,
+ project_settings::ProjectSettings,
+};
use settings::{Settings as _, SettingsStore};
use ui::{
Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
@@ -20,7 +23,7 @@ use ui::{
use workspace::{StatusItemView, Workspace};
-use crate::lsp_log::GlobalLogStore;
+use crate::lsp_log_view;
actions!(
lsp_tool,
@@ -30,7 +33,7 @@ actions!(
]
);
-pub struct LspTool {
+pub struct LspButton {
server_state: Entity<LanguageServerState>,
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
lsp_menu: Option<Entity<ContextMenu>>,
@@ -121,9 +124,8 @@ impl LanguageServerState {
menu = menu.align_popover_bottom();
let lsp_logs = cx
.try_global::<GlobalLogStore>()
- .and_then(|lsp_logs| lsp_logs.0.upgrade());
- let lsp_store = self.lsp_store.upgrade();
- let Some((lsp_logs, lsp_store)) = lsp_logs.zip(lsp_store) else {
+ .map(|lsp_logs| lsp_logs.0.clone());
+ let Some(lsp_logs) = lsp_logs else {
return menu;
};
@@ -210,10 +212,11 @@ impl LanguageServerState {
};
let server_selector = server_info.server_selector();
- // TODO currently, Zed remote does not work well with the LSP logs
- // https://github.com/zed-industries/zed/issues/28557
- let has_logs = lsp_store.read(cx).as_local().is_some()
- && lsp_logs.read(cx).has_server_logs(&server_selector);
+ let is_remote = self
+ .lsp_store
+ .update(cx, |lsp_store, _| lsp_store.as_remote().is_some())
+ .unwrap_or(false);
+ let has_logs = is_remote || lsp_logs.read(cx).has_server_logs(&server_selector);
let status_color = server_info
.binary_status
@@ -241,10 +244,10 @@ impl LanguageServerState {
.as_ref()
.or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
.cloned();
- let hover_label = if has_logs {
- Some("View Logs")
- } else if message.is_some() {
+ let hover_label = if message.is_some() {
Some("View Message")
+ } else if has_logs {
+ Some("View Logs")
} else {
None
};
@@ -288,21 +291,12 @@ impl LanguageServerState {
let server_name = server_info.name.clone();
let workspace = self.workspace.clone();
move |window, cx| {
- if has_logs {
- lsp_logs.update(cx, |lsp_logs, cx| {
- lsp_logs.open_server_trace(
- workspace.clone(),
- server_selector.clone(),
- window,
- cx,
- );
- });
- } else if let Some(message) = &message {
+ if let Some(message) = &message {
let Some(create_buffer) = workspace
.update(cx, |workspace, cx| {
workspace
.project()
- .update(cx, |project, cx| project.create_buffer(cx))
+ .update(cx, |project, cx| project.create_buffer(false, cx))
})
.ok()
else {
@@ -347,9 +341,16 @@ impl LanguageServerState {
anyhow::Ok(())
})
.detach();
+ } else if has_logs {
+ lsp_log_view::open_server_trace(
+ &lsp_logs,
+ workspace.clone(),
+ server_selector.clone(),
+ window,
+ cx,
+ );
} else {
cx.propagate();
- return;
}
}
},
@@ -511,7 +512,7 @@ impl ServerData<'_> {
}
}
-impl LspTool {
+impl LspButton {
pub fn new(
workspace: &Workspace,
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -519,38 +520,59 @@ impl LspTool {
cx: &mut Context<Self>,
) -> Self {
let settings_subscription =
- cx.observe_global_in::<SettingsStore>(window, move |lsp_tool, window, cx| {
+ cx.observe_global_in::<SettingsStore>(window, move |lsp_button, window, cx| {
if ProjectSettings::get_global(cx).global_lsp_settings.button {
- if lsp_tool.lsp_menu.is_none() {
- lsp_tool.refresh_lsp_menu(true, window, cx);
- return;
+ if lsp_button.lsp_menu.is_none() {
+ lsp_button.refresh_lsp_menu(true, window, cx);
}
- } else if lsp_tool.lsp_menu.take().is_some() {
+ } else if lsp_button.lsp_menu.take().is_some() {
cx.notify();
}
});
let lsp_store = workspace.project().read(cx).lsp_store();
+ let mut language_servers = LanguageServers::default();
+ for (_, status) in lsp_store.read(cx).language_server_statuses() {
+ language_servers.binary_statuses.insert(
+ status.name.clone(),
+ LanguageServerBinaryStatus {
+ status: BinaryStatus::None,
+ message: None,
+ },
+ );
+ }
+
let lsp_store_subscription =
- cx.subscribe_in(&lsp_store, window, |lsp_tool, _, e, window, cx| {
- lsp_tool.on_lsp_store_event(e, window, cx)
+ cx.subscribe_in(&lsp_store, window, |lsp_button, _, e, window, cx| {
+ lsp_button.on_lsp_store_event(e, window, cx)
});
- let state = cx.new(|_| LanguageServerState {
+ let server_state = cx.new(|_| LanguageServerState {
workspace: workspace.weak_handle(),
items: Vec::new(),
lsp_store: lsp_store.downgrade(),
active_editor: None,
- language_servers: LanguageServers::default(),
+ language_servers,
});
- Self {
- server_state: state,
+ let mut lsp_button = Self {
+ server_state,
popover_menu_handle,
lsp_menu: None,
lsp_menu_refresh: Task::ready(()),
_subscriptions: vec![settings_subscription, lsp_store_subscription],
+ };
+ if !lsp_button
+ .server_state
+ .read(cx)
+ .language_servers
+ .binary_statuses
+ .is_empty()
+ {
+ lsp_button.refresh_lsp_menu(true, window, cx);
}
+
+ lsp_button
}
fn on_lsp_store_event(
@@ -710,6 +732,25 @@ impl LspTool {
}
}
}
+ state
+ .lsp_store
+ .update(cx, |lsp_store, cx| {
+ for (server_id, status) in lsp_store.language_server_statuses() {
+ if let Some(worktree) = status.worktree.and_then(|worktree_id| {
+ lsp_store
+ .worktree_store()
+ .read(cx)
+ .worktree_for_id(worktree_id, cx)
+ }) {
+ server_ids_to_worktrees.insert(server_id, worktree.clone());
+ server_names_to_worktrees
+ .entry(status.name.clone())
+ .or_default()
+ .insert((worktree, server_id));
+ }
+ }
+ })
+ .ok();
let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
let mut servers_without_worktree = Vec::<ServerData>::new();
@@ -854,18 +895,18 @@ impl LspTool {
) {
if create_if_empty || self.lsp_menu.is_some() {
let state = self.server_state.clone();
- self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_tool, cx| {
+ self.lsp_menu_refresh = cx.spawn_in(window, async move |lsp_button, cx| {
cx.background_executor()
.timer(Duration::from_millis(30))
.await;
- lsp_tool
- .update_in(cx, |lsp_tool, window, cx| {
- lsp_tool.regenerate_items(cx);
+ lsp_button
+ .update_in(cx, |lsp_button, window, cx| {
+ lsp_button.regenerate_items(cx);
let menu = ContextMenu::build(window, cx, |menu, _, cx| {
state.update(cx, |state, cx| state.fill_menu(menu, cx))
});
- lsp_tool.lsp_menu = Some(menu.clone());
- lsp_tool.popover_menu_handle.refresh_menu(
+ lsp_button.lsp_menu = Some(menu.clone());
+ lsp_button.popover_menu_handle.refresh_menu(
window,
cx,
Rc::new(move |_, _| Some(menu.clone())),
@@ -878,7 +919,7 @@ impl LspTool {
}
}
-impl StatusItemView for LspTool {
+impl StatusItemView for LspButton {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>,
@@ -901,9 +942,9 @@ impl StatusItemView for LspTool {
let _editor_subscription = cx.subscribe_in(
&editor,
window,
- |lsp_tool, _, e: &EditorEvent, window, cx| match e {
+ |lsp_button, _, e: &EditorEvent, window, cx| match e {
EditorEvent::ExcerptsAdded { buffer, .. } => {
- let updated = lsp_tool.server_state.update(cx, |state, cx| {
+ let updated = lsp_button.server_state.update(cx, |state, cx| {
if let Some(active_editor) = state.active_editor.as_mut() {
let buffer_id = buffer.read(cx).remote_id();
active_editor.editor_buffers.insert(buffer_id)
@@ -912,13 +953,13 @@ impl StatusItemView for LspTool {
}
});
if updated {
- lsp_tool.refresh_lsp_menu(false, window, cx);
+ lsp_button.refresh_lsp_menu(false, window, cx);
}
}
EditorEvent::ExcerptsRemoved {
removed_buffer_ids, ..
} => {
- let removed = lsp_tool.server_state.update(cx, |state, _| {
+ let removed = lsp_button.server_state.update(cx, |state, _| {
let mut removed = false;
if let Some(active_editor) = state.active_editor.as_mut() {
for id in removed_buffer_ids {
@@ -932,7 +973,7 @@ impl StatusItemView for LspTool {
removed
});
if removed {
- lsp_tool.refresh_lsp_menu(false, window, cx);
+ lsp_button.refresh_lsp_menu(false, window, cx);
}
}
_ => {}
@@ -962,7 +1003,7 @@ impl StatusItemView for LspTool {
}
}
-impl Render for LspTool {
+impl Render for LspButton {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
return div();
@@ -1007,11 +1048,11 @@ impl Render for LspTool {
(None, "All Servers Operational")
};
- let lsp_tool = cx.entity().clone();
+ let lsp_button = cx.entity();
div().child(
PopoverMenu::new("lsp-tool")
- .menu(move |_, cx| lsp_tool.read(cx).lsp_menu.clone())
+ .menu(move |_, cx| lsp_button.read(cx).lsp_menu.clone())
.anchor(Corner::BottomLeft)
.with_handle(self.popover_menu_handle.clone())
.trigger_with_tooltip(
@@ -1,20 +1,25 @@
-use collections::{HashMap, VecDeque};
+use collections::VecDeque;
use copilot::Copilot;
use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll};
-use futures::{StreamExt, channel::mpsc};
use gpui::{
- AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, Global,
- IntoElement, ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,
+ AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
+ ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div,
};
use itertools::Itertools;
use language::{LanguageServerId, language_settings::SoftWrap};
use lsp::{
- IoKind, LanguageServer, LanguageServerName, LanguageServerSelector, MessageType,
+ LanguageServer, LanguageServerBinary, LanguageServerName, LanguageServerSelector, MessageType,
SetTraceParams, TraceValue, notification::SetTrace,
};
-use project::{Project, WorktreeId, search::SearchQuery};
+use project::{
+ Project,
+ lsp_store::log_store::{self, Event, LanguageServerKind, LogKind, LogStore, Message},
+ search::SearchQuery,
+};
+use proto::toggle_lsp_logs::LogType;
use std::{any::TypeId, borrow::Cow, sync::Arc};
use ui::{Button, Checkbox, ContextMenu, Label, PopoverMenu, ToggleState, prelude::*};
+use util::ResultExt as _;
use workspace::{
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
item::{Item, ItemHandle},
@@ -23,132 +28,53 @@ use workspace::{
use crate::get_or_create_tool;
-const SEND_LINE: &str = "\n// Send:";
-const RECEIVE_LINE: &str = "\n// Receive:";
-const MAX_STORED_LOG_ENTRIES: usize = 2000;
-
-pub struct LogStore {
- projects: HashMap<WeakEntity<Project>, ProjectState>,
- language_servers: HashMap<LanguageServerId, LanguageServerState>,
- copilot_log_subscription: Option<lsp::Subscription>,
- _copilot_subscription: Option<gpui::Subscription>,
- io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>,
-}
-
-struct ProjectState {
- _subscriptions: [gpui::Subscription; 2],
-}
-
-trait Message: AsRef<str> {
- type Level: Copy + std::fmt::Debug;
- fn should_include(&self, _: Self::Level) -> bool {
- true
- }
-}
-
-pub(super) struct LogMessage {
- message: String,
- typ: MessageType,
-}
-
-impl AsRef<str> for LogMessage {
- fn as_ref(&self) -> &str {
- &self.message
- }
-}
-
-impl Message for LogMessage {
- type Level = MessageType;
-
- fn should_include(&self, level: Self::Level) -> bool {
- match (self.typ, level) {
- (MessageType::ERROR, _) => true,
- (_, MessageType::ERROR) => false,
- (MessageType::WARNING, _) => true,
- (_, MessageType::WARNING) => false,
- (MessageType::INFO, _) => true,
- (_, MessageType::INFO) => false,
- _ => true,
- }
- }
-}
-
-pub(super) struct TraceMessage {
- message: String,
-}
-
-impl AsRef<str> for TraceMessage {
- fn as_ref(&self) -> &str {
- &self.message
- }
-}
-
-impl Message for TraceMessage {
- type Level = ();
-}
-
-struct RpcMessage {
- message: String,
-}
-
-impl AsRef<str> for RpcMessage {
- fn as_ref(&self) -> &str {
- &self.message
- }
-}
-
-impl Message for RpcMessage {
- type Level = ();
-}
-
-pub(super) struct LanguageServerState {
- name: Option<LanguageServerName>,
- worktree_id: Option<WorktreeId>,
- kind: LanguageServerKind,
- log_messages: VecDeque<LogMessage>,
- trace_messages: VecDeque<TraceMessage>,
- rpc_state: Option<LanguageServerRpcState>,
- trace_level: TraceValue,
- log_level: MessageType,
- io_logs_subscription: Option<lsp::Subscription>,
-}
-
-#[derive(PartialEq, Clone)]
-pub enum LanguageServerKind {
- Local { project: WeakEntity<Project> },
- Remote { project: WeakEntity<Project> },
- Global,
-}
-
-impl LanguageServerKind {
- fn is_remote(&self) -> bool {
- matches!(self, LanguageServerKind::Remote { .. })
- }
-}
-
-impl std::fmt::Debug for LanguageServerKind {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"),
- LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"),
- LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"),
- }
- }
-}
-
-impl LanguageServerKind {
- fn project(&self) -> Option<&WeakEntity<Project>> {
- match self {
- Self::Local { project } => Some(project),
- Self::Remote { project } => Some(project),
- Self::Global { .. } => None,
- }
- }
-}
-
-struct LanguageServerRpcState {
- rpc_messages: VecDeque<RpcMessage>,
- last_message_kind: Option<MessageKind>,
+pub fn open_server_trace(
+ log_store: &Entity<LogStore>,
+ workspace: WeakEntity<Workspace>,
+ server: LanguageServerSelector,
+ window: &mut Window,
+ cx: &mut App,
+) {
+ log_store.update(cx, |_, cx| {
+ cx.spawn_in(window, async move |log_store, cx| {
+ let Some(log_store) = log_store.upgrade() else {
+ return;
+ };
+ workspace
+ .update_in(cx, |workspace, window, cx| {
+ let project = workspace.project().clone();
+ let tool_log_store = log_store.clone();
+ let log_view = get_or_create_tool(
+ workspace,
+ SplitDirection::Right,
+ window,
+ cx,
+ move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
+ );
+ log_view.update(cx, |log_view, cx| {
+ let server_id = match server {
+ LanguageServerSelector::Id(id) => Some(id),
+ LanguageServerSelector::Name(name) => {
+ log_store.read(cx).language_servers.iter().find_map(
+ |(id, state)| {
+ if state.name.as_ref() == Some(&name) {
+ Some(*id)
+ } else {
+ None
+ }
+ },
+ )
+ }
+ };
+ if let Some(server_id) = server_id {
+ log_view.show_rpc_trace_for_server(server_id, window, cx);
+ }
+ });
+ })
+ .ok();
+ })
+ .detach();
+ })
}
pub struct LspLogView {
@@ -167,32 +93,6 @@ pub struct LspLogToolbarItemView {
_log_view_subscription: Option<Subscription>,
}
-#[derive(Copy, Clone, PartialEq, Eq)]
-enum MessageKind {
- Send,
- Receive,
-}
-
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
-pub enum LogKind {
- Rpc,
- Trace,
- #[default]
- Logs,
- ServerInfo,
-}
-
-impl LogKind {
- fn label(&self) -> &'static str {
- match self {
- LogKind::Rpc => RPC_MESSAGES,
- LogKind::Trace => SERVER_TRACE,
- LogKind::Logs => SERVER_LOGS,
- LogKind::ServerInfo => SERVER_INFO,
- }
- }
-}
-
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct LogMenuItem {
pub server_id: LanguageServerId,
@@ -212,505 +112,68 @@ actions!(
]
);
-pub(super) struct GlobalLogStore(pub WeakEntity<LogStore>);
-
-impl Global for GlobalLogStore {}
+pub fn init(on_headless_host: bool, cx: &mut App) {
+ let log_store = log_store::init(on_headless_host, cx);
-pub fn init(cx: &mut App) {
- let log_store = cx.new(LogStore::new);
- cx.set_global(GlobalLogStore(log_store.downgrade()));
-
- cx.observe_new(move |workspace: &mut Workspace, _, cx| {
- let project = workspace.project();
- if project.read(cx).is_local() || project.read(cx).is_via_ssh() {
- log_store.update(cx, |store, cx| {
- store.add_project(project, cx);
- });
- }
-
- let log_store = log_store.clone();
- workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
- let project = workspace.project().read(cx);
- if project.is_local() || project.is_via_ssh() {
- let project = workspace.project().clone();
- let log_store = log_store.clone();
- get_or_create_tool(
- workspace,
- SplitDirection::Right,
- window,
- cx,
- move |window, cx| LspLogView::new(project, log_store, window, cx),
- );
- }
- });
- })
- .detach();
-}
-
-impl LogStore {
- pub fn new(cx: &mut Context<Self>) -> Self {
- let (io_tx, mut io_rx) = mpsc::unbounded();
-
- let copilot_subscription = Copilot::global(cx).map(|copilot| {
+ log_store.update(cx, |_, cx| {
+ Copilot::global(cx).map(|copilot| {
let copilot = &copilot;
- cx.subscribe(copilot, |this, copilot, edit_prediction_event, cx| {
- if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event {
- if let Some(server) = copilot.read(cx).language_server() {
- let server_id = server.server_id();
- let weak_this = cx.weak_entity();
- this.copilot_log_subscription =
- Some(server.on_notification::<copilot::request::LogMessage, _>(
- move |params, cx| {
- weak_this
- .update(cx, |this, cx| {
- this.add_language_server_log(
- server_id,
- MessageType::LOG,
- ¶ms.message,
- cx,
- );
- })
- .ok();
- },
- ));
- let name = LanguageServerName::new_static("copilot");
- this.add_language_server(
- LanguageServerKind::Global,
- server.server_id(),
- Some(name),
- None,
- Some(server.clone()),
- cx,
- );
- }
+ cx.subscribe(copilot, |log_store, copilot, edit_prediction_event, cx| {
+ if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event
+ && let Some(server) = copilot.read(cx).language_server()
+ {
+ let server_id = server.server_id();
+ let weak_lsp_store = cx.weak_entity();
+ log_store.copilot_log_subscription =
+ Some(server.on_notification::<copilot::request::LogMessage, _>(
+ move |params, cx| {
+ weak_lsp_store
+ .update(cx, |lsp_store, cx| {
+ lsp_store.add_language_server_log(
+ server_id,
+ MessageType::LOG,
+ ¶ms.message,
+ cx,
+ );
+ })
+ .ok();
+ },
+ ));
+
+ let name = LanguageServerName::new_static("copilot");
+ log_store.add_language_server(
+ LanguageServerKind::Global,
+ server.server_id(),
+ Some(name),
+ None,
+ Some(server.clone()),
+ cx,
+ );
}
})
- });
-
- let this = Self {
- copilot_log_subscription: None,
- _copilot_subscription: copilot_subscription,
- projects: HashMap::default(),
- language_servers: HashMap::default(),
- io_tx,
- };
-
- cx.spawn(async move |this, cx| {
- while let Some((server_id, io_kind, message)) = io_rx.next().await {
- if let Some(this) = this.upgrade() {
- this.update(cx, |this, cx| {
- this.on_io(server_id, io_kind, &message, cx);
- })?;
- }
- }
- anyhow::Ok(())
+ .detach();
})
- .detach_and_log_err(cx);
- this
- }
+ });
- pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
- let weak_project = project.downgrade();
- self.projects.insert(
- project.downgrade(),
- ProjectState {
- _subscriptions: [
- cx.observe_release(project, move |this, _, _| {
- this.projects.remove(&weak_project);
- this.language_servers
- .retain(|_, state| state.kind.project() != Some(&weak_project));
- }),
- cx.subscribe(project, |this, project, event, cx| {
- let server_kind = if project.read(cx).is_via_ssh() {
- LanguageServerKind::Remote {
- project: project.downgrade(),
- }
- } else {
- LanguageServerKind::Local {
- project: project.downgrade(),
- }
- };
-
- match event {
- project::Event::LanguageServerAdded(id, name, worktree_id) => {
- this.add_language_server(
- server_kind,
- *id,
- Some(name.clone()),
- *worktree_id,
- project
- .read(cx)
- .lsp_store()
- .read(cx)
- .language_server_for_id(*id),
- cx,
- );
- }
- project::Event::LanguageServerRemoved(id) => {
- this.remove_language_server(*id, cx);
- }
- project::Event::LanguageServerLog(id, typ, message) => {
- this.add_language_server(server_kind, *id, None, None, None, cx);
- match typ {
- project::LanguageServerLogType::Log(typ) => {
- this.add_language_server_log(*id, *typ, message, cx);
- }
- project::LanguageServerLogType::Trace(_) => {
- this.add_language_server_trace(*id, message, cx);
- }
- }
- }
- _ => {}
- }
- }),
- ],
- },
- );
- }
-
- pub(super) fn get_language_server_state(
- &mut self,
- id: LanguageServerId,
- ) -> Option<&mut LanguageServerState> {
- self.language_servers.get_mut(&id)
- }
-
- fn add_language_server(
- &mut self,
- kind: LanguageServerKind,
- server_id: LanguageServerId,
- name: Option<LanguageServerName>,
- worktree_id: Option<WorktreeId>,
- server: Option<Arc<LanguageServer>>,
- cx: &mut Context<Self>,
- ) -> Option<&mut LanguageServerState> {
- let server_state = self.language_servers.entry(server_id).or_insert_with(|| {
- cx.notify();
- LanguageServerState {
- name: None,
- worktree_id: None,
- kind,
- rpc_state: None,
- log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
- trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
- trace_level: TraceValue::Off,
- log_level: MessageType::LOG,
- io_logs_subscription: None,
- }
+ cx.observe_new(move |workspace: &mut Workspace, _, cx| {
+ log_store.update(cx, |store, cx| {
+ store.add_project(workspace.project(), cx);
});
- if let Some(name) = name {
- server_state.name = Some(name);
- }
- if let Some(worktree_id) = worktree_id {
- server_state.worktree_id = Some(worktree_id);
- }
-
- if let Some(server) = server
- .clone()
- .filter(|_| server_state.io_logs_subscription.is_none())
- {
- let io_tx = self.io_tx.clone();
- let server_id = server.server_id();
- server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| {
- io_tx
- .unbounded_send((server_id, io_kind, message.to_string()))
- .ok();
- }));
- }
-
- Some(server_state)
- }
-
- fn add_language_server_log(
- &mut self,
- id: LanguageServerId,
- typ: MessageType,
- message: &str,
- cx: &mut Context<Self>,
- ) -> Option<()> {
- let language_server_state = self.get_language_server_state(id)?;
-
- let log_lines = &mut language_server_state.log_messages;
- Self::add_language_server_message(
- log_lines,
- id,
- LogMessage {
- message: message.trim_end().to_string(),
- typ,
- },
- language_server_state.log_level,
- LogKind::Logs,
- cx,
- );
- Some(())
- }
-
- fn add_language_server_trace(
- &mut self,
- id: LanguageServerId,
- message: &str,
- cx: &mut Context<Self>,
- ) -> Option<()> {
- let language_server_state = self.get_language_server_state(id)?;
-
- let log_lines = &mut language_server_state.trace_messages;
- Self::add_language_server_message(
- log_lines,
- id,
- TraceMessage {
- message: message.trim().to_string(),
- },
- (),
- LogKind::Trace,
- cx,
- );
- Some(())
- }
-
- fn add_language_server_message<T: Message>(
- log_lines: &mut VecDeque<T>,
- id: LanguageServerId,
- message: T,
- current_severity: <T as Message>::Level,
- kind: LogKind,
- cx: &mut Context<Self>,
- ) {
- while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
- log_lines.pop_front();
- }
- let text = message.as_ref().to_string();
- let visible = message.should_include(current_severity);
- log_lines.push_back(message);
-
- if visible {
- cx.emit(Event::NewServerLogEntry { id, kind, text });
- cx.notify();
- }
- }
-
- fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context<Self>) {
- self.language_servers.remove(&id);
- cx.notify();
- }
-
- pub(super) fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque<LogMessage>> {
- Some(&self.language_servers.get(&server_id)?.log_messages)
- }
-
- pub(super) fn server_trace(
- &self,
- server_id: LanguageServerId,
- ) -> Option<&VecDeque<TraceMessage>> {
- Some(&self.language_servers.get(&server_id)?.trace_messages)
- }
-
- fn server_ids_for_project<'a>(
- &'a self,
- lookup_project: &'a WeakEntity<Project>,
- ) -> impl Iterator<Item = LanguageServerId> + 'a {
- self.language_servers
- .iter()
- .filter_map(move |(id, state)| match &state.kind {
- LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => {
- if project == lookup_project {
- Some(*id)
- } else {
- None
- }
- }
- LanguageServerKind::Global => Some(*id),
- })
- }
-
- fn enable_rpc_trace_for_language_server(
- &mut self,
- server_id: LanguageServerId,
- ) -> Option<&mut LanguageServerRpcState> {
- let rpc_state = self
- .language_servers
- .get_mut(&server_id)?
- .rpc_state
- .get_or_insert_with(|| LanguageServerRpcState {
- rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
- last_message_kind: None,
- });
- Some(rpc_state)
- }
-
- pub fn disable_rpc_trace_for_language_server(
- &mut self,
- server_id: LanguageServerId,
- ) -> Option<()> {
- self.language_servers.get_mut(&server_id)?.rpc_state.take();
- Some(())
- }
-
- pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool {
- match server {
- LanguageServerSelector::Id(id) => self.language_servers.contains_key(id),
- LanguageServerSelector::Name(name) => self
- .language_servers
- .iter()
- .any(|(_, state)| state.name.as_ref() == Some(name)),
- }
- }
-
- pub fn open_server_log(
- &mut self,
- workspace: WeakEntity<Workspace>,
- server: LanguageServerSelector,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- cx.spawn_in(window, async move |log_store, cx| {
- let Some(log_store) = log_store.upgrade() else {
- return;
- };
- workspace
- .update_in(cx, |workspace, window, cx| {
- let project = workspace.project().clone();
- let tool_log_store = log_store.clone();
- let log_view = get_or_create_tool(
- workspace,
- SplitDirection::Right,
- window,
- cx,
- move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
- );
- log_view.update(cx, |log_view, cx| {
- let server_id = match server {
- LanguageServerSelector::Id(id) => Some(id),
- LanguageServerSelector::Name(name) => {
- log_store.read(cx).language_servers.iter().find_map(
- |(id, state)| {
- if state.name.as_ref() == Some(&name) {
- Some(*id)
- } else {
- None
- }
- },
- )
- }
- };
- if let Some(server_id) = server_id {
- log_view.show_logs_for_server(server_id, window, cx);
- }
- });
- })
- .ok();
- })
- .detach();
- }
-
- pub fn open_server_trace(
- &mut self,
- workspace: WeakEntity<Workspace>,
- server: LanguageServerSelector,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- cx.spawn_in(window, async move |log_store, cx| {
- let Some(log_store) = log_store.upgrade() else {
- return;
- };
- workspace
- .update_in(cx, |workspace, window, cx| {
- let project = workspace.project().clone();
- let tool_log_store = log_store.clone();
- let log_view = get_or_create_tool(
- workspace,
- SplitDirection::Right,
- window,
- cx,
- move |window, cx| LspLogView::new(project, tool_log_store, window, cx),
- );
- log_view.update(cx, |log_view, cx| {
- let server_id = match server {
- LanguageServerSelector::Id(id) => Some(id),
- LanguageServerSelector::Name(name) => {
- log_store.read(cx).language_servers.iter().find_map(
- |(id, state)| {
- if state.name.as_ref() == Some(&name) {
- Some(*id)
- } else {
- None
- }
- },
- )
- }
- };
- if let Some(server_id) = server_id {
- log_view.show_rpc_trace_for_server(server_id, window, cx);
- }
- });
- })
- .ok();
- })
- .detach();
- }
-
- fn on_io(
- &mut self,
- language_server_id: LanguageServerId,
- io_kind: IoKind,
- message: &str,
- cx: &mut Context<Self>,
- ) -> Option<()> {
- let is_received = match io_kind {
- IoKind::StdOut => true,
- IoKind::StdIn => false,
- IoKind::StdErr => {
- self.add_language_server_log(language_server_id, MessageType::LOG, &message, cx);
- return Some(());
- }
- };
-
- let state = self
- .get_language_server_state(language_server_id)?
- .rpc_state
- .as_mut()?;
- let kind = if is_received {
- MessageKind::Receive
- } else {
- MessageKind::Send
- };
-
- let rpc_log_lines = &mut state.rpc_messages;
- if state.last_message_kind != Some(kind) {
- while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
- rpc_log_lines.pop_front();
- }
- let line_before_message = match kind {
- MessageKind::Send => SEND_LINE,
- MessageKind::Receive => RECEIVE_LINE,
- };
- rpc_log_lines.push_back(RpcMessage {
- message: line_before_message.to_string(),
- });
- cx.emit(Event::NewServerLogEntry {
- id: language_server_id,
- kind: LogKind::Rpc,
- text: line_before_message.to_string(),
- });
- }
-
- while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES {
- rpc_log_lines.pop_front();
- }
-
- let message = message.trim();
- rpc_log_lines.push_back(RpcMessage {
- message: message.to_string(),
- });
- cx.emit(Event::NewServerLogEntry {
- id: language_server_id,
- kind: LogKind::Rpc,
- text: message.to_string(),
+ let log_store = log_store.clone();
+ workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, window, cx| {
+ let log_store = log_store.clone();
+ let project = workspace.project().clone();
+ get_or_create_tool(
+ workspace,
+ SplitDirection::Right,
+ window,
+ cx,
+ move |window, cx| LspLogView::new(project, log_store, window, cx),
+ );
});
- cx.notify();
- Some(())
- }
+ })
+ .detach();
}
impl LspLogView {
@@ -733,16 +196,14 @@ impl LspLogView {
let first_server_id_for_project =
store.read(cx).server_ids_for_project(&weak_project).next();
if let Some(current_lsp) = this.current_server_id {
- if !store.read(cx).language_servers.contains_key(¤t_lsp) {
- if let Some(server_id) = first_server_id_for_project {
- match this.active_entry_kind {
- LogKind::Rpc => {
- this.show_rpc_trace_for_server(server_id, window, cx)
- }
- LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
- LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
- LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
- }
+ if !store.read(cx).language_servers.contains_key(¤t_lsp)
+ && let Some(server_id) = first_server_id_for_project
+ {
+ match this.active_entry_kind {
+ LogKind::Rpc => this.show_rpc_trace_for_server(server_id, window, cx),
+ LogKind::Trace => this.show_trace_for_server(server_id, window, cx),
+ LogKind::Logs => this.show_logs_for_server(server_id, window, cx),
+ LogKind::ServerInfo => this.show_server_info(server_id, window, cx),
}
}
} else if let Some(server_id) = first_server_id_for_project {
@@ -756,13 +217,14 @@ impl LspLogView {
cx.notify();
});
+
let events_subscriptions = cx.subscribe_in(
&log_store,
window,
move |log_view, _, e, window, cx| match e {
Event::NewServerLogEntry { id, kind, text } => {
if log_view.current_server_id == Some(*id)
- && *kind == log_view.active_entry_kind
+ && LogKind::from_server_log_type(kind) == log_view.active_entry_kind
{
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
@@ -776,21 +238,17 @@ impl LspLogView {
],
cx,
);
- if text.len() > 1024 {
- if let Some((fold_offset, _)) =
+ if text.len() > 1024
+ && let Some((fold_offset, _)) =
text.char_indices().dropping(1024).next()
- {
- if fold_offset < text.len() {
- editor.fold_ranges(
- vec![
- last_offset + fold_offset..last_offset + text.len(),
- ],
- false,
- window,
- cx,
- );
- }
- }
+ && fold_offset < text.len()
+ {
+ editor.fold_ranges(
+ vec![last_offset + fold_offset..last_offset + text.len()],
+ false,
+ window,
+ cx,
+ );
}
if newest_cursor_is_at_end {
@@ -809,7 +267,20 @@ impl LspLogView {
window.focus(&log_view.editor.focus_handle(cx));
});
- let mut this = Self {
+ cx.on_release(|log_view, cx| {
+ log_view.log_store.update(cx, |log_store, cx| {
+ for (server_id, state) in &log_store.language_servers {
+ if let Some(log_kind) = state.toggled_log_kind {
+ if let Some(log_type) = log_type(log_kind) {
+ send_toggle_log_message(state, *server_id, false, log_type, cx);
+ }
+ }
+ }
+ });
+ })
+ .detach();
+
+ let mut lsp_log_view = Self {
focus_handle,
editor,
editor_subscriptions,
@@ -824,9 +295,9 @@ impl LspLogView {
],
};
if let Some(server_id) = server_id {
- this.show_logs_for_server(server_id, window, cx);
+ lsp_log_view.show_logs_for_server(server_id, window, cx);
}
- this
+ lsp_log_view
}
fn editor_for_logs(
@@ -847,14 +318,14 @@ impl LspLogView {
}
fn editor_for_server_info(
- server: &LanguageServer,
+ info: ServerInfo,
window: &mut Window,
cx: &mut Context<Self>,
) -> (Entity<Editor>, Vec<Subscription>) {
let server_info = format!(
"* Server: {NAME} (id {ID})
-* Binary: {BINARY:#?}
+* Binary: {BINARY}
* Registered workspace folders:
{WORKSPACE_FOLDERS}
@@ -862,22 +333,21 @@ impl LspLogView {
* Capabilities: {CAPABILITIES}
* Configuration: {CONFIGURATION}",
- NAME = server.name(),
- ID = server.server_id(),
- BINARY = server.binary(),
- WORKSPACE_FOLDERS = server
- .workspace_folders()
- .into_iter()
- .filter_map(|path| path
- .to_file_path()
- .ok()
- .map(|path| path.to_string_lossy().into_owned()))
- .collect::<Vec<_>>()
- .join(", "),
- CAPABILITIES = serde_json::to_string_pretty(&server.capabilities())
+ NAME = info.name,
+ ID = info.id,
+ BINARY = info
+ .binary
+ .as_ref()
+ .map_or_else(|| "Unknown".to_string(), |binary| format!("{binary:#?}")),
+ WORKSPACE_FOLDERS = info.workspace_folders.join(", "),
+ CAPABILITIES = serde_json::to_string_pretty(&info.capabilities)
.unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
- CONFIGURATION = serde_json::to_string_pretty(server.configuration())
- .unwrap_or_else(|e| format!("Failed to serialize configuration: {e}")),
+ CONFIGURATION = info
+ .configuration
+ .map(|configuration| serde_json::to_string_pretty(&configuration))
+ .transpose()
+ .unwrap_or_else(|e| Some(format!("Failed to serialize configuration: {e}")))
+ .unwrap_or_else(|| "Unknown".to_string()),
);
let editor = initialize_new_editor(server_info, false, window, cx);
let editor_subscription = cx.subscribe(
@@ -900,7 +370,9 @@ impl LspLogView {
.language_servers
.iter()
.map(|(server_id, state)| match &state.kind {
- LanguageServerKind::Local { .. } | LanguageServerKind::Remote { .. } => {
+ LanguageServerKind::Local { .. }
+ | LanguageServerKind::Remote { .. }
+ | LanguageServerKind::LocalSsh { .. } => {
let worktree_root_name = state
.worktree_id
.and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
@@ -936,7 +408,7 @@ impl LspLogView {
let state = log_store.language_servers.get(&server_id)?;
Some(LogMenuItem {
server_id,
- server_name: name.clone(),
+ server_name: name,
server_kind: state.kind.clone(),
worktree_root_name: "supplementary".to_string(),
rpc_trace_enabled: state.rpc_state.is_some(),
@@ -978,6 +450,12 @@ impl LspLogView {
cx.notify();
}
self.editor.read(cx).focus_handle(cx).focus(window);
+ self.log_store.update(cx, |log_store, cx| {
+ let state = log_store.get_language_server_state(server_id)?;
+ state.toggled_log_kind = Some(LogKind::Logs);
+ send_toggle_log_message(state, server_id, true, LogType::Log, cx);
+ Some(())
+ });
}
fn update_log_level(
@@ -1012,17 +490,29 @@ impl LspLogView {
window: &mut Window,
cx: &mut Context<Self>,
) {
+ let trace_level = self
+ .log_store
+ .update(cx, |log_store, _| {
+ Some(log_store.get_language_server_state(server_id)?.trace_level)
+ })
+ .unwrap_or(TraceValue::Messages);
let log_contents = self
.log_store
.read(cx)
.server_trace(server_id)
- .map(|v| log_contents(v, ()));
+ .map(|v| log_contents(v, trace_level));
if let Some(log_contents) = log_contents {
self.current_server_id = Some(server_id);
self.active_entry_kind = LogKind::Trace;
let (editor, editor_subscriptions) = Self::editor_for_logs(log_contents, window, cx);
self.editor = editor;
self.editor_subscriptions = editor_subscriptions;
+ self.log_store.update(cx, |log_store, cx| {
+ let state = log_store.get_language_server_state(server_id)?;
+ state.toggled_log_kind = Some(LogKind::Trace);
+ send_toggle_log_message(state, server_id, true, LogType::Trace, cx);
+ Some(())
+ });
cx.notify();
}
self.editor.read(cx).focus_handle(cx).focus(window);
@@ -1,20 +1,22 @@
use std::sync::Arc;
-use crate::lsp_log::LogMenuItem;
+use crate::lsp_log_view::LogMenuItem;
use super::*;
use futures::StreamExt;
use gpui::{AppContext as _, SemanticVersion, TestAppContext, VisualTestContext};
use language::{FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
use lsp::LanguageServerName;
-use lsp_log::LogKind;
-use project::{FakeFs, Project};
+use project::{
+ FakeFs, Project,
+ lsp_store::log_store::{LanguageServerKind, LogKind, LogStore},
+};
use serde_json::json;
use settings::SettingsStore;
use util::path;
#[gpui::test]
-async fn test_lsp_logs(cx: &mut TestAppContext) {
+async fn test_lsp_log_view(cx: &mut TestAppContext) {
zlog::init_test();
init_test(cx);
@@ -51,7 +53,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
},
);
- let log_store = cx.new(LogStore::new);
+ let log_store = cx.new(|cx| LogStore::new(false, cx));
log_store.update(cx, |store, cx| store.add_project(&project, cx));
let _rust_buffer = project
@@ -94,7 +96,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
rpc_trace_enabled: false,
selected_entry: LogKind::Logs,
trace_level: lsp::TraceValue::Off,
- server_kind: lsp_log::LanguageServerKind::Local {
+ server_kind: LanguageServerKind::Local {
project: project.downgrade()
}
}]
@@ -1,17 +1,22 @@
+use command_palette_hooks::CommandPaletteFilter;
use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll};
use gpui::{
- App, AppContext as _, Context, Div, Entity, EventEmitter, FocusHandle, Focusable, Hsla,
- InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement,
- Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle, WeakEntity, Window,
- actions, div, rems, uniform_list,
+ App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
+ Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
+ ParentElement, Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle,
+ WeakEntity, Window, actions, div, rems, uniform_list,
};
use language::{Buffer, OwnedSyntaxLayer};
-use std::{mem, ops::Range};
+use std::{any::TypeId, mem, ops::Range};
use theme::ActiveTheme;
use tree_sitter::{Node, TreeCursor};
-use ui::{ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex};
+use ui::{
+ ButtonCommon, ButtonLike, Clickable, Color, ContextMenu, FluentBuilder as _, IconButton,
+ IconName, Label, LabelCommon, LabelSize, PopoverMenu, StyledExt, Tooltip, h_flex, v_flex,
+};
use workspace::{
- SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
+ Event as WorkspaceEvent, SplitDirection, ToolbarItemEvent, ToolbarItemLocation,
+ ToolbarItemView, Workspace,
item::{Item, ItemHandle},
};
@@ -19,17 +24,51 @@ actions!(
dev,
[
/// Opens the syntax tree view for the current file.
- OpenSyntaxTreeView
+ OpenSyntaxTreeView,
+ ]
+);
+
+actions!(
+ syntax_tree_view,
+ [
+ /// Update the syntax tree view to show the last focused file.
+ UseActiveEditor
]
);
pub fn init(cx: &mut App) {
- cx.observe_new(|workspace: &mut Workspace, _, _| {
- workspace.register_action(|workspace, _: &OpenSyntaxTreeView, window, cx| {
+ let syntax_tree_actions = [TypeId::of::<UseActiveEditor>()];
+
+ CommandPaletteFilter::update_global(cx, |this, _| {
+ this.hide_action_types(&syntax_tree_actions);
+ });
+
+ cx.observe_new(move |workspace: &mut Workspace, _, _| {
+ workspace.register_action(move |workspace, _: &OpenSyntaxTreeView, window, cx| {
+ CommandPaletteFilter::update_global(cx, |this, _| {
+ this.show_action_types(&syntax_tree_actions);
+ });
+
let active_item = workspace.active_item(cx);
let workspace_handle = workspace.weak_handle();
- let syntax_tree_view =
- cx.new(|cx| SyntaxTreeView::new(workspace_handle, active_item, window, cx));
+ let syntax_tree_view = cx.new(|cx| {
+ cx.on_release(move |view: &mut SyntaxTreeView, cx| {
+ if view
+ .workspace_handle
+ .read_with(cx, |workspace, cx| {
+ workspace.item_of_type::<SyntaxTreeView>(cx).is_none()
+ })
+ .unwrap_or_default()
+ {
+ CommandPaletteFilter::update_global(cx, |this, _| {
+ this.hide_action_types(&syntax_tree_actions);
+ });
+ }
+ })
+ .detach();
+
+ SyntaxTreeView::new(workspace_handle, active_item, window, cx)
+ });
workspace.split_item(
SplitDirection::Right,
Box::new(syntax_tree_view),
@@ -37,6 +76,13 @@ pub fn init(cx: &mut App) {
cx,
)
});
+ workspace.register_action(|workspace, _: &UseActiveEditor, window, cx| {
+ if let Some(tree_view) = workspace.item_of_type::<SyntaxTreeView>(cx) {
+ tree_view.update(cx, |view, cx| {
+ view.update_active_editor(&Default::default(), window, cx)
+ })
+ }
+ });
})
.detach();
}
@@ -45,6 +91,9 @@ pub struct SyntaxTreeView {
workspace_handle: WeakEntity<Workspace>,
editor: Option<EditorState>,
list_scroll_handle: UniformListScrollHandle,
+ /// The last active editor in the workspace. Note that this is specifically not the
+ /// currently shown editor.
+ last_active_editor: Option<Entity<Editor>>,
selected_descendant_ix: Option<usize>,
hovered_descendant_ix: Option<usize>,
focus_handle: FocusHandle,
@@ -61,6 +110,14 @@ struct EditorState {
_subscription: gpui::Subscription,
}
+impl EditorState {
+ fn has_language(&self) -> bool {
+ self.active_buffer
+ .as_ref()
+ .is_some_and(|buffer| buffer.active_layer.is_some())
+ }
+}
+
#[derive(Clone)]
struct BufferState {
buffer: Entity<Buffer>,
@@ -79,17 +136,25 @@ impl SyntaxTreeView {
workspace_handle: workspace_handle.clone(),
list_scroll_handle: UniformListScrollHandle::new(),
editor: None,
+ last_active_editor: None,
hovered_descendant_ix: None,
selected_descendant_ix: None,
focus_handle: cx.focus_handle(),
};
- this.workspace_updated(active_item, window, cx);
- cx.observe_in(
+ this.handle_item_updated(active_item, window, cx);
+
+ cx.subscribe_in(
&workspace_handle.upgrade().unwrap(),
window,
- |this, workspace, window, cx| {
- this.workspace_updated(workspace.read(cx).active_item(cx), window, cx);
+ move |this, workspace, event, window, cx| match event {
+ WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::ActiveItemChanged => {
+ this.handle_item_updated(workspace.read(cx).active_item(cx), window, cx)
+ }
+ WorkspaceEvent::ItemRemoved { item_id } => {
+ this.handle_item_removed(item_id, window, cx);
+ }
+ _ => {}
},
)
.detach();
@@ -97,21 +162,56 @@ impl SyntaxTreeView {
this
}
- fn workspace_updated(
+ fn handle_item_updated(
&mut self,
active_item: Option<Box<dyn ItemHandle>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
- if let Some(item) = active_item {
- if item.item_id() != cx.entity_id() {
- if let Some(editor) = item.act_as::<Editor>(cx) {
- self.set_editor(editor, window, cx);
- }
- }
+ let Some(editor) = active_item
+ .filter(|item| item.item_id() != cx.entity_id())
+ .and_then(|item| item.act_as::<Editor>(cx))
+ else {
+ return;
+ };
+
+ if let Some(editor_state) = self.editor.as_ref().filter(|state| state.has_language()) {
+ self.last_active_editor = (editor_state.editor != editor).then_some(editor);
+ } else {
+ self.set_editor(editor, window, cx);
}
}
+ fn handle_item_removed(
+ &mut self,
+ item_id: &EntityId,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self
+ .editor
+ .as_ref()
+ .is_some_and(|state| state.editor.entity_id() == *item_id)
+ {
+ self.editor = None;
+ // Try activating the last active editor if there is one
+ self.update_active_editor(&Default::default(), window, cx);
+ cx.notify();
+ }
+ }
+
+ fn update_active_editor(
+ &mut self,
+ _: &UseActiveEditor,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(editor) = self.last_active_editor.take() else {
+ return;
+ };
+ self.set_editor(editor, window, cx);
+ }
+
fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
if let Some(state) = &self.editor {
if state.editor == editor {
@@ -157,7 +257,7 @@ impl SyntaxTreeView {
.buffer_snapshot
.range_to_buffer_ranges(selection_range)
.pop()?;
- let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap().clone();
+ let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap();
Some((buffer, range, excerpt_id))
})?;
@@ -295,101 +395,153 @@ impl SyntaxTreeView {
.pl(rems(depth as f32))
.hover(|style| style.bg(colors.element_hover))
}
-}
-
-impl Render for SyntaxTreeView {
- fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let mut rendered = div().flex_1().bg(cx.theme().colors().editor_background);
- if let Some(layer) = self
- .editor
- .as_ref()
- .and_then(|editor| editor.active_buffer.as_ref())
- .and_then(|buffer| buffer.active_layer.as_ref())
- {
- let layer = layer.clone();
- rendered = rendered.child(uniform_list(
- "SyntaxTreeView",
- layer.node().descendant_count(),
- cx.processor(move |this, range: Range<usize>, _, cx| {
- let mut items = Vec::new();
- let mut cursor = layer.node().walk();
- let mut descendant_ix = range.start;
- cursor.goto_descendant(descendant_ix);
- let mut depth = cursor.depth();
- let mut visited_children = false;
- while descendant_ix < range.end {
- if visited_children {
- if cursor.goto_next_sibling() {
- visited_children = false;
- } else if cursor.goto_parent() {
- depth -= 1;
- } else {
- break;
- }
- } else {
- items.push(
- Self::render_node(
- &cursor,
- depth,
- Some(descendant_ix) == this.selected_descendant_ix,
+ fn compute_items(
+ &mut self,
+ layer: &OwnedSyntaxLayer,
+ range: Range<usize>,
+ cx: &Context<Self>,
+ ) -> Vec<Div> {
+ let mut items = Vec::new();
+ let mut cursor = layer.node().walk();
+ let mut descendant_ix = range.start;
+ cursor.goto_descendant(descendant_ix);
+ let mut depth = cursor.depth();
+ let mut visited_children = false;
+ while descendant_ix < range.end {
+ if visited_children {
+ if cursor.goto_next_sibling() {
+ visited_children = false;
+ } else if cursor.goto_parent() {
+ depth -= 1;
+ } else {
+ break;
+ }
+ } else {
+ items.push(
+ Self::render_node(
+ &cursor,
+ depth,
+ Some(descendant_ix) == self.selected_descendant_ix,
+ cx,
+ )
+ .on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
+ tree_view.update_editor_with_range_for_descendant_ix(
+ descendant_ix,
+ window,
+ cx,
+ |editor, mut range, window, cx| {
+ // Put the cursor at the beginning of the node.
+ mem::swap(&mut range.start, &mut range.end);
+
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::newest()),
+ window,
+ cx,
+ |selections| {
+ selections.select_ranges(vec![range]);
+ },
+ );
+ },
+ );
+ }),
+ )
+ .on_mouse_move(cx.listener(
+ move |tree_view, _: &MouseMoveEvent, window, cx| {
+ if tree_view.hovered_descendant_ix != Some(descendant_ix) {
+ tree_view.hovered_descendant_ix = Some(descendant_ix);
+ tree_view.update_editor_with_range_for_descendant_ix(
+ descendant_ix,
+ window,
cx,
- )
- .on_mouse_down(
- MouseButton::Left,
- cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
- tree_view.update_editor_with_range_for_descendant_ix(
- descendant_ix,
- window, cx,
- |editor, mut range, window, cx| {
- // Put the cursor at the beginning of the node.
- mem::swap(&mut range.start, &mut range.end);
-
- editor.change_selections(
- SelectionEffects::scroll(Autoscroll::newest()),
- window, cx,
- |selections| {
- selections.select_ranges(vec![range]);
- },
- );
+ |editor, range, _, cx| {
+ editor.clear_background_highlights::<Self>(cx);
+ editor.highlight_background::<Self>(
+ &[range],
+ |theme| {
+ theme
+ .colors()
+ .editor_document_highlight_write_background
},
+ cx,
);
- }),
- )
- .on_mouse_move(cx.listener(
- move |tree_view, _: &MouseMoveEvent, window, cx| {
- if tree_view.hovered_descendant_ix != Some(descendant_ix) {
- tree_view.hovered_descendant_ix = Some(descendant_ix);
- tree_view.update_editor_with_range_for_descendant_ix(descendant_ix, window, cx, |editor, range, _, cx| {
- editor.clear_background_highlights::<Self>( cx);
- editor.highlight_background::<Self>(
- &[range],
- |theme| theme.colors().editor_document_highlight_write_background,
- cx,
- );
- });
- cx.notify();
- }
},
- )),
- );
- descendant_ix += 1;
- if cursor.goto_first_child() {
- depth += 1;
- } else {
- visited_children = true;
+ );
+ cx.notify();
}
- }
- }
- items
- }),
- )
- .size_full()
- .track_scroll(self.list_scroll_handle.clone())
- .text_bg(cx.theme().colors().background).into_any_element());
+ },
+ )),
+ );
+ descendant_ix += 1;
+ if cursor.goto_first_child() {
+ depth += 1;
+ } else {
+ visited_children = true;
+ }
+ }
}
+ items
+ }
+}
- rendered
+impl Render for SyntaxTreeView {
+ fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .flex_1()
+ .bg(cx.theme().colors().editor_background)
+ .map(|this| {
+ let editor_state = self.editor.as_ref();
+
+ if let Some(layer) = editor_state
+ .and_then(|editor| editor.active_buffer.as_ref())
+ .and_then(|buffer| buffer.active_layer.as_ref())
+ {
+ let layer = layer.clone();
+ this.child(
+ uniform_list(
+ "SyntaxTreeView",
+ layer.node().descendant_count(),
+ cx.processor(move |this, range: Range<usize>, _, cx| {
+ this.compute_items(&layer, range, cx)
+ }),
+ )
+ .size_full()
+ .track_scroll(self.list_scroll_handle.clone())
+ .text_bg(cx.theme().colors().background)
+ .into_any_element(),
+ )
+ } else {
+ let inner_content = v_flex()
+ .items_center()
+ .text_center()
+ .gap_2()
+ .max_w_3_5()
+ .map(|this| {
+ if editor_state.is_some_and(|state| !state.has_language()) {
+ this.child(Label::new("Current editor has no associated language"))
+ .child(
+ Label::new(concat!(
+ "Try assigning a language or",
+ "switching to a different buffer"
+ ))
+ .size(LabelSize::Small),
+ )
+ } else {
+ this.child(Label::new("Not attached to an editor")).child(
+ Label::new("Focus an editor to show a new tree view")
+ .size(LabelSize::Small),
+ )
+ }
+ });
+
+ this.h_flex()
+ .size_full()
+ .justify_center()
+ .child(inner_content)
+ }
+ })
}
}
@@ -456,7 +608,7 @@ impl SyntaxTreeToolbarItemView {
let active_layer = buffer_state.active_layer.clone()?;
let active_buffer = buffer_state.buffer.read(cx).snapshot();
- let view = cx.entity().clone();
+ let view = cx.entity();
Some(
PopoverMenu::new("Syntax Tree")
.trigger(Self::render_header(&active_layer))
@@ -507,6 +659,26 @@ impl SyntaxTreeToolbarItemView {
.child(Label::new(active_layer.language.name()))
.child(Label::new(format_node_range(active_layer.node())))
}
+
+ fn render_update_button(&mut self, cx: &mut Context<Self>) -> Option<IconButton> {
+ self.tree_view.as_ref().and_then(|view| {
+ view.update(cx, |view, cx| {
+ view.last_active_editor.as_ref().map(|editor| {
+ IconButton::new("syntax-view-update", IconName::RotateCw)
+ .tooltip({
+ let active_tab_name = editor.read_with(cx, |editor, cx| {
+ editor.tab_content_text(Default::default(), cx)
+ });
+
+ Tooltip::text(format!("Update view to '{active_tab_name}'"))
+ })
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.update_active_editor(&Default::default(), window, cx);
+ }))
+ })
+ })
+ })
+ }
}
fn format_node_range(node: Node) -> String {
@@ -523,8 +695,10 @@ fn format_node_range(node: Node) -> String {
impl Render for SyntaxTreeToolbarItemView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- self.render_menu(cx)
- .unwrap_or_else(|| PopoverMenu::new("Empty Syntax Tree"))
+ h_flex()
+ .gap_1()
+ .children(self.render_menu(cx))
+ .children(self.render_update_button(cx))
}
}
@@ -537,12 +711,12 @@ impl ToolbarItemView for SyntaxTreeToolbarItemView {
window: &mut Window,
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
- if let Some(item) = active_pane_item {
- if let Some(view) = item.downcast::<SyntaxTreeView>() {
- self.tree_view = Some(view.clone());
- self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
- return ToolbarItemLocation::PrimaryLeft;
- }
+ if let Some(item) = active_pane_item
+ && let Some(view) = item.downcast::<SyntaxTreeView>()
+ {
+ self.tree_view = Some(view.clone());
+ self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
+ return ToolbarItemLocation::PrimaryLeft;
}
self.tree_view = None;
self.subscription = None;
@@ -42,7 +42,6 @@ async-trait.workspace = true
chrono.workspace = true
collections.workspace = true
dap.workspace = true
-feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
@@ -5,8 +5,9 @@ use gpui::{App, AsyncApp};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};
pub use language::*;
use lsp::{InitializeParams, LanguageServerBinary, LanguageServerName};
-use project::lsp_store::clangd_ext;
+use project::{lsp_store::clangd_ext, project_settings::ProjectSettings};
use serde_json::json;
+use settings::Settings as _;
use smol::fs;
use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
@@ -22,13 +23,13 @@ impl CLspAdapter {
#[async_trait(?Send)]
impl super::LspAdapter for CLspAdapter {
fn name(&self) -> LanguageServerName {
- Self::SERVER_NAME.clone()
+ Self::SERVER_NAME
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
@@ -42,9 +43,19 @@ impl super::LspAdapter for CLspAdapter {
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
+ cx: &AsyncApp,
) -> Result<Box<dyn 'static + Send + Any>> {
- let release =
- latest_github_release("clangd/clangd", true, false, delegate.http_client()).await?;
+ let release = latest_github_release(
+ "clangd/clangd",
+ true,
+ ProjectSettings::try_read_global(cx, |s| {
+ s.lsp.get(&Self::SERVER_NAME)?.fetch.as_ref()?.pre_release
+ })
+ .flatten()
+ .unwrap_or(false),
+ delegate.http_client(),
+ )
+ .await?;
let os_suffix = match consts::OS {
"macos" => "mac",
"linux" => "linux",
@@ -253,8 +264,7 @@ impl super::LspAdapter for CLspAdapter {
.grammar()
.and_then(|g| g.highlight_id_for_name(highlight_name?))
{
- let mut label =
- CodeLabel::plain(label.to_string(), completion.filter_text.as_deref());
+ let mut label = CodeLabel::plain(label, completion.filter_text.as_deref());
label.runs.push((
0..label.text.rfind('(').unwrap_or(label.text.len()),
highlight_id,
@@ -264,10 +274,7 @@ impl super::LspAdapter for CLspAdapter {
}
_ => {}
}
- Some(CodeLabel::plain(
- label.to_string(),
- completion.filter_text.as_deref(),
- ))
+ Some(CodeLabel::plain(label, completion.filter_text.as_deref()))
}
async fn label_for_symbol(
@@ -3,8 +3,27 @@
(namespace_identifier) @namespace
(concept_definition
- (identifier) @concept)
+ name: (identifier) @concept)
+(requires_clause
+ constraint: (template_type
+ name: (type_identifier) @concept))
+
+(module_name
+ (identifier) @module)
+
+(module_declaration
+ name: (module_name
+ (identifier) @module))
+
+(import_declaration
+ name: (module_name
+ (identifier) @module))
+
+(import_declaration
+ partition: (module_partition
+ (module_name
+ (identifier) @module)))
(call_expression
function: (qualified_identifier
@@ -61,6 +80,9 @@
(operator_name
(identifier)? @operator) @function
+(operator_name
+ "<=>" @operator.spaceship)
+
(destructor_name (identifier) @function)
((namespace_identifier) @type
@@ -68,21 +90,17 @@
(auto) @type
(type_identifier) @type
-type :(primitive_type) @type.primitive
-(sized_type_specifier) @type.primitive
-
-(requires_clause
- constraint: (template_type
- name: (type_identifier) @concept))
+type: (primitive_type) @type.builtin
+(sized_type_specifier) @type.builtin
(attribute
- name: (identifier) @keyword)
+ name: (identifier) @attribute)
-((identifier) @constant
- (#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
+((identifier) @constant.builtin
+ (#match? @constant.builtin "^_*[A-Z][A-Z\\d_]*$"))
(statement_identifier) @label
-(this) @variable.special
+(this) @variable.builtin
("static_assert") @function.builtin
[
@@ -96,7 +114,9 @@ type :(primitive_type) @type.primitive
"co_return"
"co_yield"
"concept"
+ "consteval"
"constexpr"
+ "constinit"
"continue"
"decltype"
"default"
@@ -105,15 +125,20 @@ type :(primitive_type) @type.primitive
"else"
"enum"
"explicit"
+ "export"
"extern"
"final"
"for"
"friend"
+ "goto"
"if"
+ "import"
"inline"
+ "module"
"namespace"
"new"
"noexcept"
+ "operator"
"override"
"private"
"protected"
@@ -124,6 +149,7 @@ type :(primitive_type) @type.primitive
"struct"
"switch"
"template"
+ "thread_local"
"throw"
"try"
"typedef"
@@ -146,7 +172,7 @@ type :(primitive_type) @type.primitive
"#ifndef"
"#include"
(preproc_directive)
-] @keyword
+] @keyword.directive
(comment) @comment
@@ -224,10 +250,24 @@ type :(primitive_type) @type.primitive
">"
"<="
">="
- "<=>"
- "||"
"?"
+ "and"
+ "and_eq"
+ "bitand"
+ "bitor"
+ "compl"
+ "not"
+ "not_eq"
+ "or"
+ "or_eq"
+ "xor"
+ "xor_eq"
] @operator
+"<=>" @operator.spaceship
+
+(binary_expression
+ operator: "<=>" @operator.spaceship)
+
(conditional_expression ":" @operator)
(user_defined_literal (literal_suffix) @operator)
@@ -2,9 +2,9 @@ use anyhow::{Context as _, Result};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::AsyncApp;
-use language::{LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
+use language::{LspAdapter, LspAdapterDelegate, Toolchain};
use lsp::{LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use serde_json::json;
use smol::fs;
@@ -43,7 +43,7 @@ impl LspAdapter for CssLspAdapter {
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate
@@ -61,6 +61,7 @@ impl LspAdapter for CssLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(
self.node
@@ -106,9 +107,8 @@ impl LspAdapter for CssLspAdapter {
.should_install_npm_package(
Self::PACKAGE_NAME,
&server_path,
- &container_dir,
- &version,
- Default::default(),
+ container_dir,
+ VersionStrategy::Latest(version),
)
.await;
@@ -145,7 +145,7 @@ impl LspAdapter for CssLspAdapter {
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
cx: &mut AsyncApp,
) -> Result<serde_json::Value> {
let mut default_config = json!({
@@ -237,7 +237,7 @@ mod tests {
.unindent();
let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
- let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
+ let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
assert_eq!(
outline
.items
@@ -96,7 +96,7 @@ async fn stream_response_archive(
AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?,
AssetKind::Gz => extract_gz(destination_path, url, response).await?,
AssetKind::Zip => {
- util::archive::extract_zip(&destination_path, response).await?;
+ util::archive::extract_zip(destination_path, response).await?;
}
};
Ok(())
@@ -113,11 +113,11 @@ async fn stream_file_archive(
AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?,
#[cfg(not(windows))]
AssetKind::Zip => {
- util::archive::extract_seekable_zip(&destination_path, file_archive).await?;
+ util::archive::extract_seekable_zip(destination_path, file_archive).await?;
}
#[cfg(windows)]
AssetKind::Zip => {
- util::archive::extract_zip(&destination_path, file_archive).await?;
+ util::archive::extract_zip(destination_path, file_archive).await?;
}
};
Ok(())
@@ -53,12 +53,13 @@ const BINARY: &str = if cfg!(target_os = "windows") {
#[async_trait(?Send)]
impl super::LspAdapter for GoLspAdapter {
fn name(&self) -> LanguageServerName {
- Self::SERVER_NAME.clone()
+ Self::SERVER_NAME
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Send + Any>> {
let release =
latest_github_release("golang/tools", false, false, delegate.http_client()).await?;
@@ -75,7 +76,7 @@ impl super::LspAdapter for GoLspAdapter {
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
@@ -131,19 +132,19 @@ impl super::LspAdapter for GoLspAdapter {
if let Some(version) = *version {
let binary_path = container_dir.join(format!("gopls_{version}_go_{go_version}"));
- if let Ok(metadata) = fs::metadata(&binary_path).await {
- if metadata.is_file() {
- remove_matching(&container_dir, |entry| {
- entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
- })
- .await;
+ if let Ok(metadata) = fs::metadata(&binary_path).await
+ && metadata.is_file()
+ {
+ remove_matching(&container_dir, |entry| {
+ entry != binary_path && entry.file_name() != Some(OsStr::new("gobin"))
+ })
+ .await;
- return Ok(LanguageServerBinary {
- path: binary_path.to_path_buf(),
- arguments: server_binary_arguments(),
- env: None,
- });
- }
+ return Ok(LanguageServerBinary {
+ path: binary_path.to_path_buf(),
+ arguments: server_binary_arguments(),
+ env: None,
+ });
}
} else if let Some(path) = this
.cached_server_binary(container_dir.clone(), delegate)
@@ -203,7 +204,7 @@ impl super::LspAdapter for GoLspAdapter {
_: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
Ok(Some(json!({
- "usePlaceholders": true,
+ "usePlaceholders": false,
"hints": {
"assignVariableTypes": true,
"compositeLiteralFields": true,
@@ -452,7 +453,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
&& entry
.file_name()
.to_str()
- .map_or(false, |name| name.starts_with("gopls_"))
+ .is_some_and(|name| name.starts_with("gopls_"))
{
last_binary_path = Some(entry.path());
}
@@ -525,7 +526,7 @@ impl ContextProvider for GoContextProvider {
})
.unwrap_or_else(|| format!("{}", buffer_dir.to_string_lossy()));
- (GO_PACKAGE_TASK_VARIABLE.clone(), package_name.to_string())
+ (GO_PACKAGE_TASK_VARIABLE.clone(), package_name)
});
let go_module_root_variable = local_abs_path
@@ -702,7 +703,7 @@ impl ContextProvider for GoContextProvider {
label: format!("go generate {}", GO_PACKAGE_TASK_VARIABLE.template_value()),
command: "go".into(),
args: vec!["generate".into()],
- cwd: package_cwd.clone(),
+ cwd: package_cwd,
tags: vec!["go-generate".to_owned()],
..TaskTemplate::default()
},
@@ -710,7 +711,7 @@ impl ContextProvider for GoContextProvider {
label: "go generate ./...".into(),
command: "go".into(),
args: vec!["generate".into(), "./...".into()],
- cwd: module_cwd.clone(),
+ cwd: module_cwd,
..TaskTemplate::default()
},
])))
@@ -764,6 +765,7 @@ mod tests {
let highlight_type = grammar.highlight_id_for_name("type").unwrap();
let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
let highlight_number = grammar.highlight_id_for_name("number").unwrap();
+ let highlight_field = grammar.highlight_id_for_name("property").unwrap();
assert_eq!(
adapter
@@ -828,7 +830,7 @@ mod tests {
Some(CodeLabel {
text: "two.Three a.Bcd".to_string(),
filter_range: 0..9,
- runs: vec![(12..15, highlight_type)],
+ runs: vec![(4..9, highlight_field), (12..15, highlight_type)],
})
);
}
@@ -1,13 +1,15 @@
(identifier) @variable
(type_identifier) @type
-(field_identifier) @variable.member
+(field_identifier) @property
(package_identifier) @namespace
+(label_name) @label
+
(keyed_element
.
(literal_element
- (identifier) @variable.member))
+ (identifier) @property))
(call_expression
function: (identifier) @function)
@@ -6,6 +6,7 @@ first_line_pattern = '^#!.*\b(?:[/ ]node|deno run.*--ext[= ]js)\b'
line_comments = ["// "]
block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }
documentation_comment = { start = "/**", prefix = "* ", end = "*/", tab_size = 1 }
+wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "</", end_suffix = ">" }
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
@@ -231,6 +231,7 @@
"implements"
"interface"
"keyof"
+ "module"
"namespace"
"private"
"protected"
@@ -250,4 +251,4 @@
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
(jsx_attribute "=" @punctuation.delimiter.jsx)
-(jsx_text) @text.jsx
+(jsx_text) @text.jsx
@@ -11,6 +11,21 @@
(#set! injection.language "css"))
)
+(call_expression
+ function: (member_expression
+ object: (identifier) @_obj (#eq? @_obj "styled")
+ property: (property_identifier))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
+(call_expression
+ function: (call_expression
+ function: (identifier) @_name (#eq? @_name "styled"))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
(call_expression
function: (identifier) @_name (#eq? @_name "html")
arguments: (template_string) @injection.content
@@ -58,3 +73,9 @@
arguments: (arguments (template_string (string_fragment) @injection.content
(#set! injection.language "graphql")))
)
+
+(call_expression
+ function: (identifier) @_name(#match? @_name "^iso$")
+ arguments: (arguments (template_string (string_fragment) @injection.content
+ (#set! injection.language "isograph")))
+)
@@ -8,11 +8,11 @@ use futures::StreamExt;
use gpui::{App, AsyncApp, Task};
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
use language::{
- ContextProvider, LanguageName, LanguageRegistry, LanguageToolchainStore, LocalFile as _,
- LspAdapter, LspAdapterDelegate,
+ ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter,
+ LspAdapterDelegate, Toolchain,
};
use lsp::{LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
@@ -234,7 +234,7 @@ impl JsonLspAdapter {
schemas
.as_array_mut()
.unwrap()
- .extend(cx.all_action_names().into_iter().map(|&name| {
+ .extend(cx.all_action_names().iter().map(|&name| {
project::lsp_store::json_language_server_ext::url_schema_for_action(name)
}));
@@ -280,7 +280,7 @@ impl JsonLspAdapter {
)
})?;
writer.replace(config.clone());
- return Ok(config);
+ Ok(config)
}
}
@@ -303,7 +303,7 @@ impl LspAdapter for JsonLspAdapter {
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate
@@ -321,6 +321,7 @@ impl LspAdapter for JsonLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(
self.node
@@ -343,9 +344,8 @@ impl LspAdapter for JsonLspAdapter {
.should_install_npm_package(
Self::PACKAGE_NAME,
&server_path,
- &container_dir,
- &version,
- Default::default(),
+ container_dir,
+ VersionStrategy::Latest(version),
)
.await;
@@ -405,7 +405,7 @@ impl LspAdapter for JsonLspAdapter {
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
cx: &mut AsyncApp,
) -> Result<Value> {
let mut config = self.get_or_init_workspace_config(cx).await?;
@@ -489,12 +489,13 @@ impl NodeVersionAdapter {
#[async_trait(?Send)]
impl LspAdapter for NodeVersionAdapter {
fn name(&self) -> LanguageServerName {
- Self::SERVER_NAME.clone()
+ Self::SERVER_NAME
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release(
"zed-industries/package-version-server",
@@ -530,7 +531,7 @@ impl LspAdapter for NodeVersionAdapter {
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
@@ -1 +1,2 @@
+(comment) @comment.inclusive
(string) @string
@@ -1,6 +1,5 @@
use anyhow::Context as _;
-use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
-use gpui::{App, UpdateGlobal};
+use gpui::{App, SharedString, UpdateGlobal};
use node_runtime::NodeRuntime;
use python::PyprojectTomlManifestProvider;
use rust::CargoManifestProvider;
@@ -54,12 +53,6 @@ pub static LANGUAGE_GIT_COMMIT: std::sync::LazyLock<Arc<Language>> =
))
});
-struct BasedPyrightFeatureFlag;
-
-impl FeatureFlag for BasedPyrightFeatureFlag {
- const NAME: &'static str = "basedpyright";
-}
-
pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
#[cfg(feature = "load-grammars")]
languages.register_native_grammars([
@@ -97,14 +90,14 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
let python_context_provider = Arc::new(python::PythonContextProvider);
let python_lsp_adapter = Arc::new(python::PythonLspAdapter::new(node.clone()));
let basedpyright_lsp_adapter = Arc::new(BasedPyrightLspAdapter::new());
- let python_toolchain_provider = Arc::new(python::PythonToolchainProvider::default());
+ let python_toolchain_provider = Arc::new(python::PythonToolchainProvider);
let rust_context_provider = Arc::new(rust::RustContextProvider);
let rust_lsp_adapter = Arc::new(rust::RustLspAdapter);
let tailwind_adapter = Arc::new(tailwind::TailwindLspAdapter::new(node.clone()));
let typescript_context = Arc::new(typescript::TypeScriptContextProvider::new());
let typescript_lsp_adapter = Arc::new(typescript::TypeScriptLspAdapter::new(node.clone()));
let vtsls_adapter = Arc::new(vtsls::VtslsLspAdapter::new(node.clone()));
- let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node.clone()));
+ let yaml_lsp_adapter = Arc::new(yaml::YamlLspAdapter::new(node));
let built_in_languages = [
LanguageInfo {
@@ -119,12 +112,12 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
},
LanguageInfo {
name: "cpp",
- adapters: vec![c_lsp_adapter.clone()],
+ adapters: vec![c_lsp_adapter],
..Default::default()
},
LanguageInfo {
name: "css",
- adapters: vec![css_lsp_adapter.clone()],
+ adapters: vec![css_lsp_adapter],
..Default::default()
},
LanguageInfo {
@@ -146,20 +139,20 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
},
LanguageInfo {
name: "gowork",
- adapters: vec![go_lsp_adapter.clone()],
- context: Some(go_context_provider.clone()),
+ adapters: vec![go_lsp_adapter],
+ context: Some(go_context_provider),
..Default::default()
},
LanguageInfo {
name: "json",
- adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter.clone()],
+ adapters: vec![json_lsp_adapter.clone(), node_version_lsp_adapter],
context: Some(json_context_provider.clone()),
..Default::default()
},
LanguageInfo {
name: "jsonc",
- adapters: vec![json_lsp_adapter.clone()],
- context: Some(json_context_provider.clone()),
+ adapters: vec![json_lsp_adapter],
+ context: Some(json_context_provider),
..Default::default()
},
LanguageInfo {
@@ -174,14 +167,16 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
},
LanguageInfo {
name: "python",
- adapters: vec![python_lsp_adapter.clone(), py_lsp_adapter.clone()],
+ adapters: vec![basedpyright_lsp_adapter],
context: Some(python_context_provider),
toolchain: Some(python_toolchain_provider),
+ manifest_name: Some(SharedString::new_static("pyproject.toml").into()),
},
LanguageInfo {
name: "rust",
adapters: vec![rust_lsp_adapter],
context: Some(rust_context_provider),
+ manifest_name: Some(SharedString::new_static("Cargo.toml").into()),
..Default::default()
},
LanguageInfo {
@@ -199,7 +194,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
LanguageInfo {
name: "javascript",
adapters: vec![typescript_lsp_adapter.clone(), vtsls_adapter.clone()],
- context: Some(typescript_context.clone()),
+ context: Some(typescript_context),
..Default::default()
},
LanguageInfo {
@@ -234,23 +229,10 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
registration.adapters,
registration.context,
registration.toolchain,
+ registration.manifest_name,
);
}
- let mut basedpyright_lsp_adapter = Some(basedpyright_lsp_adapter);
- cx.observe_flag::<BasedPyrightFeatureFlag, _>({
- let languages = languages.clone();
- move |enabled, _| {
- if enabled {
- if let Some(adapter) = basedpyright_lsp_adapter.take() {
- languages
- .register_available_lsp_adapter(adapter.name(), move || adapter.clone());
- }
- }
- }
- })
- .detach();
-
// Register globally available language servers.
//
// This will allow users to add support for a built-in language server (e.g., Tailwind)
@@ -267,27 +249,19 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
// ```
languages.register_available_lsp_adapter(
LanguageServerName("tailwindcss-language-server".into()),
- {
- let adapter = tailwind_adapter.clone();
- move || adapter.clone()
- },
+ tailwind_adapter.clone(),
);
- languages.register_available_lsp_adapter(LanguageServerName("eslint".into()), {
- let adapter = eslint_adapter.clone();
- move || adapter.clone()
- });
- languages.register_available_lsp_adapter(LanguageServerName("vtsls".into()), {
- let adapter = vtsls_adapter.clone();
- move || adapter.clone()
- });
+ languages.register_available_lsp_adapter(
+ LanguageServerName("eslint".into()),
+ eslint_adapter.clone(),
+ );
+ languages.register_available_lsp_adapter(LanguageServerName("vtsls".into()), vtsls_adapter);
languages.register_available_lsp_adapter(
LanguageServerName("typescript-language-server".into()),
- {
- let adapter = typescript_lsp_adapter.clone();
- move || adapter.clone()
- },
+ typescript_lsp_adapter,
);
-
+ languages.register_available_lsp_adapter(python_lsp_adapter.name(), python_lsp_adapter);
+ languages.register_available_lsp_adapter(py_lsp_adapter.name(), py_lsp_adapter);
// Register Tailwind for the existing languages that should have it by default.
//
// This can be driven by the `language_servers` setting once we have a way for
@@ -296,6 +270,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
"Astro",
"CSS",
"ERB",
+ "HTML+ERB",
"HTML/ERB",
"HEEX",
"HTML",
@@ -340,7 +315,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
Arc::from(PyprojectTomlManifestProvider),
];
for provider in manifest_providers {
- project::ManifestProviders::global(cx).register(provider);
+ project::ManifestProvidersStore::global(cx).register(provider);
}
}
@@ -350,6 +325,7 @@ struct LanguageInfo {
adapters: Vec<Arc<dyn LspAdapter>>,
context: Option<Arc<dyn ContextProvider>>,
toolchain: Option<Arc<dyn ToolchainLister>>,
+ manifest_name: Option<ManifestName>,
}
fn register_language(
@@ -358,6 +334,7 @@ fn register_language(
adapters: Vec<Arc<dyn LspAdapter>>,
context: Option<Arc<dyn ContextProvider>>,
toolchain: Option<Arc<dyn ToolchainLister>>,
+ manifest_name: Option<ManifestName>,
) {
let config = load_config(name);
for adapter in adapters {
@@ -368,12 +345,14 @@ fn register_language(
config.grammar.clone(),
config.matcher.clone(),
config.hidden,
+ manifest_name.clone(),
Arc::new(move || {
Ok(LoadedLanguage {
config: config.clone(),
queries: load_queries(name),
context_provider: context.clone(),
toolchain_provider: toolchain.clone(),
+ manifest_name: manifest_name.clone(),
})
}),
);
@@ -1,6 +1,22 @@
-(emphasis) @emphasis
-(strong_emphasis) @emphasis.strong
-(code_span) @text.literal
-(link_text) @link_text
-(link_label) @link_text
-(link_destination) @link_uri
+(emphasis) @emphasis.markup
+(strong_emphasis) @emphasis.strong.markup
+(code_span) @text.literal.markup
+(strikethrough) @strikethrough.markup
+
+[
+ (inline_link)
+ (shortcut_link)
+ (collapsed_reference_link)
+ (full_reference_link)
+ (image)
+ (link_text)
+ (link_label)
+] @link_text.markup
+
+(inline_link ["(" ")"] @link_uri.markup)
+(image ["(" ")"] @link_uri.markup)
+[
+ (link_destination)
+ (uri_autolink)
+ (email_autolink)
+] @link_uri.markup
@@ -12,6 +12,7 @@ brackets = [
{ start = "\"", end = "\"", close = false, newline = false },
{ start = "'", end = "'", close = false, newline = false },
{ start = "`", end = "`", close = false, newline = false },
+ { start = "*", end = "*", close = false, newline = false, surround = true },
]
rewrap_prefixes = [
"[-*+]\\s+",
@@ -1,7 +1,15 @@
+[
+ (paragraph)
+ (indented_code_block)
+ (pipe_table)
+] @text
+
[
(atx_heading)
(setext_heading)
-] @title
+ (thematic_break)
+] @title.markup
+(setext_heading (paragraph) @title.markup)
[
(list_marker_plus)
@@ -9,8 +17,18 @@
(list_marker_star)
(list_marker_dot)
(list_marker_parenthesis)
-] @punctuation.list_marker
+] @punctuation.list_marker.markup
+
+(block_quote_marker) @punctuation.markup
+(pipe_table_header "|" @punctuation.markup)
+(pipe_table_row "|" @punctuation.markup)
+(pipe_table_delimiter_row "|" @punctuation.markup)
+(pipe_table_delimiter_cell "-" @punctuation.markup)
+
+[
+ (fenced_code_block_delimiter)
+ (info_string)
+] @punctuation.embedded.markup
-(fenced_code_block
- (info_string
- (language) @text.literal))
+(link_reference_definition) @link_text.markup
+(link_destination) @link_uri.markup
@@ -2,6 +2,7 @@ use anyhow::{Context as _, ensure};
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use collections::HashMap;
+use futures::AsyncBufReadExt;
use gpui::{App, Task};
use gpui::{AsyncApp, SharedString};
use language::ToolchainList;
@@ -10,13 +11,13 @@ use language::language_settings::language_settings;
use language::{ContextLocation, LanguageToolchainStore};
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
-use language::{Toolchain, WorkspaceFoldersContent};
+use language::{Toolchain, ToolchainMetadata};
use lsp::LanguageServerBinary;
use lsp::LanguageServerName;
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
use pet_core::Configuration;
use pet_core::os_environment::Environment;
-use pet_core::python_environment::PythonEnvironmentKind;
+use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind};
use project::Fs;
use project::lsp_store::language_server_settings;
use serde_json::{Value, json};
@@ -30,13 +31,11 @@ use std::{
borrow::Cow,
ffi::OsString,
fmt::Write,
- fs,
- io::{self, BufRead},
path::{Path, PathBuf},
sync::Arc,
};
-use task::{TaskTemplate, TaskTemplates, VariableName};
-use util::ResultExt;
+use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
+use util::{ResultExt, maybe};
pub(crate) struct PyprojectTomlManifestProvider;
@@ -88,6 +87,18 @@ fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
vec![server_path.into(), "--stdio".into()]
}
+/// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
+/// Where `XX` is the sorting category, `YYYY` is based on most recent usage,
+/// and `name` is the symbol name itself.
+///
+/// The problem with it is that Pyright adjusts the sort text based on previous resolutions (items for which we've issued `completion/resolve` call have their sortText adjusted),
+/// which - long story short - makes completion items list non-stable. Pyright probably relies on VSCode's implementation detail.
+/// see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
+fn process_pyright_completions(items: &mut [lsp::CompletionItem]) {
+ for item in items {
+ item.sort_text.take();
+ }
+}
pub struct PythonLspAdapter {
node: NodeRuntime,
}
@@ -103,7 +114,7 @@ impl PythonLspAdapter {
#[async_trait(?Send)]
impl LspAdapter for PythonLspAdapter {
fn name(&self) -> LanguageServerName {
- Self::SERVER_NAME.clone()
+ Self::SERVER_NAME
}
async fn initialization_options(
@@ -127,7 +138,7 @@ impl LspAdapter for PythonLspAdapter {
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await {
@@ -158,6 +169,7 @@ impl LspAdapter for PythonLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(
self.node
@@ -204,9 +216,8 @@ impl LspAdapter for PythonLspAdapter {
.should_install_npm_package(
Self::SERVER_NAME.as_ref(),
&server_path,
- &container_dir,
- &version,
- Default::default(),
+ container_dir,
+ VersionStrategy::Latest(version),
)
.await;
@@ -233,26 +244,7 @@ impl LspAdapter for PythonLspAdapter {
}
async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
- // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
- // Where `XX` is the sorting category, `YYYY` is based on most recent usage,
- // and `name` is the symbol name itself.
- //
- // Because the symbol name is included, there generally are not ties when
- // sorting by the `sortText`, so the symbol's fuzzy match score is not taken
- // into account. Here, we remove the symbol name from the sortText in order
- // to allow our own fuzzy score to be used to break ties.
- //
- // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
- for item in items {
- let Some(sort_text) = &mut item.sort_text else {
- continue;
- };
- let mut parts = sort_text.split('.');
- let Some(first) = parts.next() else { continue };
- let Some(second) = parts.next() else { continue };
- let Some(_) = parts.next() else { continue };
- sort_text.replace_range(first.len() + second.len() + 1.., "");
- }
+ process_pyright_completions(items);
}
async fn label_for_completion(
@@ -263,20 +255,34 @@ impl LspAdapter for PythonLspAdapter {
let label = &item.label;
let grammar = language.grammar()?;
let highlight_id = match item.kind? {
- lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
- lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
- lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
- lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
- _ => return None,
+ lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
+ lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),
+ lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),
+ lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),
+ lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),
+ _ => {
+ return None;
+ }
};
let filter_range = item
.filter_text
.as_deref()
.and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..label.len());
+ let mut text = label.clone();
+ if let Some(completion_details) = item
+ .label_details
+ .as_ref()
+ .and_then(|details| details.description.as_ref())
+ {
+ write!(&mut text, " {}", completion_details).ok();
+ }
Some(language::CodeLabel {
- text: label.clone(),
- runs: vec![(0..label.len(), highlight_id)],
+ runs: highlight_id
+ .map(|id| (0..label.len(), id))
+ .into_iter()
+ .collect(),
+ text,
filter_range,
})
}
@@ -320,17 +326,9 @@ impl LspAdapter for PythonLspAdapter {
self: Arc<Self>,
_: &dyn Fs,
adapter: &Arc<dyn LspAdapterDelegate>,
- toolchains: Arc<dyn LanguageToolchainStore>,
+ toolchain: Option<Toolchain>,
cx: &mut AsyncApp,
) -> Result<Value> {
- let toolchain = toolchains
- .active_toolchain(
- adapter.worktree_id(),
- Arc::from("".as_ref()),
- LanguageName::new("Python"),
- cx,
- )
- .await;
cx.update(move |cx| {
let mut user_settings =
language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
@@ -338,41 +336,35 @@ impl LspAdapter for PythonLspAdapter {
.unwrap_or_default();
// If we have a detected toolchain, configure Pyright to use it
- if let Some(toolchain) = toolchain {
+ if let Some(toolchain) = toolchain
+ && let Ok(env) = serde_json::from_value::<
+ pet_core::python_environment::PythonEnvironment,
+ >(toolchain.as_json.clone())
+ {
if user_settings.is_null() {
user_settings = Value::Object(serde_json::Map::default());
}
let object = user_settings.as_object_mut().unwrap();
let interpreter_path = toolchain.path.to_string();
+ if let Some(venv_dir) = env.prefix {
+ // Set venvPath and venv at the root level
+ // This matches the format of a pyrightconfig.json file
+ if let Some(parent) = venv_dir.parent() {
+ // Use relative path if the venv is inside the workspace
+ let venv_path = if parent == adapter.worktree_root_path() {
+ ".".to_string()
+ } else {
+ parent.to_string_lossy().into_owned()
+ };
+ object.insert("venvPath".to_string(), Value::String(venv_path));
+ }
- // Detect if this is a virtual environment
- if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() {
- if let Some(venv_dir) = interpreter_dir.parent() {
- // Check if this looks like a virtual environment
- if venv_dir.join("pyvenv.cfg").exists()
- || venv_dir.join("bin/activate").exists()
- || venv_dir.join("Scripts/activate.bat").exists()
- {
- // Set venvPath and venv at the root level
- // This matches the format of a pyrightconfig.json file
- if let Some(parent) = venv_dir.parent() {
- // Use relative path if the venv is inside the workspace
- let venv_path = if parent == adapter.worktree_root_path() {
- ".".to_string()
- } else {
- parent.to_string_lossy().into_owned()
- };
- object.insert("venvPath".to_string(), Value::String(venv_path));
- }
-
- if let Some(venv_name) = venv_dir.file_name() {
- object.insert(
- "venv".to_owned(),
- Value::String(venv_name.to_string_lossy().into_owned()),
- );
- }
- }
+ if let Some(venv_name) = venv_dir.file_name() {
+ object.insert(
+ "venv".to_owned(),
+ Value::String(venv_name.to_string_lossy().into_owned()),
+ );
}
}
@@ -398,12 +390,6 @@ impl LspAdapter for PythonLspAdapter {
user_settings
})
}
- fn manifest_name(&self) -> Option<ManifestName> {
- Some(SharedString::new_static("pyproject.toml").into())
- }
- fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
- WorkspaceFoldersContent::WorktreeRoot
- }
}
async fn get_cached_server_binary(
@@ -431,9 +417,6 @@ const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
-const PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW: VariableName =
- VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN_RAW"));
-
const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME"));
@@ -457,7 +440,7 @@ impl ContextProvider for PythonContextProvider {
let worktree_id = location_file.as_ref().map(|f| f.worktree_id(cx));
cx.spawn(async move |cx| {
- let raw_toolchain = if let Some(worktree_id) = worktree_id {
+ let active_toolchain = if let Some(worktree_id) = worktree_id {
let file_path = location_file
.as_ref()
.and_then(|f| f.path().parent())
@@ -475,15 +458,13 @@ impl ContextProvider for PythonContextProvider {
String::from("python3")
};
- let active_toolchain = format!("\"{raw_toolchain}\"");
let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
- let raw_toolchain_var = (PYTHON_ACTIVE_TOOLCHAIN_PATH_RAW, raw_toolchain);
Ok(task::TaskVariables::from_iter(
test_target
.into_iter()
.chain(module_target.into_iter())
- .chain([toolchain, raw_toolchain_var]),
+ .chain([toolchain]),
))
})
}
@@ -500,31 +481,31 @@ impl ContextProvider for PythonContextProvider {
// Execute a selection
TaskTemplate {
label: "execute selection".to_owned(),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
args: vec![
"-c".to_owned(),
VariableName::SelectedText.template_value_with_whitespace(),
],
- cwd: Some("$ZED_WORKTREE_ROOT".into()),
+ cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
},
// Execute an entire file
TaskTemplate {
label: format!("run '{}'", VariableName::File.template_value()),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
args: vec![VariableName::File.template_value_with_whitespace()],
- cwd: Some("$ZED_WORKTREE_ROOT".into()),
+ cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
},
// Execute a file as module
TaskTemplate {
label: format!("run module '{}'", VariableName::File.template_value()),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
args: vec![
"-m".to_owned(),
- PYTHON_MODULE_NAME_TASK_VARIABLE.template_value(),
+ PYTHON_MODULE_NAME_TASK_VARIABLE.template_value_with_whitespace(),
],
- cwd: Some("$ZED_WORKTREE_ROOT".into()),
+ cwd: Some(VariableName::WorktreeRoot.template_value()),
tags: vec!["python-module-main-method".to_owned()],
..TaskTemplate::default()
},
@@ -536,19 +517,19 @@ impl ContextProvider for PythonContextProvider {
// Run tests for an entire file
TaskTemplate {
label: format!("unittest '{}'", VariableName::File.template_value()),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
VariableName::File.template_value_with_whitespace(),
],
- cwd: Some("$ZED_WORKTREE_ROOT".into()),
+ cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
},
// Run test(s) for a specific target within a file
TaskTemplate {
label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
args: vec![
"-m".to_owned(),
"unittest".to_owned(),
@@ -558,7 +539,7 @@ impl ContextProvider for PythonContextProvider {
"python-unittest-class".to_owned(),
"python-unittest-method".to_owned(),
],
- cwd: Some("$ZED_WORKTREE_ROOT".into()),
+ cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
},
]
@@ -568,25 +549,25 @@ impl ContextProvider for PythonContextProvider {
// Run tests for an entire file
TaskTemplate {
label: format!("pytest '{}'", VariableName::File.template_value()),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
args: vec![
"-m".to_owned(),
"pytest".to_owned(),
VariableName::File.template_value_with_whitespace(),
],
- cwd: Some("$ZED_WORKTREE_ROOT".into()),
+ cwd: Some(VariableName::WorktreeRoot.template_value()),
..TaskTemplate::default()
},
// Run test(s) for a specific target within a file
TaskTemplate {
label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
- command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
+ command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value_with_whitespace(),
args: vec![
"-m".to_owned(),
"pytest".to_owned(),
PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
],
- cwd: Some("$ZED_WORKTREE_ROOT".into()),
+ cwd: Some(VariableName::WorktreeRoot.template_value()),
tags: vec![
"python-pytest-class".to_owned(),
"python-pytest-method".to_owned(),
@@ -714,19 +695,9 @@ fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str {
}
}
-pub(crate) struct PythonToolchainProvider {
- term: SharedString,
-}
-
-impl Default for PythonToolchainProvider {
- fn default() -> Self {
- Self {
- term: SharedString::new_static("Virtual Environment"),
- }
- }
-}
+pub(crate) struct PythonToolchainProvider;
-static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[
+static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[
// Prioritize non-Conda environments.
PythonEnvironmentKind::Poetry,
PythonEnvironmentKind::Pipenv,
@@ -756,25 +727,24 @@ fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
/// Return the name of environment declared in <worktree-root/.venv.
///
/// https://virtualfish.readthedocs.io/en/latest/plugins.html#auto-activation-auto-activation
-fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
- fs::File::open(worktree_root.join(".venv"))
- .and_then(|file| {
- let mut venv_name = String::new();
- io::BufReader::new(file).read_line(&mut venv_name)?;
- Ok(venv_name.trim().to_string())
- })
- .ok()
+async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
+ let file = async_fs::File::open(worktree_root.join(".venv"))
+ .await
+ .ok()?;
+ let mut venv_name = String::new();
+ smol::io::BufReader::new(file)
+ .read_line(&mut venv_name)
+ .await
+ .ok()?;
+ Some(venv_name.trim().to_string())
}
#[async_trait]
impl ToolchainLister for PythonToolchainProvider {
- fn manifest_name(&self) -> language::ManifestName {
- ManifestName::from(SharedString::new_static("pyproject.toml"))
- }
async fn list(
&self,
worktree_root: PathBuf,
- subroot_relative_path: Option<Arc<Path>>,
+ subroot_relative_path: Arc<Path>,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList {
let env = project_env.unwrap_or_default();
@@ -786,13 +756,15 @@ impl ToolchainLister for PythonToolchainProvider {
);
let mut config = Configuration::default();
- let mut directories = vec![worktree_root.clone()];
- if let Some(subroot_relative_path) = subroot_relative_path {
- debug_assert!(subroot_relative_path.is_relative());
- directories.push(worktree_root.join(subroot_relative_path));
- }
-
- config.workspace_directories = Some(directories);
+ debug_assert!(subroot_relative_path.is_relative());
+ // `.ancestors()` will yield at least one path, so in case of empty `subroot_relative_path`, we'll just use
+ // worktree root as the workspace directory.
+ config.workspace_directories = Some(
+ subroot_relative_path
+ .ancestors()
+ .map(|ancestor| worktree_root.join(ancestor))
+ .collect(),
+ );
for locator in locators.iter() {
locator.configure(&config);
}
@@ -806,7 +778,7 @@ impl ToolchainLister for PythonToolchainProvider {
.map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
let wr = worktree_root;
- let wr_venv = get_worktree_venv_declaration(&wr);
+ let wr_venv = get_worktree_venv_declaration(&wr).await;
// Sort detected environments by:
// environment name matching activation file (<workdir>/.venv)
// environment project dir matching worktree_root
@@ -843,7 +815,7 @@ impl ToolchainLister for PythonToolchainProvider {
.get_env_var("CONDA_PREFIX".to_string())
.map(|conda_prefix| {
let is_match = |exe: &Option<PathBuf>| {
- exe.as_ref().map_or(false, |e| e.starts_with(&conda_prefix))
+ exe.as_ref().is_some_and(|e| e.starts_with(&conda_prefix))
};
match (is_match(&lhs.executable), is_match(&rhs.executable)) {
(true, false) => Ordering::Less,
@@ -869,32 +841,7 @@ impl ToolchainLister for PythonToolchainProvider {
let mut toolchains: Vec<_> = toolchains
.into_iter()
- .filter_map(|toolchain| {
- let mut name = String::from("Python");
- if let Some(ref version) = toolchain.version {
- _ = write!(name, " {version}");
- }
-
- let name_and_kind = match (&toolchain.name, &toolchain.kind) {
- (Some(name), Some(kind)) => {
- Some(format!("({name}; {})", python_env_kind_display(kind)))
- }
- (Some(name), None) => Some(format!("({name})")),
- (None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
- (None, None) => None,
- };
-
- if let Some(nk) = name_and_kind {
- _ = write!(name, " {nk}");
- }
-
- Some(Toolchain {
- name: name.into(),
- path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
- language_name: LanguageName::new("Python"),
- as_json: serde_json::to_value(toolchain).ok()?,
- })
- })
+ .filter_map(venv_to_toolchain)
.collect();
toolchains.dedup();
ToolchainList {
@@ -903,9 +850,125 @@ impl ToolchainLister for PythonToolchainProvider {
groups: Default::default(),
}
}
- fn term(&self) -> SharedString {
- self.term.clone()
+ fn meta(&self) -> ToolchainMetadata {
+ ToolchainMetadata {
+ term: SharedString::new_static("Virtual Environment"),
+ new_toolchain_placeholder: SharedString::new_static(
+ "A path to the python3 executable within a virtual environment, or path to virtual environment itself",
+ ),
+ manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
+ }
+ }
+
+ async fn resolve(
+ &self,
+ path: PathBuf,
+ env: Option<HashMap<String, String>>,
+ ) -> anyhow::Result<Toolchain> {
+ let env = env.unwrap_or_default();
+ let environment = EnvironmentApi::from_env(&env);
+ let locators = pet::locators::create_locators(
+ Arc::new(pet_conda::Conda::from(&environment)),
+ Arc::new(pet_poetry::Poetry::from(&environment)),
+ &environment,
+ );
+ let toolchain = pet::resolve::resolve_environment(&path, &locators, &environment)
+ .context("Could not find a virtual environment in provided path")?;
+ let venv = toolchain.resolved.unwrap_or(toolchain.discovered);
+ venv_to_toolchain(venv).context("Could not convert a venv into a toolchain")
}
+
+ async fn activation_script(
+ &self,
+ toolchain: &Toolchain,
+ shell: ShellKind,
+ fs: &dyn Fs,
+ ) -> Vec<String> {
+ let Ok(toolchain) = serde_json::from_value::<pet_core::python_environment::PythonEnvironment>(
+ toolchain.as_json.clone(),
+ ) else {
+ return vec![];
+ };
+ let mut activation_script = vec![];
+
+ match toolchain.kind {
+ Some(PythonEnvironmentKind::Conda) => {
+ if let Some(name) = &toolchain.name {
+ activation_script.push(format!("conda activate {name}"));
+ } else {
+ activation_script.push("conda activate".to_string());
+ }
+ }
+ Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
+ if let Some(prefix) = &toolchain.prefix {
+ let activate_keyword = match shell {
+ ShellKind::Cmd => ".",
+ ShellKind::Nushell => "overlay use",
+ ShellKind::Powershell => ".",
+ ShellKind::Fish => "source",
+ ShellKind::Csh => "source",
+ ShellKind::Posix => "source",
+ };
+ let activate_script_name = match shell {
+ ShellKind::Posix => "activate",
+ ShellKind::Csh => "activate.csh",
+ ShellKind::Fish => "activate.fish",
+ ShellKind::Nushell => "activate.nu",
+ ShellKind::Powershell => "activate.ps1",
+ ShellKind::Cmd => "activate.bat",
+ };
+ let path = prefix.join(BINARY_DIR).join(activate_script_name);
+ if fs.is_file(&path).await {
+ activation_script
+ .push(format!("{activate_keyword} \"{}\"", path.display()));
+ }
+ }
+ }
+ Some(PythonEnvironmentKind::Pyenv) => {
+ let Some(manager) = toolchain.manager else {
+ return vec![];
+ };
+ let version = toolchain.version.as_deref().unwrap_or("system");
+ let pyenv = manager.executable;
+ let pyenv = pyenv.display();
+ activation_script.extend(match shell {
+ ShellKind::Fish => Some(format!("\"{pyenv}\" shell - fish {version}")),
+ ShellKind::Posix => Some(format!("\"{pyenv}\" shell - sh {version}")),
+ ShellKind::Nushell => Some(format!("\"{pyenv}\" shell - nu {version}")),
+ ShellKind::Powershell => None,
+ ShellKind::Csh => None,
+ ShellKind::Cmd => None,
+ })
+ }
+ _ => {}
+ }
+ activation_script
+ }
+}
+
+fn venv_to_toolchain(venv: PythonEnvironment) -> Option<Toolchain> {
+ let mut name = String::from("Python");
+ if let Some(ref version) = venv.version {
+ _ = write!(name, " {version}");
+ }
+
+ let name_and_kind = match (&venv.name, &venv.kind) {
+ (Some(name), Some(kind)) => Some(format!("({name}; {})", python_env_kind_display(kind))),
+ (Some(name), None) => Some(format!("({name})")),
+ (None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
+ (None, None) => None,
+ };
+
+ if let Some(nk) = name_and_kind {
+ _ = write!(name, " {nk}");
+ }
+
+ Some(Toolchain {
+ name: name.into(),
+ path: venv.executable.as_ref()?.to_str()?.to_owned().into(),
+ language_name: LanguageName::new("Python"),
+ as_json: serde_json::to_value(venv).ok()?,
+ })
}
pub struct EnvironmentApi<'a> {
@@ -1041,14 +1104,14 @@ const BINARY_DIR: &str = if cfg!(target_os = "windows") {
#[async_trait(?Send)]
impl LspAdapter for PyLspAdapter {
fn name(&self) -> LanguageServerName {
- Self::SERVER_NAME.clone()
+ Self::SERVER_NAME
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- toolchains: Arc<dyn LanguageToolchainStore>,
- cx: &AsyncApp,
+ toolchain: Option<Toolchain>,
+ _: &AsyncApp,
) -> Option<LanguageServerBinary> {
if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await {
let env = delegate.shell_env().await;
@@ -1058,17 +1121,10 @@ impl LspAdapter for PyLspAdapter {
arguments: vec![],
})
} else {
- let venv = toolchains
- .active_toolchain(
- delegate.worktree_id(),
- Arc::from("".as_ref()),
- LanguageName::new("Python"),
- &mut cx.clone(),
- )
- .await?;
- let pylsp_path = Path::new(venv.path.as_ref()).parent()?.join("pylsp");
+ let toolchain = toolchain?;
+ let pylsp_path = Path::new(toolchain.path.as_ref()).parent()?.join("pylsp");
pylsp_path.exists().then(|| LanguageServerBinary {
- path: venv.path.to_string().into(),
+ path: toolchain.path.to_string().into(),
arguments: vec![pylsp_path.into()],
env: None,
})
@@ -1078,6 +1134,7 @@ impl LspAdapter for PyLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(()) as Box<_>)
}
@@ -1212,17 +1269,9 @@ impl LspAdapter for PyLspAdapter {
self: Arc<Self>,
_: &dyn Fs,
adapter: &Arc<dyn LspAdapterDelegate>,
- toolchains: Arc<dyn LanguageToolchainStore>,
+ toolchain: Option<Toolchain>,
cx: &mut AsyncApp,
) -> Result<Value> {
- let toolchain = toolchains
- .active_toolchain(
- adapter.worktree_id(),
- Arc::from("".as_ref()),
- LanguageName::new("Python"),
- cx,
- )
- .await;
cx.update(move |cx| {
let mut user_settings =
language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
@@ -1283,12 +1332,6 @@ impl LspAdapter for PyLspAdapter {
user_settings
})
}
- fn manifest_name(&self) -> Option<ManifestName> {
- Some(SharedString::new_static("pyproject.toml").into())
- }
- fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
- WorkspaceFoldersContent::WorktreeRoot
- }
}
pub(crate) struct BasedPyrightLspAdapter {
@@ -1354,7 +1397,7 @@ impl BasedPyrightLspAdapter {
#[async_trait(?Send)]
impl LspAdapter for BasedPyrightLspAdapter {
fn name(&self) -> LanguageServerName {
- Self::SERVER_NAME.clone()
+ Self::SERVER_NAME
}
async fn initialization_options(
@@ -1378,8 +1421,8 @@ impl LspAdapter for BasedPyrightLspAdapter {
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- toolchains: Arc<dyn LanguageToolchainStore>,
- cx: &AsyncApp,
+ toolchain: Option<Toolchain>,
+ _: &AsyncApp,
) -> Option<LanguageServerBinary> {
if let Some(bin) = delegate.which(Self::BINARY_NAME.as_ref()).await {
let env = delegate.shell_env().await;
@@ -1389,15 +1432,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
arguments: vec!["--stdio".into()],
})
} else {
- let venv = toolchains
- .active_toolchain(
- delegate.worktree_id(),
- Arc::from("".as_ref()),
- LanguageName::new("Python"),
- &mut cx.clone(),
- )
- .await?;
- let path = Path::new(venv.path.as_ref())
+ let path = Path::new(toolchain?.path.as_ref())
.parent()?
.join(Self::BINARY_NAME);
path.exists().then(|| LanguageServerBinary {
@@ -1411,6 +1446,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(()) as Box<_>)
}
@@ -1457,26 +1493,7 @@ impl LspAdapter for BasedPyrightLspAdapter {
}
async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
- // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
- // Where `XX` is the sorting category, `YYYY` is based on most recent usage,
- // and `name` is the symbol name itself.
- //
- // Because the symbol name is included, there generally are not ties when
- // sorting by the `sortText`, so the symbol's fuzzy match score is not taken
- // into account. Here, we remove the symbol name from the sortText in order
- // to allow our own fuzzy score to be used to break ties.
- //
- // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
- for item in items {
- let Some(sort_text) = &mut item.sort_text else {
- continue;
- };
- let mut parts = sort_text.split('.');
- let Some(first) = parts.next() else { continue };
- let Some(second) = parts.next() else { continue };
- let Some(_) = parts.next() else { continue };
- sort_text.replace_range(first.len() + second.len() + 1.., "");
- }
+ process_pyright_completions(items);
}
async fn label_for_completion(
@@ -1487,20 +1504,34 @@ impl LspAdapter for BasedPyrightLspAdapter {
let label = &item.label;
let grammar = language.grammar()?;
let highlight_id = match item.kind? {
- lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
- lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
- lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
- lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
- _ => return None,
+ lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
+ lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),
+ lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),
+ lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),
+ lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),
+ _ => {
+ return None;
+ }
};
let filter_range = item
.filter_text
.as_deref()
.and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..label.len());
+ let mut text = label.clone();
+ if let Some(completion_details) = item
+ .label_details
+ .as_ref()
+ .and_then(|details| details.description.as_ref())
+ {
+ write!(&mut text, " {}", completion_details).ok();
+ }
Some(language::CodeLabel {
- text: label.clone(),
- runs: vec![(0..label.len(), highlight_id)],
+ runs: highlight_id
+ .map(|id| (0..label.len(), id))
+ .into_iter()
+ .collect(),
+ text,
filter_range,
})
}
@@ -1544,17 +1575,9 @@ impl LspAdapter for BasedPyrightLspAdapter {
self: Arc<Self>,
_: &dyn Fs,
adapter: &Arc<dyn LspAdapterDelegate>,
- toolchains: Arc<dyn LanguageToolchainStore>,
+ toolchain: Option<Toolchain>,
cx: &mut AsyncApp,
) -> Result<Value> {
- let toolchain = toolchains
- .active_toolchain(
- adapter.worktree_id(),
- Arc::from("".as_ref()),
- LanguageName::new("Python"),
- cx,
- )
- .await;
cx.update(move |cx| {
let mut user_settings =
language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
@@ -1562,74 +1585,74 @@ impl LspAdapter for BasedPyrightLspAdapter {
.unwrap_or_default();
// If we have a detected toolchain, configure Pyright to use it
- if let Some(toolchain) = toolchain {
+ if let Some(toolchain) = toolchain
+ && let Ok(env) = serde_json::from_value::<
+ pet_core::python_environment::PythonEnvironment,
+ >(toolchain.as_json.clone())
+ {
if user_settings.is_null() {
user_settings = Value::Object(serde_json::Map::default());
}
let object = user_settings.as_object_mut().unwrap();
let interpreter_path = toolchain.path.to_string();
+ if let Some(venv_dir) = env.prefix {
+ // Set venvPath and venv at the root level
+ // This matches the format of a pyrightconfig.json file
+ if let Some(parent) = venv_dir.parent() {
+ // Use relative path if the venv is inside the workspace
+ let venv_path = if parent == adapter.worktree_root_path() {
+ ".".to_string()
+ } else {
+ parent.to_string_lossy().into_owned()
+ };
+ object.insert("venvPath".to_string(), Value::String(venv_path));
+ }
- // Detect if this is a virtual environment
- if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() {
- if let Some(venv_dir) = interpreter_dir.parent() {
- // Check if this looks like a virtual environment
- if venv_dir.join("pyvenv.cfg").exists()
- || venv_dir.join("bin/activate").exists()
- || venv_dir.join("Scripts/activate.bat").exists()
- {
- // Set venvPath and venv at the root level
- // This matches the format of a pyrightconfig.json file
- if let Some(parent) = venv_dir.parent() {
- // Use relative path if the venv is inside the workspace
- let venv_path = if parent == adapter.worktree_root_path() {
- ".".to_string()
- } else {
- parent.to_string_lossy().into_owned()
- };
- object.insert("venvPath".to_string(), Value::String(venv_path));
- }
-
- if let Some(venv_name) = venv_dir.file_name() {
- object.insert(
- "venv".to_owned(),
- Value::String(venv_name.to_string_lossy().into_owned()),
- );
- }
- }
+ if let Some(venv_name) = venv_dir.file_name() {
+ object.insert(
+ "venv".to_owned(),
+ Value::String(venv_name.to_string_lossy().into_owned()),
+ );
}
}
- // Always set the python interpreter path
- // Get or create the python section
- let python = object
+ // Set both pythonPath and defaultInterpreterPath for compatibility
+ if let Some(python) = object
.entry("python")
.or_insert(Value::Object(serde_json::Map::default()))
.as_object_mut()
- .unwrap();
-
- // Set both pythonPath and defaultInterpreterPath for compatibility
- python.insert(
- "pythonPath".to_owned(),
- Value::String(interpreter_path.clone()),
- );
- python.insert(
- "defaultInterpreterPath".to_owned(),
- Value::String(interpreter_path),
- );
+ {
+ python.insert(
+ "pythonPath".to_owned(),
+ Value::String(interpreter_path.clone()),
+ );
+ python.insert(
+ "defaultInterpreterPath".to_owned(),
+ Value::String(interpreter_path),
+ );
+ }
+ // Basedpyright by default uses `strict` type checking, we tone it down as to not surpris users
+ maybe!({
+ let basedpyright = object
+ .entry("basedpyright")
+ .or_insert(Value::Object(serde_json::Map::default()));
+ let analysis = basedpyright
+ .as_object_mut()?
+ .entry("analysis")
+ .or_insert(Value::Object(serde_json::Map::default()));
+ if let serde_json::map::Entry::Vacant(v) =
+ analysis.as_object_mut()?.entry("typeCheckingMode")
+ {
+ v.insert(Value::String("standard".to_owned()));
+ }
+ Some(())
+ });
}
user_settings
})
}
-
- fn manifest_name(&self) -> Option<ManifestName> {
- Some(SharedString::new_static("pyproject.toml").into())
- }
-
- fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
- WorkspaceFoldersContent::WorktreeRoot
- }
}
#[cfg(test)]
@@ -106,17 +106,13 @@ impl ManifestProvider for CargoManifestProvider {
#[async_trait(?Send)]
impl LspAdapter for RustLspAdapter {
fn name(&self) -> LanguageServerName {
- SERVER_NAME.clone()
- }
-
- fn manifest_name(&self) -> Option<ManifestName> {
- Some(SharedString::new_static("Cargo.toml").into())
+ SERVER_NAME
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which("rust-analyzer".as_ref()).await?;
@@ -151,11 +147,16 @@ impl LspAdapter for RustLspAdapter {
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
+ cx: &AsyncApp,
) -> Result<Box<dyn 'static + Send + Any>> {
let release = latest_github_release(
"rust-lang/rust-analyzer",
true,
- false,
+ ProjectSettings::try_read_global(cx, |s| {
+ s.lsp.get(&SERVER_NAME)?.fetch.as_ref()?.pre_release
+ })
+ .flatten()
+ .unwrap_or(false),
delegate.http_client(),
)
.await?;
@@ -407,7 +408,7 @@ impl LspAdapter for RustLspAdapter {
} else if completion
.detail
.as_ref()
- .map_or(false, |detail| detail.starts_with("macro_rules! "))
+ .is_some_and(|detail| detail.starts_with("macro_rules! "))
{
let text = completion.label.clone();
let len = text.len();
@@ -500,7 +501,7 @@ impl LspAdapter for RustLspAdapter {
let enable_lsp_tasks = ProjectSettings::get_global(cx)
.lsp
.get(&SERVER_NAME)
- .map_or(false, |s| s.enable_lsp_tasks);
+ .is_some_and(|s| s.enable_lsp_tasks);
if enable_lsp_tasks {
let experimental = json!({
"runnables": {
@@ -514,20 +515,6 @@ impl LspAdapter for RustLspAdapter {
}
}
- let cargo_diagnostics_fetched_separately = ProjectSettings::get_global(cx)
- .diagnostics
- .fetch_cargo_diagnostics();
- if cargo_diagnostics_fetched_separately {
- let disable_check_on_save = json!({
- "checkOnSave": false,
- });
- if let Some(initialization_options) = &mut original.initialization_options {
- merge_json_value_into(disable_check_on_save, initialization_options);
- } else {
- original.initialization_options = Some(disable_check_on_save);
- }
- }
-
Ok(original)
}
}
@@ -585,7 +572,7 @@ impl ContextProvider for RustContextProvider {
if let (Some(path), Some(stem)) = (&local_abs_path, task_variables.get(&VariableName::Stem))
{
- let fragment = test_fragment(&variables, &path, stem);
+ let fragment = test_fragment(&variables, path, stem);
variables.insert(RUST_TEST_FRAGMENT_TASK_VARIABLE, fragment);
};
if let Some(test_name) =
@@ -602,16 +589,14 @@ impl ContextProvider for RustContextProvider {
if let Some(path) = local_abs_path
.as_deref()
.and_then(|local_abs_path| local_abs_path.parent())
- {
- if let Some(package_name) =
+ && let Some(package_name) =
human_readable_package_name(path, project_env.as_ref()).await
- {
- variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
- }
+ {
+ variables.insert(RUST_PACKAGE_TASK_VARIABLE.clone(), package_name);
}
if let Some(path) = local_abs_path.as_ref()
&& let Some((target, manifest_path)) =
- target_info_from_abs_path(&path, project_env.as_ref()).await
+ target_info_from_abs_path(path, project_env.as_ref()).await
{
if let Some(target) = target {
variables.extend(TaskVariables::from_iter([
@@ -665,7 +650,7 @@ impl ContextProvider for RustContextProvider {
.variables
.get(CUSTOM_TARGET_DIR)
.cloned();
- let run_task_args = if let Some(package_to_run) = package_to_run.clone() {
+ let run_task_args = if let Some(package_to_run) = package_to_run {
vec!["run".into(), "-p".into(), package_to_run]
} else {
vec!["run".into()]
@@ -1025,8 +1010,8 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
let path = last.context("no cached binary")?;
let path = match RustLspAdapter::GITHUB_ASSET_KIND {
- AssetKind::TarGz | AssetKind::Gz => path.clone(), // Tar and gzip extract in place.
- AssetKind::Zip => path.clone().join("rust-analyzer.exe"), // zip contains a .exe
+ AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
+ AssetKind::Zip => path.join("rust-analyzer.exe"), // zip contains a .exe
};
anyhow::Ok(LanguageServerBinary {
@@ -1078,7 +1063,7 @@ mod tests {
#[gpui::test]
async fn test_process_rust_diagnostics() {
let mut params = lsp::PublishDiagnosticsParams {
- uri: lsp::Url::from_file_path(path!("/a")).unwrap(),
+ uri: lsp::Uri::from_file_path(path!("/a")).unwrap(),
version: None,
diagnostics: vec![
// no newlines
@@ -1574,7 +1559,7 @@ mod tests {
let found = test_fragment(
&TaskVariables::from_iter(variables.into_iter().map(|(k, v)| (k, v.to_owned()))),
path,
- &path.file_stem().unwrap().to_str().unwrap(),
+ path.file_stem().unwrap().to_str().unwrap(),
);
assert_eq!(expected, found);
}
@@ -5,6 +5,7 @@
(primitive_type) @type.builtin
(self) @variable.special
(field_identifier) @property
+(shorthand_field_identifier) @property
(trait_item name: (type_identifier) @type.interface)
(impl_item trait: (type_identifier) @type.interface)
@@ -195,12 +196,13 @@ operator: "/" @operator
(attribute_item (attribute [
(identifier) @attribute
(scoped_identifier name: (identifier) @attribute)
+ (token_tree (identifier) @attribute (#match? @attribute "^[a-z\\d_]*$"))
+ (token_tree (identifier) @none "::" (#match? @none "^[a-z\\d_]*$"))
]))
+
(inner_attribute_item (attribute [
(identifier) @attribute
(scoped_identifier name: (identifier) @attribute)
+ (token_tree (identifier) @attribute (#match? @attribute "^[a-z\\d_]*$"))
+ (token_tree (identifier) @none "::" (#match? @none "^[a-z\\d_]*$"))
]))
-; Match nested snake case identifiers in attribute items.
-(token_tree (identifier) @attribute (#match? @attribute "^[a-z\\d_]*$"))
-; Override the attribute match for paths in scoped type/enum identifiers.
-(token_tree (identifier) @variable "::" (identifier) @type (#match? @type "^[A-Z]"))
@@ -3,9 +3,9 @@ use async_trait::async_trait;
use collections::HashMap;
use futures::StreamExt;
use gpui::AsyncApp;
-use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
+use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain};
use lsp::{LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use smol::fs;
@@ -44,13 +44,13 @@ impl TailwindLspAdapter {
#[async_trait(?Send)]
impl LspAdapter for TailwindLspAdapter {
fn name(&self) -> LanguageServerName {
- Self::SERVER_NAME.clone()
+ Self::SERVER_NAME
}
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
@@ -66,6 +66,7 @@ impl LspAdapter for TailwindLspAdapter {
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(
self.node
@@ -111,9 +112,8 @@ impl LspAdapter for TailwindLspAdapter {
.should_install_npm_package(
Self::PACKAGE_NAME,
&server_path,
- &container_dir,
- &version,
- Default::default(),
+ container_dir,
+ VersionStrategy::Latest(version),
)
.await;
@@ -156,7 +156,7 @@ impl LspAdapter for TailwindLspAdapter {
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
cx: &mut AsyncApp,
) -> Result<Value> {
let mut tailwind_user_settings = cx.update(|cx| {
@@ -185,6 +185,7 @@ impl LspAdapter for TailwindLspAdapter {
(LanguageName::new("Elixir"), "phoenix-heex".to_string()),
(LanguageName::new("HEEX"), "phoenix-heex".to_string()),
(LanguageName::new("ERB"), "erb".to_string()),
+ (LanguageName::new("HTML+ERB"), "erb".to_string()),
(LanguageName::new("HTML/ERB"), "erb".to_string()),
(LanguageName::new("PHP"), "php".to_string()),
(LanguageName::new("Vue.js"), "vue".to_string()),
@@ -4,6 +4,7 @@ path_suffixes = ["tsx"]
line_comments = ["// "]
block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }
documentation_comment = { start = "/**", prefix = "* ", end = "*/", tab_size = 1 }
+wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "</", end_suffix = ">" }
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
@@ -237,6 +237,7 @@
"implements"
"interface"
"keyof"
+ "module"
"namespace"
"private"
"protected"
@@ -256,4 +257,4 @@
(jsx_closing_element (["</" ">"]) @punctuation.bracket.jsx)
(jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx)
(jsx_attribute "=" @punctuation.delimiter.jsx)
-(jsx_text) @text.jsx
+(jsx_text) @text.jsx
@@ -11,6 +11,21 @@
(#set! injection.language "css"))
)
+(call_expression
+ function: (member_expression
+ object: (identifier) @_obj (#eq? @_obj "styled")
+ property: (property_identifier))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
+(call_expression
+ function: (call_expression
+ function: (identifier) @_name (#eq? @_name "styled"))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
(call_expression
function: (identifier) @_name (#eq? @_name "html")
arguments: (template_string (string_fragment) @injection.content
@@ -58,3 +73,9 @@
arguments: (arguments (template_string (string_fragment) @injection.content
(#set! injection.language "graphql")))
)
+
+(call_expression
+ function: (identifier) @_name(#match? @_name "^iso$")
+ arguments: (arguments (template_string (string_fragment) @injection.content
+ (#set! injection.language "isograph")))
+)
@@ -7,10 +7,10 @@ use gpui::{App, AppContext, AsyncApp, Task};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
use language::{
ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter,
- LspAdapterDelegate,
+ LspAdapterDelegate, Toolchain,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use smol::{fs, lock::RwLock, stream::StreamExt};
@@ -341,10 +341,10 @@ async fn detect_package_manager(
fs: Arc<dyn Fs>,
package_json_data: Option<PackageJsonData>,
) -> &'static str {
- if let Some(package_json_data) = package_json_data {
- if let Some(package_manager) = package_json_data.package_manager {
- return package_manager;
- }
+ if let Some(package_json_data) = package_json_data
+ && let Some(package_manager) = package_json_data.package_manager
+ {
+ return package_manager;
}
if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
return "pnpm";
@@ -557,12 +557,13 @@ struct TypeScriptVersions {
#[async_trait(?Send)]
impl LspAdapter for TypeScriptLspAdapter {
fn name(&self) -> LanguageServerName {
- Self::SERVER_NAME.clone()
+ Self::SERVER_NAME
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(TypeScriptVersions {
typescript_version: self.node.npm_package_latest_version("typescript").await?,
@@ -587,9 +588,8 @@ impl LspAdapter for TypeScriptLspAdapter {
.should_install_npm_package(
Self::PACKAGE_NAME,
&server_path,
- &container_dir,
- version.typescript_version.as_str(),
- Default::default(),
+ container_dir,
+ VersionStrategy::Latest(version.typescript_version.as_str()),
)
.await;
@@ -723,7 +723,7 @@ impl LspAdapter for TypeScriptLspAdapter {
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
cx: &mut AsyncApp,
) -> Result<Value> {
let override_options = cx.update(|cx| {
@@ -823,7 +823,7 @@ impl LspAdapter for EsLintLspAdapter {
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
cx: &mut AsyncApp,
) -> Result<Value> {
let workspace_root = delegate.worktree_root_path();
@@ -880,12 +880,13 @@ impl LspAdapter for EsLintLspAdapter {
}
fn name(&self) -> LanguageServerName {
- Self::SERVER_NAME.clone()
+ Self::SERVER_NAME
}
async fn fetch_latest_server_version(
&self,
_delegate: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Send + Any>> {
let url = build_asset_url(
"zed-industries/vscode-eslint",
@@ -911,7 +912,7 @@ impl LspAdapter for EsLintLspAdapter {
let server_path = destination_path.join(Self::SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
- remove_matching(&container_dir, |entry| entry != destination_path).await;
+ remove_matching(&container_dir, |_| true).await;
download_server_binary(
delegate,
@@ -1029,7 +1030,7 @@ mod tests {
.unindent();
let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
- let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
+ let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
assert_eq!(
outline
.items
@@ -1083,7 +1084,7 @@ mod tests {
.unindent();
let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx));
- let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap());
+ let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None));
assert_eq!(
outline
.items
@@ -5,6 +5,7 @@ first_line_pattern = '^#!.*\b(?:deno run|ts-node|bun|tsx|[/ ]node)\b'
line_comments = ["// "]
block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 }
documentation_comment = { start = "/**", prefix = "* ", end = "*/", tab_size = 1 }
+wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "</", end_suffix = ">" }
autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
@@ -248,6 +248,7 @@
"is"
"keyof"
"let"
+ "module"
"namespace"
"new"
"of"
@@ -272,4 +273,4 @@
"while"
"with"
"yield"
-] @keyword
+] @keyword
@@ -15,6 +15,21 @@
(#set! injection.language "css"))
)
+(call_expression
+ function: (member_expression
+ object: (identifier) @_obj (#eq? @_obj "styled")
+ property: (property_identifier))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
+(call_expression
+ function: (call_expression
+ function: (identifier) @_name (#eq? @_name "styled"))
+ arguments: (template_string (string_fragment) @injection.content
+ (#set! injection.language "css"))
+)
+
(call_expression
function: (identifier) @_name (#eq? @_name "html")
arguments: (template_string) @injection.content
@@ -63,6 +78,12 @@
(#set! injection.language "graphql")))
)
+(call_expression
+ function: (identifier) @_name(#match? @_name "^iso$")
+ arguments: (arguments (template_string (string_fragment) @injection.content
+ (#set! injection.language "isograph")))
+)
+
;; Angular Component template injection
(call_expression
function: [
@@ -2,9 +2,9 @@ use anyhow::Result;
use async_trait::async_trait;
use collections::HashMap;
use gpui::AsyncApp;
-use language::{LanguageName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
+use language::{LanguageName, LspAdapter, LspAdapterDelegate, Toolchain};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use serde_json::Value;
use std::{
@@ -67,12 +67,13 @@ const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vtsls");
#[async_trait(?Send)]
impl LspAdapter for VtslsLspAdapter {
fn name(&self) -> LanguageServerName {
- SERVER_NAME.clone()
+ SERVER_NAME
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(TypeScriptVersions {
typescript_version: self.node.npm_package_latest_version("typescript").await?,
@@ -86,7 +87,7 @@ impl LspAdapter for VtslsLspAdapter {
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let env = delegate.shell_env().await;
@@ -115,8 +116,7 @@ impl LspAdapter for VtslsLspAdapter {
Self::PACKAGE_NAME,
&server_path,
&container_dir,
- &latest_version.server_version,
- Default::default(),
+ VersionStrategy::Latest(&latest_version.server_version),
)
.await
{
@@ -129,8 +129,7 @@ impl LspAdapter for VtslsLspAdapter {
Self::TYPESCRIPT_PACKAGE_NAME,
&container_dir.join(Self::TYPESCRIPT_TSDK_PATH),
&container_dir,
- &latest_version.typescript_version,
- Default::default(),
+ VersionStrategy::Latest(&latest_version.typescript_version),
)
.await
{
@@ -213,7 +212,7 @@ impl LspAdapter for VtslsLspAdapter {
self: Arc<Self>,
fs: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
cx: &mut AsyncApp,
) -> Result<Value> {
let tsdk_path = Self::tsdk_path(fs, delegate).await;
@@ -2,11 +2,9 @@ use anyhow::{Context as _, Result};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::AsyncApp;
-use language::{
- LanguageToolchainStore, LspAdapter, LspAdapterDelegate, language_settings::AllLanguageSettings,
-};
+use language::{LspAdapter, LspAdapterDelegate, Toolchain, language_settings::AllLanguageSettings};
use lsp::{LanguageServerBinary, LanguageServerName};
-use node_runtime::NodeRuntime;
+use node_runtime::{NodeRuntime, VersionStrategy};
use project::{Fs, lsp_store::language_server_settings};
use serde_json::Value;
use settings::{Settings, SettingsLocation};
@@ -40,12 +38,13 @@ impl YamlLspAdapter {
#[async_trait(?Send)]
impl LspAdapter for YamlLspAdapter {
fn name(&self) -> LanguageServerName {
- Self::SERVER_NAME.clone()
+ Self::SERVER_NAME
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
+ _: &AsyncApp,
) -> Result<Box<dyn 'static + Any + Send>> {
Ok(Box::new(
self.node
@@ -57,7 +56,7 @@ impl LspAdapter for YamlLspAdapter {
async fn check_if_user_installed(
&self,
delegate: &dyn LspAdapterDelegate,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
_: &AsyncApp,
) -> Option<LanguageServerBinary> {
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
@@ -107,9 +106,8 @@ impl LspAdapter for YamlLspAdapter {
.should_install_npm_package(
Self::PACKAGE_NAME,
&server_path,
- &container_dir,
- &version,
- Default::default(),
+ container_dir,
+ VersionStrategy::Latest(version),
)
.await;
@@ -136,7 +134,7 @@ impl LspAdapter for YamlLspAdapter {
self: Arc<Self>,
_: &dyn Fs,
delegate: &Arc<dyn LspAdapterDelegate>,
- _: Arc<dyn LanguageToolchainStore>,
+ _: Option<Toolchain>,
cx: &mut AsyncApp,
) -> Result<Value> {
let location = SettingsLocation {
@@ -0,0 +1,5 @@
+(comment) @comment.inclusive
+[
+ (single_quote_scalar)
+ (double_quote_scalar)
+] @string
@@ -0,0 +1,24 @@
+[package]
+name = "line_ending_selector"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/line_ending_selector.rs"
+doctest = false
+
+[dependencies]
+editor.workspace = true
+gpui.workspace = true
+language.workspace = true
+picker.workspace = true
+project.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true
+workspace-hack.workspace = true
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,192 @@
+use editor::Editor;
+use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, actions};
+use language::{Buffer, LineEnding};
+use picker::{Picker, PickerDelegate};
+use project::Project;
+use std::sync::Arc;
+use ui::{ListItem, ListItemSpacing, prelude::*};
+use util::ResultExt;
+use workspace::ModalView;
+
+actions!(
+ line_ending,
+ [
+ /// Toggles the line ending selector modal.
+ Toggle
+ ]
+);
+
+pub fn init(cx: &mut App) {
+ cx.observe_new(LineEndingSelector::register).detach();
+}
+
+pub struct LineEndingSelector {
+ picker: Entity<Picker<LineEndingSelectorDelegate>>,
+}
+
+impl LineEndingSelector {
+ fn register(editor: &mut Editor, _window: Option<&mut Window>, cx: &mut Context<Editor>) {
+ let editor_handle = cx.weak_entity();
+ editor
+ .register_action(move |_: &Toggle, window, cx| {
+ Self::toggle(&editor_handle, window, cx);
+ })
+ .detach();
+ }
+
+ fn toggle(editor: &WeakEntity<Editor>, window: &mut Window, cx: &mut App) {
+ let Some((workspace, buffer)) = editor
+ .update(cx, |editor, cx| {
+ Some((editor.workspace()?, editor.active_excerpt(cx)?.1))
+ })
+ .ok()
+ .flatten()
+ else {
+ return;
+ };
+
+ workspace.update(cx, |workspace, cx| {
+ let project = workspace.project().clone();
+ workspace.toggle_modal(window, cx, move |window, cx| {
+ LineEndingSelector::new(buffer, project, window, cx)
+ });
+ })
+ }
+
+ fn new(
+ buffer: Entity<Buffer>,
+ project: Entity<Project>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ let line_ending = buffer.read(cx).line_ending();
+ let delegate =
+ LineEndingSelectorDelegate::new(cx.entity().downgrade(), buffer, project, line_ending);
+ let picker = cx.new(|cx| Picker::nonsearchable_uniform_list(delegate, window, cx));
+ Self { picker }
+ }
+}
+
+impl Render for LineEndingSelector {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ v_flex().w(rems(34.)).child(self.picker.clone())
+ }
+}
+
+impl Focusable for LineEndingSelector {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl EventEmitter<DismissEvent> for LineEndingSelector {}
+impl ModalView for LineEndingSelector {}
+
+struct LineEndingSelectorDelegate {
+ line_ending_selector: WeakEntity<LineEndingSelector>,
+ buffer: Entity<Buffer>,
+ project: Entity<Project>,
+ line_ending: LineEnding,
+ matches: Vec<LineEnding>,
+ selected_index: usize,
+}
+
+impl LineEndingSelectorDelegate {
+ fn new(
+ line_ending_selector: WeakEntity<LineEndingSelector>,
+ buffer: Entity<Buffer>,
+ project: Entity<Project>,
+ line_ending: LineEnding,
+ ) -> Self {
+ Self {
+ line_ending_selector,
+ buffer,
+ project,
+ line_ending,
+ matches: vec![LineEnding::Unix, LineEnding::Windows],
+ selected_index: 0,
+ }
+ }
+}
+
+impl PickerDelegate for LineEndingSelectorDelegate {
+ type ListItem = ListItem;
+
+ fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+ "Select a line ending…".into()
+ }
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ if let Some(line_ending) = self.matches.get(self.selected_index) {
+ self.buffer.update(cx, |this, cx| {
+ this.set_line_ending(*line_ending, cx);
+ });
+ let buffer = self.buffer.clone();
+ let project = self.project.clone();
+ cx.defer(move |cx| {
+ project.update(cx, |this, cx| {
+ this.save_buffer(buffer, cx).detach();
+ });
+ });
+ }
+ self.dismissed(window, cx);
+ }
+
+ fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
+ self.line_ending_selector
+ .update(cx, |_, cx| cx.emit(DismissEvent))
+ .log_err();
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(
+ &mut self,
+ ix: usize,
+ _window: &mut Window,
+ _: &mut Context<Picker<Self>>,
+ ) {
+ self.selected_index = ix;
+ }
+
+ fn update_matches(
+ &mut self,
+ _query: String,
+ _window: &mut Window,
+ _cx: &mut Context<Picker<Self>>,
+ ) -> gpui::Task<()> {
+ return Task::ready(());
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ _: &mut Window,
+ _: &mut Context<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let line_ending = self.matches.get(ix)?;
+ let label = match line_ending {
+ LineEnding::Unix => "LF",
+ LineEnding::Windows => "CRLF",
+ };
+
+ let mut list_item = ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .toggle_state(selected)
+ .child(Label::new(label));
+
+ if &self.line_ending == line_ending {
+ list_item = list_item.end_slot(Icon::new(IconName::Check).color(Color::Muted));
+ }
+
+ Some(list_item)
+ }
+}
@@ -22,6 +22,7 @@ test-support = ["collections/test-support", "gpui/test-support"]
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
+audio.workspace = true
collections.workspace = true
cpal.workspace = true
futures.workspace = true
@@ -34,6 +35,10 @@ log.workspace = true
nanoid.workspace = true
parking_lot.workspace = true
postage.workspace = true
+rodio = { workspace = true, features = ["wav_output", "recording"] }
+serde.workspace = true
+serde_urlencoded.workspace = true
+settings.workspace = true
smallvec.workspace = true
tokio-tungstenite.workspace = true
util.workspace = true
@@ -159,14 +159,14 @@ impl LivekitWindow {
if output
.audio_output_stream
.as_ref()
- .map_or(false, |(track, _)| track.sid() == unpublish_sid)
+ .is_some_and(|(track, _)| track.sid() == unpublish_sid)
{
output.audio_output_stream.take();
}
if output
.screen_share_output_view
.as_ref()
- .map_or(false, |(track, _)| track.sid() == unpublish_sid)
+ .is_some_and(|(track, _)| track.sid() == unpublish_sid)
{
output.screen_share_output_view.take();
}
@@ -183,7 +183,7 @@ impl LivekitWindow {
match track {
livekit_client::RemoteTrack::Audio(track) => {
output.audio_output_stream = Some((
- publication.clone(),
+ publication,
room.play_remote_audio_track(&track, cx).unwrap(),
));
}
@@ -255,7 +255,10 @@ impl LivekitWindow {
} else {
let room = self.room.clone();
cx.spawn_in(window, async move |this, cx| {
- let (publication, stream) = room.publish_local_microphone_track(cx).await.unwrap();
+ let (publication, stream) = room
+ .publish_local_microphone_track("test_user".to_string(), false, cx)
+ .await
+ .unwrap();
this.update(cx, |this, cx| {
this.microphone_track = Some(publication);
this.microphone_stream = Some(stream);
@@ -1,7 +1,13 @@
+use anyhow::Context as _;
use collections::HashMap;
mod remote_video_track_view;
+use cpal::traits::HostTrait as _;
pub use remote_video_track_view::{RemoteVideoTrackView, RemoteVideoTrackViewEvent};
+use rodio::DeviceTrait as _;
+
+mod record;
+pub use record::CaptureInput;
#[cfg(not(any(
test,
@@ -18,6 +24,11 @@ mod livekit_client;
)))]
pub use livekit_client::*;
+// If you need proper LSP in livekit_client you've got to comment
+// - the cfg blocks above
+// - the mods: mock_client & test and their conditional blocks
+// - the pub use mock_client::* and their conditional blocks
+
#[cfg(any(
test,
feature = "test-support",
@@ -168,3 +179,59 @@ pub enum RoomEvent {
Reconnecting,
Reconnected,
}
+
+pub(crate) fn default_device(
+ input: bool,
+) -> anyhow::Result<(cpal::Device, cpal::SupportedStreamConfig)> {
+ let device;
+ let config;
+ if input {
+ device = cpal::default_host()
+ .default_input_device()
+ .context("no audio input device available")?;
+ config = device
+ .default_input_config()
+ .context("failed to get default input config")?;
+ } else {
+ device = cpal::default_host()
+ .default_output_device()
+ .context("no audio output device available")?;
+ config = device
+ .default_output_config()
+ .context("failed to get default output config")?;
+ }
+ Ok((device, config))
+}
+
+pub(crate) fn get_sample_data(
+ sample_format: cpal::SampleFormat,
+ data: &cpal::Data,
+) -> anyhow::Result<Vec<i16>> {
+ match sample_format {
+ cpal::SampleFormat::I8 => Ok(convert_sample_data::<i8, i16>(data)),
+ cpal::SampleFormat::I16 => Ok(data.as_slice::<i16>().unwrap().to_vec()),
+ cpal::SampleFormat::I24 => Ok(convert_sample_data::<cpal::I24, i16>(data)),
+ cpal::SampleFormat::I32 => Ok(convert_sample_data::<i32, i16>(data)),
+ cpal::SampleFormat::I64 => Ok(convert_sample_data::<i64, i16>(data)),
+ cpal::SampleFormat::U8 => Ok(convert_sample_data::<u8, i16>(data)),
+ cpal::SampleFormat::U16 => Ok(convert_sample_data::<u16, i16>(data)),
+ cpal::SampleFormat::U32 => Ok(convert_sample_data::<u32, i16>(data)),
+ cpal::SampleFormat::U64 => Ok(convert_sample_data::<u64, i16>(data)),
+ cpal::SampleFormat::F32 => Ok(convert_sample_data::<f32, i16>(data)),
+ cpal::SampleFormat::F64 => Ok(convert_sample_data::<f64, i16>(data)),
+ _ => anyhow::bail!("Unsupported sample format"),
+ }
+}
+
+pub(crate) fn convert_sample_data<
+ TSource: cpal::SizedSample,
+ TDest: cpal::SizedSample + cpal::FromSample<TSource>,
+>(
+ data: &cpal::Data,
+) -> Vec<TDest> {
+ data.as_slice::<TSource>()
+ .unwrap()
+ .iter()
+ .map(|e| e.to_sample::<TDest>())
+ .collect()
+}
@@ -1,11 +1,14 @@
use std::sync::Arc;
use anyhow::{Context as _, Result};
+use audio::AudioSettings;
use collections::HashMap;
use futures::{SinkExt, channel::mpsc};
use gpui::{App, AsyncApp, ScreenCaptureSource, ScreenCaptureStream, Task};
use gpui_tokio::Tokio;
+use log::info;
use playback::capture_local_video_track;
+use settings::Settings;
mod playback;
@@ -94,9 +97,13 @@ impl Room {
pub async fn publish_local_microphone_track(
&self,
+ user_name: String,
+ is_staff: bool,
cx: &mut AsyncApp,
) -> Result<(LocalTrackPublication, playback::AudioStream)> {
- let (track, stream) = self.playback.capture_local_microphone_track()?;
+ let (track, stream) = self
+ .playback
+ .capture_local_microphone_track(user_name, is_staff, &cx)?;
let publication = self
.local_participant()
.publish_track(
@@ -123,9 +130,14 @@ impl Room {
pub fn play_remote_audio_track(
&self,
track: &RemoteAudioTrack,
- _cx: &App,
+ cx: &mut App,
) -> Result<playback::AudioStream> {
- Ok(self.playback.play_remote_audio_track(&track.0))
+ if AudioSettings::get_global(cx).rodio_audio {
+ info!("Using experimental.rodio_audio audio pipeline for output");
+ playback::play_remote_audio_track(&track.0, cx)
+ } else {
+ Ok(self.playback.play_remote_audio_track(&track.0))
+ }
}
}
@@ -1,11 +1,12 @@
use anyhow::{Context as _, Result};
-use cpal::traits::{DeviceTrait, HostTrait, StreamTrait as _};
-use cpal::{Data, FromSample, I24, SampleFormat, SizedSample};
+use audio::{AudioSettings, CHANNEL_COUNT, SAMPLE_RATE};
+use cpal::traits::{DeviceTrait, StreamTrait as _};
use futures::channel::mpsc::UnboundedSender;
use futures::{Stream, StreamExt as _};
use gpui::{
- BackgroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Task,
+ AsyncApp, BackgroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
+ Task,
};
use libwebrtc::native::{apm, audio_mixer, audio_resampler};
use livekit::track;
@@ -18,14 +19,20 @@ use livekit::webrtc::{
video_source::{RtcVideoSource, VideoResolution, native::NativeVideoSource},
video_stream::native::NativeVideoStream,
};
+use log::info;
use parking_lot::Mutex;
+use rodio::Source;
+use serde::{Deserialize, Serialize};
+use settings::Settings;
use std::cell::RefCell;
use std::sync::Weak;
-use std::sync::atomic::{self, AtomicI32};
+use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::time::Duration;
use std::{borrow::Cow, collections::VecDeque, sync::Arc, thread};
use util::{ResultExt as _, maybe};
+mod source;
+
pub(crate) struct AudioStack {
executor: BackgroundExecutor,
apm: Arc<Mutex<apm::AudioProcessingModule>>,
@@ -34,12 +41,36 @@ pub(crate) struct AudioStack {
next_ssrc: AtomicI32,
}
-// NOTE: We use WebRTC's mixer which only supports
-// 16kHz, 32kHz and 48kHz. As 48 is the most common "next step up"
-// for audio output devices like speakers/bluetooth, we just hard-code
-// this; and downsample when we need to.
-const SAMPLE_RATE: u32 = 48000;
-const NUM_CHANNELS: u32 = 2;
+pub(crate) fn play_remote_audio_track(
+ track: &livekit::track::RemoteAudioTrack,
+ cx: &mut gpui::App,
+) -> Result<AudioStream> {
+ let stop_handle = Arc::new(AtomicBool::new(false));
+ let stop_handle_clone = stop_handle.clone();
+ let stream = source::LiveKitStream::new(cx.background_executor(), track);
+
+ let stream = stream
+ .stoppable()
+ .periodic_access(Duration::from_millis(50), move |s| {
+ if stop_handle.load(Ordering::Relaxed) {
+ s.stop();
+ }
+ });
+
+ let speaker: Speaker = serde_urlencoded::from_str(&track.name()).unwrap_or_else(|_| Speaker {
+ name: track.name(),
+ is_staff: false,
+ });
+ audio::Audio::play_voip_stream(stream, speaker.name, speaker.is_staff, cx)
+ .context("Could not play audio")?;
+
+ let on_drop = util::defer(move || {
+ stop_handle_clone.store(true, Ordering::Relaxed);
+ });
+ Ok(AudioStream::Output {
+ _drop: Box::new(on_drop),
+ })
+}
impl AudioStack {
pub(crate) fn new(executor: BackgroundExecutor) -> Self {
@@ -62,11 +93,11 @@ impl AudioStack {
) -> AudioStream {
let output_task = self.start_output();
- let next_ssrc = self.next_ssrc.fetch_add(1, atomic::Ordering::Relaxed);
+ let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed);
let source = AudioMixerSource {
ssrc: next_ssrc,
- sample_rate: SAMPLE_RATE,
- num_channels: NUM_CHANNELS,
+ sample_rate: SAMPLE_RATE.get(),
+ num_channels: CHANNEL_COUNT.get() as u32,
buffer: Arc::default(),
};
self.mixer.lock().add_source(source.clone());
@@ -98,19 +129,45 @@ impl AudioStack {
}
}
+ fn start_output(&self) -> Arc<Task<()>> {
+ if let Some(task) = self._output_task.borrow().upgrade() {
+ return task;
+ }
+ let task = Arc::new(self.executor.spawn({
+ let apm = self.apm.clone();
+ let mixer = self.mixer.clone();
+ async move {
+ Self::play_output(apm, mixer, SAMPLE_RATE.get(), CHANNEL_COUNT.get().into())
+ .await
+ .log_err();
+ }
+ }));
+ *self._output_task.borrow_mut() = Arc::downgrade(&task);
+ task
+ }
+
pub(crate) fn capture_local_microphone_track(
&self,
+ user_name: String,
+ is_staff: bool,
+ cx: &AsyncApp,
) -> Result<(crate::LocalAudioTrack, AudioStream)> {
let source = NativeAudioSource::new(
// n.b. this struct's options are always ignored, noise cancellation is provided by apm.
AudioSourceOptions::default(),
- SAMPLE_RATE,
- NUM_CHANNELS,
+ SAMPLE_RATE.get(),
+ CHANNEL_COUNT.get().into(),
10,
);
+ let track_name = serde_urlencoded::to_string(Speaker {
+ name: user_name,
+ is_staff,
+ })
+ .context("Could not encode user information in track name")?;
+
let track = track::LocalAudioTrack::create_audio_track(
- "microphone",
+ &track_name,
RtcAudioSource::Native(source.clone()),
);
@@ -118,44 +175,41 @@ impl AudioStack {
let (frame_tx, mut frame_rx) = futures::channel::mpsc::unbounded();
let transmit_task = self.executor.spawn({
- let source = source.clone();
async move {
while let Some(frame) = frame_rx.next().await {
source.capture_frame(&frame).await.log_err();
}
}
});
- let capture_task = self.executor.spawn(async move {
- Self::capture_input(apm, frame_tx, SAMPLE_RATE, NUM_CHANNELS).await
- });
+ let rodio_pipeline =
+ AudioSettings::try_read_global(cx, |setting| setting.rodio_audio).unwrap_or_default();
+ let capture_task = if rodio_pipeline {
+ info!("Using experimental.rodio_audio audio pipeline");
+ let voip_parts = audio::VoipParts::new(cx)?;
+ thread::spawn(move || {
+ // microphone is non send on mac
+ let microphone = audio::Audio::open_microphone(voip_parts)?;
+ send_to_livekit(frame_tx, microphone);
+ Ok::<(), anyhow::Error>(())
+ });
+ Task::ready(Ok(()))
+ } else {
+ self.executor.spawn(async move {
+ Self::capture_input(apm, frame_tx, SAMPLE_RATE.get(), CHANNEL_COUNT.get().into())
+ .await
+ })
+ };
let on_drop = util::defer(|| {
drop(transmit_task);
drop(capture_task);
});
- return Ok((
+ Ok((
super::LocalAudioTrack(track),
AudioStream::Output {
_drop: Box::new(on_drop),
},
- ));
- }
-
- fn start_output(&self) -> Arc<Task<()>> {
- if let Some(task) = self._output_task.borrow().upgrade() {
- return task;
- }
- let task = Arc::new(self.executor.spawn({
- let apm = self.apm.clone();
- let mixer = self.mixer.clone();
- async move {
- Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS)
- .await
- .log_err();
- }
- }));
- *self._output_task.borrow_mut() = Arc::downgrade(&task);
- task
+ ))
}
async fn play_output(
@@ -166,7 +220,7 @@ impl AudioStack {
) -> Result<()> {
loop {
let mut device_change_listener = DeviceChangeListener::new(false)?;
- let (output_device, output_config) = default_device(false)?;
+ let (output_device, output_config) = crate::default_device(false)?;
let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>();
let mixer = mixer.clone();
let apm = apm.clone();
@@ -238,7 +292,7 @@ impl AudioStack {
) -> Result<()> {
loop {
let mut device_change_listener = DeviceChangeListener::new(true)?;
- let (device, config) = default_device(true)?;
+ let (device, config) = crate::default_device(true)?;
let (end_on_drop_tx, end_on_drop_rx) = std::sync::mpsc::channel::<()>();
let apm = apm.clone();
let frame_tx = frame_tx.clone();
@@ -262,7 +316,7 @@ impl AudioStack {
config.sample_format(),
move |data, _: &_| {
let data =
- Self::get_sample_data(config.sample_format(), data).log_err();
+ crate::get_sample_data(config.sample_format(), data).log_err();
let Some(data) = data else {
return;
};
@@ -320,32 +374,35 @@ impl AudioStack {
drop(end_on_drop_tx)
}
}
+}
- fn get_sample_data(sample_format: SampleFormat, data: &Data) -> Result<Vec<i16>> {
- match sample_format {
- SampleFormat::I8 => Ok(Self::convert_sample_data::<i8, i16>(data)),
- SampleFormat::I16 => Ok(data.as_slice::<i16>().unwrap().to_vec()),
- SampleFormat::I24 => Ok(Self::convert_sample_data::<I24, i16>(data)),
- SampleFormat::I32 => Ok(Self::convert_sample_data::<i32, i16>(data)),
- SampleFormat::I64 => Ok(Self::convert_sample_data::<i64, i16>(data)),
- SampleFormat::U8 => Ok(Self::convert_sample_data::<u8, i16>(data)),
- SampleFormat::U16 => Ok(Self::convert_sample_data::<u16, i16>(data)),
- SampleFormat::U32 => Ok(Self::convert_sample_data::<u32, i16>(data)),
- SampleFormat::U64 => Ok(Self::convert_sample_data::<u64, i16>(data)),
- SampleFormat::F32 => Ok(Self::convert_sample_data::<f32, i16>(data)),
- SampleFormat::F64 => Ok(Self::convert_sample_data::<f64, i16>(data)),
- _ => anyhow::bail!("Unsupported sample format"),
- }
- }
+#[derive(Serialize, Deserialize)]
+struct Speaker {
+ name: String,
+ is_staff: bool,
+}
- fn convert_sample_data<TSource: SizedSample, TDest: SizedSample + FromSample<TSource>>(
- data: &Data,
- ) -> Vec<TDest> {
- data.as_slice::<TSource>()
- .unwrap()
- .iter()
- .map(|e| e.to_sample::<TDest>())
- .collect()
+fn send_to_livekit(frame_tx: UnboundedSender<AudioFrame<'static>>, mut microphone: impl Source) {
+ use cpal::Sample;
+ loop {
+ let sampled: Vec<_> = microphone
+ .by_ref()
+ .take(audio::BUFFER_SIZE)
+ .map(|s| s.to_sample())
+ .collect();
+
+ if frame_tx
+ .unbounded_send(AudioFrame {
+ sample_rate: SAMPLE_RATE.get(),
+ num_channels: CHANNEL_COUNT.get() as u32,
+ samples_per_channel: sampled.len() as u32 / CHANNEL_COUNT.get() as u32,
+ data: Cow::Owned(sampled),
+ })
+ .is_err()
+ {
+ // must rx has dropped or is not consuming
+ break;
+ }
}
}
@@ -393,27 +450,6 @@ pub(crate) async fn capture_local_video_track(
))
}
-fn default_device(input: bool) -> Result<(cpal::Device, cpal::SupportedStreamConfig)> {
- let device;
- let config;
- if input {
- device = cpal::default_host()
- .default_input_device()
- .context("no audio input device available")?;
- config = device
- .default_input_config()
- .context("failed to get default input config")?;
- } else {
- device = cpal::default_host()
- .default_output_device()
- .context("no audio output device available")?;
- config = device
- .default_output_config()
- .context("failed to get default output config")?;
- }
- Ok((device, config))
-}
-
#[derive(Clone)]
struct AudioMixerSource {
ssrc: i32,
@@ -0,0 +1,84 @@
+use std::num::NonZero;
+
+use futures::StreamExt;
+use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame};
+use livekit::track::RemoteAudioTrack;
+use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter, nz};
+
+use audio::{CHANNEL_COUNT, SAMPLE_RATE};
+
+fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer {
+ let samples = frame.data.iter().copied();
+ let samples = SampleTypeConverter::<_, _>::new(samples);
+ let samples: Vec<f32> = samples.collect();
+ SamplesBuffer::new(
+ // here be dragons
+ // NonZero::new(frame.num_channels as u16).expect("audio frame channels is nonzero"),
+ nz!(2),
+ NonZero::new(frame.sample_rate).expect("audio frame sample rate is nonzero"),
+ samples,
+ )
+}
+
+pub struct LiveKitStream {
+ // shared_buffer: SharedBuffer,
+ inner: rodio::queue::SourcesQueueOutput,
+ _receiver_task: gpui::Task<()>,
+}
+
+impl LiveKitStream {
+ pub fn new(executor: &gpui::BackgroundExecutor, track: &RemoteAudioTrack) -> Self {
+ let mut stream = NativeAudioStream::new(
+ track.rtc_track(),
+ SAMPLE_RATE.get() as i32,
+ CHANNEL_COUNT.get().into(),
+ );
+ let (queue_input, queue_output) = rodio::queue::queue(true);
+ // spawn rtc stream
+ let receiver_task = executor.spawn({
+ async move {
+ while let Some(frame) = stream.next().await {
+ let samples = frame_to_samplesbuffer(frame);
+ queue_input.append(samples);
+ }
+ }
+ });
+
+ LiveKitStream {
+ _receiver_task: receiver_task,
+ inner: queue_output,
+ }
+ }
+}
+
+impl Iterator for LiveKitStream {
+ type Item = rodio::Sample;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.inner.next()
+ }
+}
+
+impl Source for LiveKitStream {
+ fn current_span_len(&self) -> Option<usize> {
+ self.inner.current_span_len()
+ }
+
+ fn channels(&self) -> rodio::ChannelCount {
+ // This must be hardcoded because the playback source assumes constant
+ // sample rate and channel count. The queue upon which this is build
+ // will however report different counts and rates. Even though we put in
+ // only items with our (constant) CHANNEL_COUNT & SAMPLE_RATE this will
+ // play silence on one channel and at 44100 which is not what our
+ // constants are.
+ CHANNEL_COUNT
+ }
+
+ fn sample_rate(&self) -> rodio::SampleRate {
+ SAMPLE_RATE // see comment on channels
+ }
+
+ fn total_duration(&self) -> Option<std::time::Duration> {
+ self.inner.total_duration()
+ }
+}
@@ -0,0 +1,96 @@
+use std::{
+ env,
+ num::NonZero,
+ path::{Path, PathBuf},
+ sync::{Arc, Mutex},
+ time::Duration,
+};
+
+use anyhow::{Context, Result};
+use cpal::traits::{DeviceTrait, StreamTrait};
+use rodio::{buffer::SamplesBuffer, conversions::SampleTypeConverter};
+use util::ResultExt;
+
+pub struct CaptureInput {
+ pub name: String,
+ config: cpal::SupportedStreamConfig,
+ samples: Arc<Mutex<Vec<i16>>>,
+ _stream: cpal::Stream,
+}
+
+impl CaptureInput {
+ pub fn start() -> anyhow::Result<Self> {
+ let (device, config) = crate::default_device(true)?;
+ let name = device.name().unwrap_or("<unknown>".to_string());
+ log::info!("Using microphone: {}", name);
+
+ let samples = Arc::new(Mutex::new(Vec::new()));
+ let stream = start_capture(device, config.clone(), samples.clone())?;
+
+ Ok(Self {
+ name,
+ _stream: stream,
+ config,
+ samples,
+ })
+ }
+
+ pub fn finish(self) -> Result<PathBuf> {
+ let name = self.name;
+ let mut path = env::current_dir().context("Could not get current dir")?;
+ path.push(&format!("test_recording_{name}.wav"));
+ log::info!("Test recording written to: {}", path.display());
+ write_out(self.samples, self.config, &path)?;
+ Ok(path)
+ }
+}
+
+fn start_capture(
+ device: cpal::Device,
+ config: cpal::SupportedStreamConfig,
+ samples: Arc<Mutex<Vec<i16>>>,
+) -> Result<cpal::Stream> {
+ let stream = device
+ .build_input_stream_raw(
+ &config.config(),
+ config.sample_format(),
+ move |data, _: &_| {
+ let data = crate::get_sample_data(config.sample_format(), data).log_err();
+ let Some(data) = data else {
+ return;
+ };
+ samples
+ .try_lock()
+ .expect("Only locked after stream ends")
+ .extend_from_slice(&data);
+ },
+ |err| log::error!("error capturing audio track: {:?}", err),
+ Some(Duration::from_millis(100)),
+ )
+ .context("failed to build input stream")?;
+
+ stream.play()?;
+ Ok(stream)
+}
+
+fn write_out(
+ samples: Arc<Mutex<Vec<i16>>>,
+ config: cpal::SupportedStreamConfig,
+ path: &Path,
+) -> Result<()> {
+ let samples = std::mem::take(
+ &mut *samples
+ .try_lock()
+ .expect("Stream has ended, callback cant hold the lock"),
+ );
+ let samples: Vec<f32> = SampleTypeConverter::<_, f32>::new(samples.into_iter()).collect();
+ let mut samples = SamplesBuffer::new(
+ NonZero::new(config.channels()).expect("config channel is never zero"),
+ NonZero::new(config.sample_rate().0).expect("config sample_rate is never zero"),
+ samples,
+ );
+ match rodio::wav_to_file(&mut samples, path) {
+ Ok(_) => Ok(()),
+ Err(e) => Err(anyhow::anyhow!("Failed to write wav file: {}", e)),
+ }
+}
@@ -421,7 +421,7 @@ impl TestServer {
track_sid: &TrackSid,
muted: bool,
) -> Result<()> {
- let claims = livekit_api::token::validate(&token, &self.secret_key)?;
+ let claims = livekit_api::token::validate(token, &self.secret_key)?;
let room_name = claims.video.room.unwrap();
let identity = ParticipantIdentity(claims.sub.unwrap().to_string());
let mut server_rooms = self.rooms.lock();
@@ -475,7 +475,7 @@ impl TestServer {
}
pub(crate) fn is_track_muted(&self, token: &str, track_sid: &TrackSid) -> Option<bool> {
- let claims = livekit_api::token::validate(&token, &self.secret_key).ok()?;
+ let claims = livekit_api::token::validate(token, &self.secret_key).ok()?;
let room_name = claims.video.room.unwrap();
let mut server_rooms = self.rooms.lock();
@@ -728,6 +728,8 @@ impl Room {
pub async fn publish_local_microphone_track(
&self,
+ _track_name: String,
+ _is_staff: bool,
cx: &mut AsyncApp,
) -> Result<(LocalTrackPublication, AudioStream)> {
self.local_participant().publish_microphone_track(cx).await
@@ -736,14 +738,14 @@ impl Room {
impl Drop for RoomState {
fn drop(&mut self) {
- if self.connection_state == ConnectionState::Connected {
- if let Ok(server) = TestServer::get(&self.url) {
- let executor = server.executor.clone();
- let token = self.token.clone();
- executor
- .spawn(async move { server.leave_room(token).await.ok() })
- .detach();
- }
+ if self.connection_state == ConnectionState::Connected
+ && let Ok(server) = TestServer::get(&self.url)
+ {
+ let executor = server.executor.clone();
+ let token = self.token.clone();
+ executor
+ .spawn(async move { server.leave_room(token).await.ok() })
+ .detach();
}
}
}
@@ -86,11 +86,12 @@ impl Model {
}
#[derive(Debug, Serialize, Deserialize)]
-#[serde(untagged)]
+#[serde(rename_all = "lowercase")]
pub enum ToolChoice {
Auto,
Required,
None,
+ #[serde(untagged)]
Other(ToolDefinition),
}
@@ -45,7 +45,7 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact};
const JSON_RPC_VERSION: &str = "2.0";
const CONTENT_LEN_HEADER: &str = "Content-Length: ";
-const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
+pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2);
const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
type NotificationHandler = Box<dyn Send + FnMut(Option<RequestId>, Value, &mut AsyncApp)>;
@@ -100,8 +100,8 @@ pub struct LanguageServer {
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
output_done_rx: Mutex<Option<barrier::Receiver>>,
server: Arc<Mutex<Option<Child>>>,
- workspace_folders: Option<Arc<Mutex<BTreeSet<Url>>>>,
- root_uri: Url,
+ workspace_folders: Option<Arc<Mutex<BTreeSet<Uri>>>>,
+ root_uri: Uri,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
@@ -166,6 +166,12 @@ impl<'a> From<&'a str> for LanguageServerName {
}
}
+impl PartialEq<str> for LanguageServerName {
+ fn eq(&self, other: &str) -> bool {
+ self.0 == other
+ }
+}
+
/// Handle to a language server RPC activity subscription.
pub enum Subscription {
Notification {
@@ -310,7 +316,7 @@ impl LanguageServer {
binary: LanguageServerBinary,
root_path: &Path,
code_action_kinds: Option<Vec<CodeActionKind>>,
- workspace_folders: Option<Arc<Mutex<BTreeSet<Url>>>>,
+ workspace_folders: Option<Arc<Mutex<BTreeSet<Uri>>>>,
cx: &mut AsyncApp,
) -> Result<Self> {
let working_dir = if root_path.is_dir() {
@@ -318,7 +324,7 @@ impl LanguageServer {
} else {
root_path.parent().unwrap_or_else(|| Path::new("/"))
};
- let root_uri = Url::from_file_path(&working_dir)
+ let root_uri = Uri::from_file_path(&working_dir)
.map_err(|()| anyhow!("{working_dir:?} is not a valid URI"))?;
log::info!(
@@ -384,8 +390,8 @@ impl LanguageServer {
server: Option<Child>,
code_action_kinds: Option<Vec<CodeActionKind>>,
binary: LanguageServerBinary,
- root_uri: Url,
- workspace_folders: Option<Arc<Mutex<BTreeSet<Url>>>>,
+ root_uri: Uri,
+ workspace_folders: Option<Arc<Mutex<BTreeSet<Uri>>>>,
cx: &mut AsyncApp,
on_unhandled_notification: F,
) -> Self
@@ -1350,7 +1356,7 @@ impl LanguageServer {
}
/// Add new workspace folder to the list.
- pub fn add_workspace_folder(&self, uri: Url) {
+ pub fn add_workspace_folder(&self, uri: Uri) {
if self
.capabilities()
.workspace
@@ -1383,8 +1389,9 @@ impl LanguageServer {
self.notify::<DidChangeWorkspaceFolders>(¶ms).ok();
}
}
- /// Add new workspace folder to the list.
- pub fn remove_workspace_folder(&self, uri: Url) {
+
+ /// Remove existing workspace folder from the list.
+ pub fn remove_workspace_folder(&self, uri: Uri) {
if self
.capabilities()
.workspace
@@ -1416,7 +1423,7 @@ impl LanguageServer {
self.notify::<DidChangeWorkspaceFolders>(¶ms).ok();
}
}
- pub fn set_workspace_folders(&self, folders: BTreeSet<Url>) {
+ pub fn set_workspace_folders(&self, folders: BTreeSet<Uri>) {
let Some(workspace_folders) = self.workspace_folders.as_ref() else {
return;
};
@@ -1449,7 +1456,7 @@ impl LanguageServer {
}
}
- pub fn workspace_folders(&self) -> BTreeSet<Url> {
+ pub fn workspace_folders(&self) -> BTreeSet<Uri> {
self.workspace_folders.as_ref().map_or_else(
|| BTreeSet::from_iter([self.root_uri.clone()]),
|folders| folders.lock().clone(),
@@ -1458,7 +1465,7 @@ impl LanguageServer {
pub fn register_buffer(
&self,
- uri: Url,
+ uri: Uri,
language_id: String,
version: i32,
initial_text: String,
@@ -1469,7 +1476,7 @@ impl LanguageServer {
.ok();
}
- pub fn unregister_buffer(&self, uri: Url) {
+ pub fn unregister_buffer(&self, uri: Uri) {
self.notify::<notification::DidCloseTextDocument>(&DidCloseTextDocumentParams {
text_document: TextDocumentIdentifier::new(uri),
})
@@ -1586,7 +1593,7 @@ impl FakeLanguageServer {
let server_name = LanguageServerName(name.clone().into());
let process_name = Arc::from(name.as_str());
let root = Self::root_path();
- let workspace_folders: Arc<Mutex<BTreeSet<Url>>> = Default::default();
+ let workspace_folders: Arc<Mutex<BTreeSet<Uri>>> = Default::default();
let mut server = LanguageServer::new_internal(
server_id,
server_name.clone(),
@@ -1656,13 +1663,13 @@ impl FakeLanguageServer {
(server, fake)
}
#[cfg(target_os = "windows")]
- fn root_path() -> Url {
- Url::from_file_path("C:/").unwrap()
+ fn root_path() -> Uri {
+ Uri::from_file_path("C:/").unwrap()
}
#[cfg(not(target_os = "windows"))]
- fn root_path() -> Url {
- Url::from_file_path("/").unwrap()
+ fn root_path() -> Uri {
+ Uri::from_file_path("/").unwrap()
}
}
@@ -1864,7 +1871,7 @@ mod tests {
server
.notify::<notification::DidOpenTextDocument>(&DidOpenTextDocumentParams {
text_document: TextDocumentItem::new(
- Url::from_str("file://a/b").unwrap(),
+ Uri::from_str("file://a/b").unwrap(),
"rust".to_string(),
0,
"".to_string(),
@@ -1885,7 +1892,7 @@ mod tests {
message: "ok".to_string(),
});
fake.notify::<notification::PublishDiagnostics>(&PublishDiagnosticsParams {
- uri: Url::from_str("file://b/c").unwrap(),
+ uri: Uri::from_str("file://b/c").unwrap(),
version: Some(5),
diagnostics: vec![],
});
@@ -30,7 +30,7 @@ pub fn main() {
let node_runtime = NodeRuntime::unavailable();
let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
- languages::init(language_registry.clone(), node_runtime, cx);
+ languages::init(language_registry, node_runtime, cx);
theme::init(LoadThemes::JustBase, cx);
Assets.load_fonts(cx).unwrap();
@@ -69,6 +69,7 @@ pub struct MarkdownStyle {
pub heading_level_styles: Option<HeadingLevelStyles>,
pub table_overflow_x_scroll: bool,
pub height_is_multiple_of_line_height: bool,
+ pub prevent_mouse_interaction: bool,
}
impl Default for MarkdownStyle {
@@ -89,6 +90,7 @@ impl Default for MarkdownStyle {
heading_level_styles: None,
table_overflow_x_scroll: false,
height_is_multiple_of_line_height: false,
+ prevent_mouse_interaction: false,
}
}
}
@@ -340,27 +342,26 @@ impl Markdown {
}
for (range, event) in &events {
- if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event {
- if let Some(data_url) = dest_url.strip_prefix("data:") {
- let Some((mime_info, data)) = data_url.split_once(',') else {
- continue;
- };
- let Some((mime_type, encoding)) = mime_info.split_once(';') else {
- continue;
- };
- let Some(format) = ImageFormat::from_mime_type(mime_type) else {
- continue;
- };
- let is_base64 = encoding == "base64";
- if is_base64 {
- if let Some(bytes) = base64::prelude::BASE64_STANDARD
- .decode(data)
- .log_with_level(Level::Debug)
- {
- let image = Arc::new(Image::from_bytes(format, bytes));
- images_by_source_offset.insert(range.start, image);
- }
- }
+ if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event
+ && let Some(data_url) = dest_url.strip_prefix("data:")
+ {
+ let Some((mime_info, data)) = data_url.split_once(',') else {
+ continue;
+ };
+ let Some((mime_type, encoding)) = mime_info.split_once(';') else {
+ continue;
+ };
+ let Some(format) = ImageFormat::from_mime_type(mime_type) else {
+ continue;
+ };
+ let is_base64 = encoding == "base64";
+ if is_base64
+ && let Some(bytes) = base64::prelude::BASE64_STANDARD
+ .decode(data)
+ .log_with_level(Level::Debug)
+ {
+ let image = Arc::new(Image::from_bytes(format, bytes));
+ images_by_source_offset.insert(range.start, image);
}
}
}
@@ -576,16 +577,22 @@ impl MarkdownElement {
window: &mut Window,
cx: &mut App,
) {
+ if self.style.prevent_mouse_interaction {
+ return;
+ }
+
let is_hovering_link = hitbox.is_hovered(window)
&& !self.markdown.read(cx).selection.pending
&& rendered_text
.link_for_position(window.mouse_position())
.is_some();
- if is_hovering_link {
- window.set_cursor_style(CursorStyle::PointingHand, hitbox);
- } else {
- window.set_cursor_style(CursorStyle::IBeam, hitbox);
+ if !self.style.prevent_mouse_interaction {
+ if is_hovering_link {
+ window.set_cursor_style(CursorStyle::PointingHand, hitbox);
+ } else {
+ window.set_cursor_style(CursorStyle::IBeam, hitbox);
+ }
}
let on_open_url = self.on_url_click.take();
@@ -659,13 +666,13 @@ impl MarkdownElement {
let rendered_text = rendered_text.clone();
move |markdown, event: &MouseUpEvent, phase, window, cx| {
if phase.bubble() {
- if let Some(pressed_link) = markdown.pressed_link.take() {
- if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
- if let Some(open_url) = on_open_url.as_ref() {
- open_url(pressed_link.destination_url, window, cx);
- } else {
- cx.open_url(&pressed_link.destination_url);
- }
+ if let Some(pressed_link) = markdown.pressed_link.take()
+ && Some(&pressed_link) == rendered_text.link_for_position(event.position)
+ {
+ if let Some(open_url) = on_open_url.as_ref() {
+ open_url(pressed_link.destination_url, window, cx);
+ } else {
+ cx.open_url(&pressed_link.destination_url);
}
}
} else if markdown.selection.pending {
@@ -758,10 +765,10 @@ impl Element for MarkdownElement {
let mut current_img_block_range: Option<Range<usize>> = None;
for (range, event) in parsed_markdown.events.iter() {
// Skip alt text for images that rendered
- if let Some(current_img_block_range) = ¤t_img_block_range {
- if current_img_block_range.end > range.end {
- continue;
- }
+ if let Some(current_img_block_range) = ¤t_img_block_range
+ && current_img_block_range.end > range.end
+ {
+ continue;
}
match event {
@@ -875,7 +882,7 @@ impl Element for MarkdownElement {
(CodeBlockRenderer::Custom { render, .. }, _) => {
let parent_container = render(
kind,
- &parsed_markdown,
+ parsed_markdown,
range.clone(),
metadata.clone(),
window,
@@ -1085,7 +1092,13 @@ impl Element for MarkdownElement {
cx,
);
el.child(
- div().absolute().top_1().right_0p5().w_5().child(codeblock),
+ h_flex()
+ .w_4()
+ .absolute()
+ .top_1p5()
+ .right_1p5()
+ .justify_end()
+ .child(codeblock),
)
});
}
@@ -1110,11 +1123,12 @@ impl Element for MarkdownElement {
cx,
);
el.child(
- div()
+ h_flex()
+ .w_4()
.absolute()
.top_0()
.right_0()
- .w_5()
+ .justify_end()
.visible_on_hover("code_block")
.child(codeblock),
)
@@ -1315,11 +1329,11 @@ fn render_copy_code_block_button(
)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
+ .style(ButtonStyle::Filled)
.shape(ui::IconButtonShape::Square)
- .tooltip(Tooltip::text("Copy Code"))
+ .tooltip(Tooltip::text("Copy"))
.on_click({
- let id = id.clone();
- let markdown = markdown.clone();
+ let markdown = markdown;
move |_event, _window, cx| {
let id = id.clone();
markdown.update(cx, |this, cx| {
@@ -1696,10 +1710,10 @@ impl RenderedText {
while let Some(line) = lines.next() {
let line_bounds = line.layout.bounds();
if position.y > line_bounds.bottom() {
- if let Some(next_line) = lines.peek() {
- if position.y < next_line.layout.bounds().top() {
- return Err(line.source_end);
- }
+ if let Some(next_line) = lines.peek()
+ && position.y < next_line.layout.bounds().top()
+ {
+ return Err(line.source_end);
}
continue;
@@ -247,7 +247,7 @@ pub fn parse_markdown(
events.push(event_for(
text,
range.source_range.start..range.source_range.start + prefix_len,
- &head,
+ head,
));
range.parsed = CowStr::Boxed(tail.into());
range.merged_range.start += prefix_len;
@@ -19,19 +19,21 @@ anyhow.workspace = true
async-recursion.workspace = true
collections.workspace = true
editor.workspace = true
+fs.workspace = true
gpui.workspace = true
+html5ever.workspace = true
language.workspace = true
linkify.workspace = true
log.workspace = true
+markup5ever_rcdom.workspace = true
pretty_assertions.workspace = true
pulldown-cmark.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
-workspace.workspace = true
workspace-hack.workspace = true
-fs.workspace = true
+workspace.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
@@ -1,5 +1,6 @@
use gpui::{
- FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle, UnderlineStyle, px,
+ DefiniteLength, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle,
+ UnderlineStyle, px,
};
use language::HighlightId;
use std::{fmt::Display, ops::Range, path::PathBuf};
@@ -15,6 +16,7 @@ pub enum ParsedMarkdownElement {
/// A paragraph of text and other inline elements.
Paragraph(MarkdownParagraph),
HorizontalRule(Range<usize>),
+ Image(Image),
}
impl ParsedMarkdownElement {
@@ -30,6 +32,7 @@ impl ParsedMarkdownElement {
MarkdownParagraphChunk::Image(image) => image.source_range.clone(),
},
Self::HorizontalRule(range) => range.clone(),
+ Self::Image(image) => image.source_range.clone(),
})
}
@@ -290,6 +293,8 @@ pub struct Image {
pub link: Link,
pub source_range: Range<usize>,
pub alt_text: Option<SharedString>,
+ pub width: Option<DefiniteLength>,
+ pub height: Option<DefiniteLength>,
}
impl Image {
@@ -303,10 +308,20 @@ impl Image {
source_range,
link,
alt_text: None,
+ width: None,
+ height: None,
})
}
pub fn set_alt_text(&mut self, alt_text: SharedString) {
self.alt_text = Some(alt_text);
}
+
+ pub fn set_width(&mut self, width: DefiniteLength) {
+ self.width = Some(width);
+ }
+
+ pub fn set_height(&mut self, height: DefiniteLength) {
+ self.height = Some(height);
+ }
}
@@ -1,10 +1,12 @@
use crate::markdown_elements::*;
use async_recursion::async_recursion;
use collections::FxHashMap;
-use gpui::FontWeight;
+use gpui::{DefiniteLength, FontWeight, px, relative};
+use html5ever::{ParseOpts, local_name, parse_document, tendril::TendrilSink};
use language::LanguageRegistry;
+use markup5ever_rcdom::RcDom;
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
-use std::{ops::Range, path::PathBuf, sync::Arc, vec};
+use std::{cell::RefCell, collections::HashMap, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec};
pub async fn parse_markdown(
markdown_input: &str,
@@ -76,22 +78,22 @@ impl<'a> MarkdownParser<'a> {
if self.eof() || (steps + self.cursor) >= self.tokens.len() {
return self.tokens.last();
}
- return self.tokens.get(self.cursor + steps);
+ self.tokens.get(self.cursor + steps)
}
fn previous(&self) -> Option<&(Event<'_>, Range<usize>)> {
if self.cursor == 0 || self.cursor > self.tokens.len() {
return None;
}
- return self.tokens.get(self.cursor - 1);
+ self.tokens.get(self.cursor - 1)
}
fn current(&self) -> Option<&(Event<'_>, Range<usize>)> {
- return self.peek(0);
+ self.peek(0)
}
fn current_event(&self) -> Option<&Event<'_>> {
- return self.current().map(|(event, _)| event);
+ self.current().map(|(event, _)| event)
}
fn is_text_like(event: &Event) -> bool {
@@ -172,13 +174,17 @@ impl<'a> MarkdownParser<'a> {
self.cursor += 1;
- let code_block = self.parse_code_block(language).await;
+ let code_block = self.parse_code_block(language).await?;
Some(vec![ParsedMarkdownElement::CodeBlock(code_block)])
}
+ Tag::HtmlBlock => {
+ self.cursor += 1;
+
+ Some(self.parse_html_block().await)
+ }
_ => None,
},
Event::Rule => {
- let source_range = source_range.clone();
self.cursor += 1;
Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)])
}
@@ -300,13 +306,12 @@ impl<'a> MarkdownParser<'a> {
if style != MarkdownHighlightStyle::default() && last_run_len < text.len() {
let mut new_highlight = true;
- if let Some((last_range, last_style)) = highlights.last_mut() {
- if last_range.end == last_run_len
- && last_style == &MarkdownHighlight::Style(style.clone())
- {
- last_range.end = text.len();
- new_highlight = false;
- }
+ if let Some((last_range, last_style)) = highlights.last_mut()
+ && last_range.end == last_run_len
+ && last_style == &MarkdownHighlight::Style(style.clone())
+ {
+ last_range.end = text.len();
+ new_highlight = false;
}
if new_highlight {
highlights.push((
@@ -380,7 +385,7 @@ impl<'a> MarkdownParser<'a> {
TagEnd::Image => {
if let Some(mut image) = image.take() {
if !text.is_empty() {
- image.alt_text = Some(std::mem::take(&mut text).into());
+ image.set_alt_text(std::mem::take(&mut text).into());
}
markdown_text_like.push(MarkdownParagraphChunk::Image(image));
}
@@ -402,7 +407,7 @@ impl<'a> MarkdownParser<'a> {
}
if !text.is_empty() {
markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
- source_range: source_range.clone(),
+ source_range,
contents: text,
highlights,
regions,
@@ -421,7 +426,7 @@ impl<'a> MarkdownParser<'a> {
self.cursor += 1;
ParsedMarkdownHeading {
- source_range: source_range.clone(),
+ source_range,
level: match level {
pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1,
pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2,
@@ -579,10 +584,10 @@ impl<'a> MarkdownParser<'a> {
}
} else {
let block = self.parse_block().await;
- if let Some(block) = block {
- if let Some(list_item) = items_stack.last_mut() {
- list_item.content.extend(block);
- }
+ if let Some(block) = block
+ && let Some(list_item) = items_stack.last_mut()
+ {
+ list_item.content.extend(block);
}
}
}
@@ -697,13 +702,22 @@ impl<'a> MarkdownParser<'a> {
}
}
- async fn parse_code_block(&mut self, language: Option<String>) -> ParsedMarkdownCodeBlock {
- let (_event, source_range) = self.previous().unwrap();
+ async fn parse_code_block(
+ &mut self,
+ language: Option<String>,
+ ) -> Option<ParsedMarkdownCodeBlock> {
+ let Some((_event, source_range)) = self.previous() else {
+ return None;
+ };
+
let source_range = source_range.clone();
let mut code = String::new();
while !self.eof() {
- let (current, _source_range) = self.current().unwrap();
+ let Some((current, _source_range)) = self.current() else {
+ break;
+ };
+
match current {
Event::Text(text) => {
code.push_str(text);
@@ -736,23 +750,190 @@ impl<'a> MarkdownParser<'a> {
None
};
- ParsedMarkdownCodeBlock {
+ Some(ParsedMarkdownCodeBlock {
source_range,
contents: code.into(),
language,
highlights,
+ })
+ }
+
+ async fn parse_html_block(&mut self) -> Vec<ParsedMarkdownElement> {
+ let mut elements = Vec::new();
+ let Some((_event, _source_range)) = self.previous() else {
+ return elements;
+ };
+
+ while !self.eof() {
+ let Some((current, source_range)) = self.current() else {
+ break;
+ };
+ let source_range = source_range.clone();
+ match current {
+ Event::Html(html) => {
+ let mut cursor = std::io::Cursor::new(html.as_bytes());
+ let Some(dom) = parse_document(RcDom::default(), ParseOpts::default())
+ .from_utf8()
+ .read_from(&mut cursor)
+ .ok()
+ else {
+ self.cursor += 1;
+ continue;
+ };
+
+ self.cursor += 1;
+
+ self.parse_html_node(source_range, &dom.document, &mut elements);
+ }
+ Event::End(TagEnd::CodeBlock) => {
+ self.cursor += 1;
+ break;
+ }
+ _ => {
+ break;
+ }
+ }
+ }
+
+ elements
+ }
+
+ fn parse_html_node(
+ &self,
+ source_range: Range<usize>,
+ node: &Rc<markup5ever_rcdom::Node>,
+ elements: &mut Vec<ParsedMarkdownElement>,
+ ) {
+ match &node.data {
+ markup5ever_rcdom::NodeData::Document => {
+ self.consume_children(source_range, node, elements);
+ }
+ markup5ever_rcdom::NodeData::Doctype { .. } => {}
+ markup5ever_rcdom::NodeData::Text { contents } => {
+ elements.push(ParsedMarkdownElement::Paragraph(vec![
+ MarkdownParagraphChunk::Text(ParsedMarkdownText {
+ source_range,
+ contents: contents.borrow().to_string(),
+ highlights: Vec::default(),
+ region_ranges: Vec::default(),
+ regions: Vec::default(),
+ }),
+ ]));
+ }
+ markup5ever_rcdom::NodeData::Comment { .. } => {}
+ markup5ever_rcdom::NodeData::Element { name, attrs, .. } => {
+ if local_name!("img") == name.local {
+ if let Some(image) = self.extract_image(source_range, attrs) {
+ elements.push(ParsedMarkdownElement::Image(image));
+ }
+ } else {
+ self.consume_children(source_range, node, elements);
+ }
+ }
+ markup5ever_rcdom::NodeData::ProcessingInstruction { .. } => {}
+ }
+ }
+
+ fn consume_children(
+ &self,
+ source_range: Range<usize>,
+ node: &Rc<markup5ever_rcdom::Node>,
+ elements: &mut Vec<ParsedMarkdownElement>,
+ ) {
+ for node in node.children.borrow().iter() {
+ self.parse_html_node(source_range.clone(), node, elements);
+ }
+ }
+
+ fn attr_value(
+ attrs: &RefCell<Vec<html5ever::Attribute>>,
+ name: html5ever::LocalName,
+ ) -> Option<String> {
+ attrs.borrow().iter().find_map(|attr| {
+ if attr.name.local == name {
+ Some(attr.value.to_string())
+ } else {
+ None
+ }
+ })
+ }
+
+ fn extract_styles_from_attributes(
+ attrs: &RefCell<Vec<html5ever::Attribute>>,
+ ) -> HashMap<String, String> {
+ let mut styles = HashMap::new();
+
+ if let Some(style) = Self::attr_value(attrs, local_name!("style")) {
+ for decl in style.split(';') {
+ let mut parts = decl.splitn(2, ':');
+ if let Some((key, value)) = parts.next().zip(parts.next()) {
+ styles.insert(
+ key.trim().to_lowercase().to_string(),
+ value.trim().to_string(),
+ );
+ }
+ }
+ }
+
+ styles
+ }
+
+ fn extract_image(
+ &self,
+ source_range: Range<usize>,
+ attrs: &RefCell<Vec<html5ever::Attribute>>,
+ ) -> Option<Image> {
+ let src = Self::attr_value(attrs, local_name!("src"))?;
+
+ let mut image = Image::identify(src, source_range, self.file_location_directory.clone())?;
+
+ if let Some(alt) = Self::attr_value(attrs, local_name!("alt")) {
+ image.set_alt_text(alt.into());
+ }
+
+ let styles = Self::extract_styles_from_attributes(attrs);
+
+ if let Some(width) = Self::attr_value(attrs, local_name!("width"))
+ .or_else(|| styles.get("width").cloned())
+ .and_then(|width| Self::parse_length(&width))
+ {
+ image.set_width(width);
+ }
+
+ if let Some(height) = Self::attr_value(attrs, local_name!("height"))
+ .or_else(|| styles.get("height").cloned())
+ .and_then(|height| Self::parse_length(&height))
+ {
+ image.set_height(height);
+ }
+
+ Some(image)
+ }
+
+ /// Parses the width/height attribute value of an html element (e.g. img element)
+ fn parse_length(value: &str) -> Option<DefiniteLength> {
+ if value.ends_with("%") {
+ value
+ .trim_end_matches("%")
+ .parse::<f32>()
+ .ok()
+ .map(|value| relative(value / 100.))
+ } else {
+ value
+ .trim_end_matches("px")
+ .parse()
+ .ok()
+ .map(|value| px(value).into())
}
}
}
#[cfg(test)]
mod tests {
- use core::panic;
-
use super::*;
-
use ParsedMarkdownListItemType::*;
- use gpui::BackgroundExecutor;
+ use core::panic;
+ use gpui::{AbsoluteLength, BackgroundExecutor, DefiniteLength};
use language::{
HighlightId, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, tree_sitter_rust,
};
@@ -927,6 +1108,8 @@ mod tests {
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
},
alt_text: Some("test".into()),
+ height: None,
+ width: None,
},)
);
}
@@ -948,6 +1131,8 @@ mod tests {
url: "http://example.com/foo.png".to_string(),
},
alt_text: None,
+ height: None,
+ width: None,
},)
);
}
@@ -967,6 +1152,8 @@ mod tests {
url: "http://example.com/foo.png".to_string(),
},
alt_text: Some("foo bar baz".into()),
+ height: None,
+ width: None,
}),],
);
}
@@ -992,6 +1179,8 @@ mod tests {
url: "http://example.com/foo.png".to_string(),
},
alt_text: Some("foo".into()),
+ height: None,
+ width: None,
}),
MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: 0..81,
@@ -1006,11 +1195,168 @@ mod tests {
url: "http://example.com/bar.png".to_string(),
},
alt_text: Some("bar".into()),
+ height: None,
+ width: None,
})
]
);
}
+ #[test]
+ fn test_parse_length() {
+ // Test percentage values
+ assert_eq!(
+ MarkdownParser::parse_length("50%"),
+ Some(DefiniteLength::Fraction(0.5))
+ );
+ assert_eq!(
+ MarkdownParser::parse_length("100%"),
+ Some(DefiniteLength::Fraction(1.0))
+ );
+ assert_eq!(
+ MarkdownParser::parse_length("25%"),
+ Some(DefiniteLength::Fraction(0.25))
+ );
+ assert_eq!(
+ MarkdownParser::parse_length("0%"),
+ Some(DefiniteLength::Fraction(0.0))
+ );
+
+ // Test pixel values
+ assert_eq!(
+ MarkdownParser::parse_length("100px"),
+ Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
+ );
+ assert_eq!(
+ MarkdownParser::parse_length("50px"),
+ Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(50.0))))
+ );
+ assert_eq!(
+ MarkdownParser::parse_length("0px"),
+ Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(0.0))))
+ );
+
+ // Test values without units (should be treated as pixels)
+ assert_eq!(
+ MarkdownParser::parse_length("100"),
+ Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0))))
+ );
+ assert_eq!(
+ MarkdownParser::parse_length("42"),
+ Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
+ );
+
+ // Test invalid values
+ assert_eq!(MarkdownParser::parse_length("invalid"), None);
+ assert_eq!(MarkdownParser::parse_length("px"), None);
+ assert_eq!(MarkdownParser::parse_length("%"), None);
+ assert_eq!(MarkdownParser::parse_length(""), None);
+ assert_eq!(MarkdownParser::parse_length("abc%"), None);
+ assert_eq!(MarkdownParser::parse_length("abcpx"), None);
+
+ // Test decimal values
+ assert_eq!(
+ MarkdownParser::parse_length("50.5%"),
+ Some(DefiniteLength::Fraction(0.505))
+ );
+ assert_eq!(
+ MarkdownParser::parse_length("100.25px"),
+ Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.25))))
+ );
+ assert_eq!(
+ MarkdownParser::parse_length("42.0"),
+ Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))))
+ );
+ }
+
+ #[gpui::test]
+ async fn test_html_image_tag() {
+ let parsed = parse("<img src=\"http://example.com/foo.png\" />").await;
+
+ let ParsedMarkdownElement::Image(image) = &parsed.children[0] else {
+ panic!("Expected a image element");
+ };
+ assert_eq!(
+ image.clone(),
+ Image {
+ source_range: 0..40,
+ link: Link::Web {
+ url: "http://example.com/foo.png".to_string(),
+ },
+ alt_text: None,
+ height: None,
+ width: None,
+ },
+ );
+ }
+
+ #[gpui::test]
+ async fn test_html_image_tag_with_alt_text() {
+ let parsed = parse("<img src=\"http://example.com/foo.png\" alt=\"Foo\" />").await;
+
+ let ParsedMarkdownElement::Image(image) = &parsed.children[0] else {
+ panic!("Expected a image element");
+ };
+ assert_eq!(
+ image.clone(),
+ Image {
+ source_range: 0..50,
+ link: Link::Web {
+ url: "http://example.com/foo.png".to_string(),
+ },
+ alt_text: Some("Foo".into()),
+ height: None,
+ width: None,
+ },
+ );
+ }
+
+ #[gpui::test]
+ async fn test_html_image_tag_with_height_and_width() {
+ let parsed =
+ parse("<img src=\"http://example.com/foo.png\" height=\"100\" width=\"200\" />").await;
+
+ let ParsedMarkdownElement::Image(image) = &parsed.children[0] else {
+ panic!("Expected a image element");
+ };
+ assert_eq!(
+ image.clone(),
+ Image {
+ source_range: 0..65,
+ link: Link::Web {
+ url: "http://example.com/foo.png".to_string(),
+ },
+ alt_text: None,
+ height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
+ width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
+ },
+ );
+ }
+
+ #[gpui::test]
+ async fn test_html_image_style_tag_with_height_and_width() {
+ let parsed = parse(
+ "<img src=\"http://example.com/foo.png\" style=\"height:100px; width:200px;\" />",
+ )
+ .await;
+
+ let ParsedMarkdownElement::Image(image) = &parsed.children[0] else {
+ panic!("Expected a image element");
+ };
+ assert_eq!(
+ image.clone(),
+ Image {
+ source_range: 0..75,
+ link: Link::Web {
+ url: "http://example.com/foo.png".to_string(),
+ },
+ alt_text: None,
+ height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))),
+ width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))),
+ },
+ );
+ }
+
#[gpui::test]
async fn test_header_only_table() {
let markdown = "\
@@ -115,8 +115,7 @@ impl MarkdownPreviewView {
pane.activate_item(existing_follow_view_idx, true, true, window, cx);
});
} else {
- let view =
- Self::create_following_markdown_view(workspace, editor.clone(), window, cx);
+ let view = Self::create_following_markdown_view(workspace, editor, window, cx);
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(Box::new(view.clone()), true, true, None, window, cx)
});
@@ -151,10 +150,9 @@ impl MarkdownPreviewView {
if let Some(editor) = workspace
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
+ && Self::is_markdown_file(&editor, cx)
{
- if Self::is_markdown_file(&editor, cx) {
- return Some(editor);
- }
+ return Some(editor);
}
None
}
@@ -243,32 +241,30 @@ impl MarkdownPreviewView {
window: &mut Window,
cx: &mut Context<Self>,
) {
- if let Some(item) = active_item {
- if item.item_id() != cx.entity_id() {
- if let Some(editor) = item.act_as::<Editor>(cx) {
- if Self::is_markdown_file(&editor, cx) {
- self.set_editor(editor, window, cx);
- }
- }
- }
+ if let Some(item) = active_item
+ && item.item_id() != cx.entity_id()
+ && let Some(editor) = item.act_as::<Editor>(cx)
+ && Self::is_markdown_file(&editor, cx)
+ {
+ self.set_editor(editor, window, cx);
}
}
pub fn is_markdown_file<V>(editor: &Entity<Editor>, cx: &mut Context<V>) -> bool {
let buffer = editor.read(cx).buffer().read(cx);
- if let Some(buffer) = buffer.as_singleton() {
- if let Some(language) = buffer.read(cx).language() {
- return language.name() == "Markdown".into();
- }
+ if let Some(buffer) = buffer.as_singleton()
+ && let Some(language) = buffer.read(cx).language()
+ {
+ return language.name() == "Markdown".into();
}
false
}
fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
- if let Some(active) = &self.active_editor {
- if active.editor == editor {
- return;
- }
+ if let Some(active) = &self.active_editor
+ && active.editor == editor
+ {
+ return;
}
let subscription = cx.subscribe_in(
@@ -552,21 +548,20 @@ impl Render for MarkdownPreviewView {
.group("markdown-block")
.on_click(cx.listener(
move |this, event: &ClickEvent, window, cx| {
- if event.click_count() == 2 {
- if let Some(source_range) = this
+ if event.click_count() == 2
+ && let Some(source_range) = this
.contents
.as_ref()
.and_then(|c| c.children.get(ix))
.and_then(|block: &ParsedMarkdownElement| {
block.source_range()
})
- {
- this.move_cursor_to_block(
- window,
- cx,
- source_range.start..source_range.start,
- );
- }
+ {
+ this.move_cursor_to_block(
+ window,
+ cx,
+ source_range.start..source_range.start,
+ );
}
},
))
@@ -1,5 +1,5 @@
use crate::markdown_elements::{
- HeadingLevel, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown,
+ HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown,
ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement,
ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable,
ParsedMarkdownTableAlignment, ParsedMarkdownTableRow,
@@ -111,11 +111,10 @@ impl RenderContext {
/// buffer font size changes. The callees of this function should be reimplemented to use real
/// relative sizing once that is implemented in GPUI
pub fn scaled_rems(&self, rems: f32) -> Rems {
- return self
- .buffer_text_style
+ self.buffer_text_style
.font_size
.to_rems(self.window_rem_size)
- .mul(rems);
+ .mul(rems)
}
/// This ensures that children inside of block quotes
@@ -165,6 +164,7 @@ pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderConte
BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx),
CodeBlock(code_block) => render_markdown_code_block(code_block, cx),
HorizontalRule(_) => render_markdown_rule(cx),
+ Image(image) => render_markdown_image(image, cx),
}
}
@@ -277,7 +277,11 @@ fn render_markdown_list_item(
.items_start()
.children(vec![
bullet,
- div().children(contents).pr(cx.scaled_rems(1.0)).w_full(),
+ v_flex()
+ .children(contents)
+ .gap(cx.scaled_rems(1.0))
+ .pr(cx.scaled_rems(1.0))
+ .w_full(),
]);
cx.with_common_p(item).into_any()
@@ -459,13 +463,13 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -
let mut max_lengths: Vec<usize> = vec![0; parsed.header.children.len()];
for (index, cell) in parsed.header.children.iter().enumerate() {
- let length = paragraph_len(&cell);
+ let length = paragraph_len(cell);
max_lengths[index] = length;
}
for row in &parsed.body {
for (index, cell) in row.children.iter().enumerate() {
- let length = paragraph_len(&cell);
+ let length = paragraph_len(cell);
if length > max_lengths[index] {
max_lengths[index] = length;
@@ -723,65 +727,7 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
}
MarkdownParagraphChunk::Image(image) => {
- let image_resource = match image.link.clone() {
- Link::Web { url } => Resource::Uri(url.into()),
- Link::Path { path, .. } => Resource::Path(Arc::from(path)),
- };
-
- let element_id = cx.next_id(&image.source_range);
-
- let image_element = div()
- .id(element_id)
- .cursor_pointer()
- .child(
- img(ImageSource::Resource(image_resource))
- .max_w_full()
- .with_fallback({
- let alt_text = image.alt_text.clone();
- move || div().children(alt_text.clone()).into_any_element()
- }),
- )
- .tooltip({
- let link = image.link.clone();
- move |_, cx| {
- InteractiveMarkdownElementTooltip::new(
- Some(link.to_string()),
- "open image",
- cx,
- )
- .into()
- }
- })
- .on_click({
- let workspace = workspace_clone.clone();
- let link = image.link.clone();
- move |_, window, cx| {
- if window.modifiers().secondary() {
- match &link {
- Link::Web { url } => cx.open_url(url),
- Link::Path { path, .. } => {
- if let Some(workspace) = &workspace {
- _ = workspace.update(cx, |workspace, cx| {
- workspace
- .open_abs_path(
- path.clone(),
- OpenOptions {
- visible: Some(OpenVisible::None),
- ..Default::default()
- },
- window,
- cx,
- )
- .detach();
- });
- }
- }
- }
- }
- }
- })
- .into_any();
- any_element.push(image_element);
+ any_element.push(render_markdown_image(image, cx));
}
}
}
@@ -794,18 +740,86 @@ fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement {
div().py(cx.scaled_rems(0.5)).child(rule).into_any()
}
+fn render_markdown_image(image: &Image, cx: &mut RenderContext) -> AnyElement {
+ let image_resource = match image.link.clone() {
+ Link::Web { url } => Resource::Uri(url.into()),
+ Link::Path { path, .. } => Resource::Path(Arc::from(path)),
+ };
+
+ let element_id = cx.next_id(&image.source_range);
+ let workspace = cx.workspace.clone();
+
+ div()
+ .id(element_id)
+ .cursor_pointer()
+ .child(
+ img(ImageSource::Resource(image_resource))
+ .max_w_full()
+ .with_fallback({
+ let alt_text = image.alt_text.clone();
+ move || div().children(alt_text.clone()).into_any_element()
+ })
+ .when_some(image.height, |this, height| this.h(height))
+ .when_some(image.width, |this, width| this.w(width)),
+ )
+ .tooltip({
+ let link = image.link.clone();
+ let alt_text = image.alt_text.clone();
+ move |_, cx| {
+ InteractiveMarkdownElementTooltip::new(
+ Some(alt_text.clone().unwrap_or(link.to_string().into())),
+ "open image",
+ cx,
+ )
+ .into()
+ }
+ })
+ .on_click({
+ let link = image.link.clone();
+ move |_, window, cx| {
+ if window.modifiers().secondary() {
+ match &link {
+ Link::Web { url } => cx.open_url(url),
+ Link::Path { path, .. } => {
+ if let Some(workspace) = &workspace {
+ _ = workspace.update(cx, |workspace, cx| {
+ workspace
+ .open_abs_path(
+ path.clone(),
+ OpenOptions {
+ visible: Some(OpenVisible::None),
+ ..Default::default()
+ },
+ window,
+ cx,
+ )
+ .detach();
+ });
+ }
+ }
+ }
+ }
+ }
+ })
+ .into_any()
+}
+
struct InteractiveMarkdownElementTooltip {
tooltip_text: Option<SharedString>,
- action_text: String,
+ action_text: SharedString,
}
impl InteractiveMarkdownElementTooltip {
- pub fn new(tooltip_text: Option<String>, action_text: &str, cx: &mut App) -> Entity<Self> {
+ pub fn new(
+ tooltip_text: Option<SharedString>,
+ action_text: impl Into<SharedString>,
+ cx: &mut App,
+ ) -> Entity<Self> {
let tooltip_text = tooltip_text.map(|t| util::truncate_and_trailoff(&t, 50).into());
cx.new(|_cx| Self {
tooltip_text,
- action_text: action_text.to_string(),
+ action_text: action_text.into(),
})
}
}
@@ -20,14 +20,14 @@ fn replace_deprecated_settings_values(
.nodes_for_capture_index(parent_object_capture_ix)
.next()?
.byte_range();
- let parent_object_name = contents.get(parent_object_range.clone())?;
+ let parent_object_name = contents.get(parent_object_range)?;
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_name_ix)
.next()?
.byte_range();
- let setting_name = contents.get(setting_name_range.clone())?;
+ let setting_name = contents.get(setting_name_range)?;
let setting_value_ix = query.capture_index_for_name("setting_value")?;
let setting_value_range = mat
@@ -242,22 +242,22 @@ static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
"inline_completion::ToggleMenu",
"edit_prediction::ToggleMenu",
),
- ("editor::NextEditPrediction", "editor::NextEditPrediction"),
+ ("editor::NextInlineCompletion", "editor::NextEditPrediction"),
(
- "editor::PreviousEditPrediction",
+ "editor::PreviousInlineCompletion",
"editor::PreviousEditPrediction",
),
(
- "editor::AcceptPartialEditPrediction",
+ "editor::AcceptPartialInlineCompletion",
"editor::AcceptPartialEditPrediction",
),
- ("editor::ShowEditPrediction", "editor::ShowEditPrediction"),
+ ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"),
(
- "editor::AcceptEditPrediction",
+ "editor::AcceptInlineCompletion",
"editor::AcceptEditPrediction",
),
(
- "editor::ToggleEditPredictions",
+ "editor::ToggleInlineCompletions",
"editor::ToggleEditPrediction",
),
])
@@ -279,7 +279,7 @@ fn rename_context_key(
new_predicate = new_predicate.replace(old_key, new_key);
}
if new_predicate != old_predicate {
- Some((context_predicate_range, new_predicate.to_string()))
+ Some((context_predicate_range, new_predicate))
} else {
None
}
@@ -57,7 +57,7 @@ pub fn replace_edit_prediction_provider_setting(
.nodes_for_capture_index(parent_object_capture_ix)
.next()?
.byte_range();
- let parent_object_name = contents.get(parent_object_range.clone())?;
+ let parent_object_name = contents.get(parent_object_range)?;
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_range = mat
@@ -25,7 +25,7 @@ fn replace_tab_close_button_setting_key(
.nodes_for_capture_index(parent_object_capture_ix)
.next()?
.byte_range();
- let parent_object_name = contents.get(parent_object_range.clone())?;
+ let parent_object_name = contents.get(parent_object_range)?;
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_range = mat
@@ -51,14 +51,14 @@ fn replace_tab_close_button_setting_value(
.nodes_for_capture_index(parent_object_capture_ix)
.next()?
.byte_range();
- let parent_object_name = contents.get(parent_object_range.clone())?;
+ let parent_object_name = contents.get(parent_object_range)?;
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_name_ix)
.next()?
.byte_range();
- let setting_name = contents.get(setting_name_range.clone())?;
+ let setting_name = contents.get(setting_name_range)?;
let setting_value_ix = query.capture_index_for_name("setting_value")?;
let setting_value_range = mat
@@ -19,7 +19,7 @@ fn replace_setting_value(
.nodes_for_capture_index(setting_capture_ix)
.next()?
.byte_range();
- let setting_name = contents.get(setting_name_range.clone())?;
+ let setting_name = contents.get(setting_name_range)?;
if setting_name != "hide_mouse_while_typing" {
return None;
@@ -24,7 +24,7 @@ fn rename_assistant(
.nodes_for_capture_index(key_capture_ix)
.next()?
.byte_range();
- return Some((key_range, "agent".to_string()));
+ Some((key_range, "agent".to_string()))
}
fn rename_edit_prediction_assistant(
@@ -37,5 +37,5 @@ fn rename_edit_prediction_assistant(
.nodes_for_capture_index(key_capture_ix)
.next()?
.byte_range();
- return Some((key_range, "enabled_in_text_threads".to_string()));
+ Some((key_range, "enabled_in_text_threads".to_string()))
}
@@ -19,7 +19,7 @@ fn replace_preferred_completion_mode_value(
.nodes_for_capture_index(parent_object_capture_ix)
.next()?
.byte_range();
- let parent_object_name = contents.get(parent_object_range.clone())?;
+ let parent_object_name = contents.get(parent_object_range)?;
if parent_object_name != "agent" {
return None;
@@ -30,7 +30,7 @@ fn replace_preferred_completion_mode_value(
.nodes_for_capture_index(setting_name_capture_ix)
.next()?
.byte_range();
- let setting_name = contents.get(setting_name_range.clone())?;
+ let setting_name = contents.get(setting_name_range)?;
if setting_name != "preferred_completion_mode" {
return None;
@@ -40,20 +40,20 @@ fn migrate_context_server_settings(
// Parse the server settings to check what keys it contains
let mut cursor = server_settings.walk();
for child in server_settings.children(&mut cursor) {
- if child.kind() == "pair" {
- if let Some(key_node) = child.child_by_field_name("key") {
- if let (None, Some(quote_content)) = (column, key_node.child(0)) {
- column = Some(quote_content.start_position().column);
- }
- if let Some(string_content) = key_node.child(1) {
- let key = &contents[string_content.byte_range()];
- match key {
- // If it already has a source key, don't modify it
- "source" => return None,
- "command" => has_command = true,
- "settings" => has_settings = true,
- _ => other_keys += 1,
- }
+ if child.kind() == "pair"
+ && let Some(key_node) = child.child_by_field_name("key")
+ {
+ if let (None, Some(quote_content)) = (column, key_node.child(0)) {
+ column = Some(quote_content.start_position().column);
+ }
+ if let Some(string_content) = key_node.child(1) {
+ let key = &contents[string_content.byte_range()];
+ match key {
+ // If it already has a source key, don't modify it
+ "source" => return None,
+ "command" => has_command = true,
+ "settings" => has_settings = true,
+ _ => other_keys += 1,
}
}
}
@@ -84,10 +84,10 @@ fn remove_pair_with_whitespace(
}
} else {
// If no next sibling, check if there's a comma before
- if let Some(prev_sibling) = pair_node.prev_sibling() {
- if prev_sibling.kind() == "," {
- range_to_remove.start = prev_sibling.start_byte();
- }
+ if let Some(prev_sibling) = pair_node.prev_sibling()
+ && prev_sibling.kind() == ","
+ {
+ range_to_remove.start = prev_sibling.start_byte();
}
}
@@ -123,10 +123,10 @@ fn remove_pair_with_whitespace(
// Also check if we need to include trailing whitespace up to the next line
let text_after = &contents[range_to_remove.end..];
- if let Some(newline_pos) = text_after.find('\n') {
- if text_after[..newline_pos].chars().all(|c| c.is_whitespace()) {
- range_to_remove.end += newline_pos + 1;
- }
+ if let Some(newline_pos) = text_after.find('\n')
+ && text_after[..newline_pos].chars().all(|c| c.is_whitespace())
+ {
+ range_to_remove.end += newline_pos + 1;
}
Some((range_to_remove, String::new()))
@@ -56,19 +56,18 @@ fn flatten_context_server_command(
let mut cursor = command_object.walk();
for child in command_object.children(&mut cursor) {
- if child.kind() == "pair" {
- if let Some(key_node) = child.child_by_field_name("key") {
- if let Some(string_content) = key_node.child(1) {
- let key = &contents[string_content.byte_range()];
- if let Some(value_node) = child.child_by_field_name("value") {
- let value_range = value_node.byte_range();
- match key {
- "path" => path_value = Some(&contents[value_range]),
- "args" => args_value = Some(&contents[value_range]),
- "env" => env_value = Some(&contents[value_range]),
- _ => {}
- }
- }
+ if child.kind() == "pair"
+ && let Some(key_node) = child.child_by_field_name("key")
+ && let Some(string_content) = key_node.child(1)
+ {
+ let key = &contents[string_content.byte_range()];
+ if let Some(value_node) = child.child_by_field_name("value") {
+ let value_range = value_node.byte_range();
+ match key {
+ "path" => path_value = Some(&contents[value_range]),
+ "args" => args_value = Some(&contents[value_range]),
+ "env" => env_value = Some(&contents[value_range]),
+ _ => {}
}
}
}
@@ -28,7 +28,7 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
let syntax_tree = parser
- .parse(&text, None)
+ .parse(text, None)
.context("failed to parse settings")?;
let mut cursor = tree_sitter::QueryCursor::new();
@@ -37,7 +37,7 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt
let mut edits = vec![];
while let Some(mat) = matches.next() {
if let Some((_, callback)) = patterns.get(mat.pattern_index) {
- edits.extend(callback(&text, &mat, query));
+ edits.extend(callback(text, mat, query));
}
}
@@ -170,7 +170,7 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
migrate(
- &text,
+ text,
&[(
SETTINGS_NESTED_KEY_VALUE_PATTERN,
migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
@@ -293,12 +293,12 @@ mod tests {
use super::*;
fn assert_migrate_keymap(input: &str, output: Option<&str>) {
- let migrated = migrate_keymap(&input).unwrap();
+ let migrated = migrate_keymap(input).unwrap();
pretty_assertions::assert_eq!(migrated.as_deref(), output);
}
fn assert_migrate_settings(input: &str, output: Option<&str>) {
- let migrated = migrate_settings(&input).unwrap();
+ let migrated = migrate_settings(input).unwrap();
pretty_assertions::assert_eq!(migrated.as_deref(), output);
}
@@ -286,12 +286,13 @@ pub enum Prediction {
}
#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "snake_case")]
+#[serde(rename_all = "lowercase")]
pub enum ToolChoice {
Auto,
Required,
None,
Any,
+ #[serde(untagged)]
Function(ToolDefinition),
}
@@ -482,7 +483,7 @@ pub async fn stream_completion(
.method(Method::POST)
.uri(uri)
.header("Content-Type", "application/json")
- .header("Authorization", format!("Bearer {}", api_key));
+ .header("Authorization", format!("Bearer {}", api_key.trim()));
let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?;
let mut response = client.send(request).await?;
@@ -76,27 +76,26 @@ impl Anchor {
if text_cmp.is_ne() {
return text_cmp;
}
- if self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some() {
- if let Some(base_text) = snapshot
+ if (self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some())
+ && let Some(base_text) = snapshot
.diffs
.get(&excerpt.buffer_id)
.map(|diff| diff.base_text())
- {
- let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a));
- let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a));
- return match (self_anchor, other_anchor) {
- (Some(a), Some(b)) => a.cmp(&b, base_text),
- (Some(_), None) => match other.text_anchor.bias {
- Bias::Left => Ordering::Greater,
- Bias::Right => Ordering::Less,
- },
- (None, Some(_)) => match self.text_anchor.bias {
- Bias::Left => Ordering::Less,
- Bias::Right => Ordering::Greater,
- },
- (None, None) => Ordering::Equal,
- };
- }
+ {
+ let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a));
+ let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a));
+ return match (self_anchor, other_anchor) {
+ (Some(a), Some(b)) => a.cmp(&b, base_text),
+ (Some(_), None) => match other.text_anchor.bias {
+ Bias::Left => Ordering::Greater,
+ Bias::Right => Ordering::Less,
+ },
+ (None, Some(_)) => match self.text_anchor.bias {
+ Bias::Left => Ordering::Less,
+ Bias::Right => Ordering::Greater,
+ },
+ (None, None) => Ordering::Equal,
+ };
}
}
Ordering::Equal
@@ -107,51 +106,49 @@ impl Anchor {
}
pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
- if self.text_anchor.bias != Bias::Left {
- if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
- return Self {
- buffer_id: self.buffer_id,
- excerpt_id: self.excerpt_id,
- text_anchor: self.text_anchor.bias_left(&excerpt.buffer),
- diff_base_anchor: self.diff_base_anchor.map(|a| {
- if let Some(base_text) = snapshot
- .diffs
- .get(&excerpt.buffer_id)
- .map(|diff| diff.base_text())
- {
- if a.buffer_id == Some(base_text.remote_id()) {
- return a.bias_left(base_text);
- }
- }
- a
- }),
- };
- }
+ if self.text_anchor.bias != Bias::Left
+ && let Some(excerpt) = snapshot.excerpt(self.excerpt_id)
+ {
+ return Self {
+ buffer_id: self.buffer_id,
+ excerpt_id: self.excerpt_id,
+ text_anchor: self.text_anchor.bias_left(&excerpt.buffer),
+ diff_base_anchor: self.diff_base_anchor.map(|a| {
+ if let Some(base_text) = snapshot
+ .diffs
+ .get(&excerpt.buffer_id)
+ .map(|diff| diff.base_text())
+ && a.buffer_id == Some(base_text.remote_id())
+ {
+ return a.bias_left(base_text);
+ }
+ a
+ }),
+ };
}
*self
}
pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
- if self.text_anchor.bias != Bias::Right {
- if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
- return Self {
- buffer_id: self.buffer_id,
- excerpt_id: self.excerpt_id,
- text_anchor: self.text_anchor.bias_right(&excerpt.buffer),
- diff_base_anchor: self.diff_base_anchor.map(|a| {
- if let Some(base_text) = snapshot
- .diffs
- .get(&excerpt.buffer_id)
- .map(|diff| diff.base_text())
- {
- if a.buffer_id == Some(base_text.remote_id()) {
- return a.bias_right(&base_text);
- }
- }
- a
- }),
- };
- }
+ if self.text_anchor.bias != Bias::Right
+ && let Some(excerpt) = snapshot.excerpt(self.excerpt_id)
+ {
+ return Self {
+ buffer_id: self.buffer_id,
+ excerpt_id: self.excerpt_id,
+ text_anchor: self.text_anchor.bias_right(&excerpt.buffer),
+ diff_base_anchor: self.diff_base_anchor.map(|a| {
+ if let Some(base_text) = snapshot
+ .diffs
+ .get(&excerpt.buffer_id)
+ .map(|diff| diff.base_text())
+ && a.buffer_id == Some(base_text.remote_id())
+ {
+ return a.bias_right(base_text);
+ }
+ a
+ }),
+ };
}
*self
}
@@ -212,7 +209,7 @@ impl AnchorRangeExt for Range<Anchor> {
}
fn includes(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
- self.start.cmp(&other.start, &buffer).is_le() && other.end.cmp(&self.end, &buffer).is_le()
+ self.start.cmp(&other.start, buffer).is_le() && other.end.cmp(&self.end, buffer).is_le()
}
fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
@@ -113,15 +113,10 @@ pub enum Event {
transaction_id: TransactionId,
},
Reloaded,
- ReloadNeeded,
-
LanguageChanged(BufferId),
- CapabilityChanged,
Reparsed(BufferId),
Saved,
FileHandleChanged,
- Closed,
- Discarded,
DirtyChanged,
DiagnosticsUpdated,
BufferDiffChanged,
@@ -735,7 +730,7 @@ impl MultiBuffer {
pub fn as_singleton(&self) -> Option<Entity<Buffer>> {
if self.singleton {
- return Some(
+ Some(
self.buffers
.borrow()
.values()
@@ -743,7 +738,7 @@ impl MultiBuffer {
.unwrap()
.buffer
.clone(),
- );
+ )
} else {
None
}
@@ -835,7 +830,7 @@ impl MultiBuffer {
this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns);
drop(snapshot);
- let mut buffer_ids = Vec::new();
+ let mut buffer_ids = Vec::with_capacity(buffer_edits.len());
for (buffer_id, mut edits) in buffer_edits {
buffer_ids.push(buffer_id);
edits.sort_by_key(|edit| edit.range.start);
@@ -1082,11 +1077,11 @@ impl MultiBuffer {
let mut ranges: Vec<Range<usize>> = Vec::new();
for edit in edits {
- if let Some(last_range) = ranges.last_mut() {
- if edit.range.start <= last_range.end {
- last_range.end = last_range.end.max(edit.range.end);
- continue;
- }
+ if let Some(last_range) = ranges.last_mut()
+ && edit.range.start <= last_range.end
+ {
+ last_range.end = last_range.end.max(edit.range.end);
+ continue;
}
ranges.push(edit.range);
}
@@ -1146,13 +1141,13 @@ impl MultiBuffer {
pub fn last_transaction_id(&self, cx: &App) -> Option<TransactionId> {
if let Some(buffer) = self.as_singleton() {
- return buffer
+ buffer
.read(cx)
.peek_undo_stack()
- .map(|history_entry| history_entry.transaction_id());
+ .map(|history_entry| history_entry.transaction_id())
} else {
let last_transaction = self.history.undo_stack.last()?;
- return Some(last_transaction.id);
+ Some(last_transaction.id)
}
}
@@ -1212,25 +1207,24 @@ impl MultiBuffer {
for range in buffer.edited_ranges_for_transaction_id::<D>(*buffer_transaction) {
for excerpt_id in &buffer_state.excerpts {
cursor.seek(excerpt_id, Bias::Left);
- if let Some(excerpt) = cursor.item() {
- if excerpt.locator == *excerpt_id {
- let excerpt_buffer_start =
- excerpt.range.context.start.summary::<D>(buffer);
- let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(buffer);
- let excerpt_range = excerpt_buffer_start..excerpt_buffer_end;
- if excerpt_range.contains(&range.start)
- && excerpt_range.contains(&range.end)
- {
- let excerpt_start = D::from_text_summary(&cursor.start().text);
+ if let Some(excerpt) = cursor.item()
+ && excerpt.locator == *excerpt_id
+ {
+ let excerpt_buffer_start = excerpt.range.context.start.summary::<D>(buffer);
+ let excerpt_buffer_end = excerpt.range.context.end.summary::<D>(buffer);
+ let excerpt_range = excerpt_buffer_start..excerpt_buffer_end;
+ if excerpt_range.contains(&range.start)
+ && excerpt_range.contains(&range.end)
+ {
+ let excerpt_start = D::from_text_summary(&cursor.start().text);
- let mut start = excerpt_start;
- start.add_assign(&(range.start - excerpt_buffer_start));
- let mut end = excerpt_start;
- end.add_assign(&(range.end - excerpt_buffer_start));
+ let mut start = excerpt_start;
+ start.add_assign(&(range.start - excerpt_buffer_start));
+ let mut end = excerpt_start;
+ end.add_assign(&(range.end - excerpt_buffer_start));
- ranges.push(start..end);
- break;
- }
+ ranges.push(start..end);
+ break;
}
}
}
@@ -1251,25 +1245,25 @@ impl MultiBuffer {
buffer.update(cx, |buffer, _| {
buffer.merge_transactions(transaction, destination)
});
- } else if let Some(transaction) = self.history.forget(transaction) {
- if let Some(destination) = self.history.transaction_mut(destination) {
- for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions {
- if let Some(destination_buffer_transaction_id) =
- destination.buffer_transactions.get(&buffer_id)
- {
- if let Some(state) = self.buffers.borrow().get(&buffer_id) {
- state.buffer.update(cx, |buffer, _| {
- buffer.merge_transactions(
- buffer_transaction_id,
- *destination_buffer_transaction_id,
- )
- });
- }
- } else {
- destination
- .buffer_transactions
- .insert(buffer_id, buffer_transaction_id);
+ } else if let Some(transaction) = self.history.forget(transaction)
+ && let Some(destination) = self.history.transaction_mut(destination)
+ {
+ for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions {
+ if let Some(destination_buffer_transaction_id) =
+ destination.buffer_transactions.get(&buffer_id)
+ {
+ if let Some(state) = self.buffers.borrow().get(&buffer_id) {
+ state.buffer.update(cx, |buffer, _| {
+ buffer.merge_transactions(
+ buffer_transaction_id,
+ *destination_buffer_transaction_id,
+ )
+ });
}
+ } else {
+ destination
+ .buffer_transactions
+ .insert(buffer_id, buffer_transaction_id);
}
}
}
@@ -1562,11 +1556,11 @@ impl MultiBuffer {
});
let mut merged_ranges: Vec<ExcerptRange<Point>> = Vec::new();
for range in expanded_ranges {
- if let Some(last_range) = merged_ranges.last_mut() {
- if last_range.context.end >= range.context.start {
- last_range.context.end = range.context.end;
- continue;
- }
+ if let Some(last_range) = merged_ranges.last_mut()
+ && last_range.context.end >= range.context.start
+ {
+ last_range.context.end = range.context.end;
+ continue;
}
merged_ranges.push(range)
}
@@ -1686,7 +1680,7 @@ impl MultiBuffer {
cx: &mut Context<Self>,
) -> (Vec<Range<Anchor>>, bool) {
let (excerpt_ids, added_a_new_excerpt) =
- self.update_path_excerpts(path, buffer, &buffer_snapshot, new, cx);
+ self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx);
let mut result = Vec::new();
let mut ranges = ranges.into_iter();
@@ -1726,7 +1720,7 @@ impl MultiBuffer {
merged_ranges.push(range.clone());
counts.push(1);
}
- return (merged_ranges, counts);
+ (merged_ranges, counts)
}
fn update_path_excerpts(
@@ -1784,7 +1778,7 @@ impl MultiBuffer {
}
Some((
*existing_id,
- excerpt.range.context.to_point(&buffer_snapshot),
+ excerpt.range.context.to_point(buffer_snapshot),
))
} else {
None
@@ -1794,25 +1788,25 @@ impl MultiBuffer {
};
if let Some((last_id, last)) = to_insert.last_mut() {
- if let Some(new) = new {
- if last.context.end >= new.context.start {
- last.context.end = last.context.end.max(new.context.end);
- excerpt_ids.push(*last_id);
- new_iter.next();
- continue;
- }
+ if let Some(new) = new
+ && last.context.end >= new.context.start
+ {
+ last.context.end = last.context.end.max(new.context.end);
+ excerpt_ids.push(*last_id);
+ new_iter.next();
+ continue;
}
- if let Some((existing_id, existing_range)) = &existing {
- if last.context.end >= existing_range.start {
- last.context.end = last.context.end.max(existing_range.end);
- to_remove.push(*existing_id);
- self.snapshot
- .borrow_mut()
- .replaced_excerpts
- .insert(*existing_id, *last_id);
- existing_iter.next();
- continue;
- }
+ if let Some((existing_id, existing_range)) = &existing
+ && last.context.end >= existing_range.start
+ {
+ last.context.end = last.context.end.max(existing_range.end);
+ to_remove.push(*existing_id);
+ self.snapshot
+ .borrow_mut()
+ .replaced_excerpts
+ .insert(*existing_id, *last_id);
+ existing_iter.next();
+ continue;
}
}
@@ -2105,10 +2099,10 @@ impl MultiBuffer {
.flatten()
{
cursor.seek_forward(&Some(locator), Bias::Left);
- if let Some(excerpt) = cursor.item() {
- if excerpt.locator == *locator {
- excerpts.push((excerpt.id, excerpt.range.clone()));
- }
+ if let Some(excerpt) = cursor.item()
+ && excerpt.locator == *locator
+ {
+ excerpts.push((excerpt.id, excerpt.range.clone()));
}
}
@@ -2132,22 +2126,21 @@ impl MultiBuffer {
let mut result = Vec::new();
for locator in locators {
excerpts.seek_forward(&Some(locator), Bias::Left);
- if let Some(excerpt) = excerpts.item() {
- if excerpt.locator == *locator {
- let excerpt_start = excerpts.start().1.clone();
- let excerpt_end =
- ExcerptDimension(excerpt_start.0 + excerpt.text_summary.lines);
+ if let Some(excerpt) = excerpts.item()
+ && excerpt.locator == *locator
+ {
+ let excerpt_start = excerpts.start().1.clone();
+ let excerpt_end = ExcerptDimension(excerpt_start.0 + excerpt.text_summary.lines);
- diff_transforms.seek_forward(&excerpt_start, Bias::Left);
- let overshoot = excerpt_start.0 - diff_transforms.start().0.0;
- let start = diff_transforms.start().1.0 + overshoot;
+ diff_transforms.seek_forward(&excerpt_start, Bias::Left);
+ let overshoot = excerpt_start.0 - diff_transforms.start().0.0;
+ let start = diff_transforms.start().1.0 + overshoot;
- diff_transforms.seek_forward(&excerpt_end, Bias::Right);
- let overshoot = excerpt_end.0 - diff_transforms.start().0.0;
- let end = diff_transforms.start().1.0 + overshoot;
+ diff_transforms.seek_forward(&excerpt_end, Bias::Right);
+ let overshoot = excerpt_end.0 - diff_transforms.start().0.0;
+ let end = diff_transforms.start().1.0 + overshoot;
- result.push(start..end)
- }
+ result.push(start..end)
}
}
result
@@ -2198,6 +2191,15 @@ impl MultiBuffer {
})
}
+ pub fn buffer_for_anchor(&self, anchor: Anchor, cx: &App) -> Option<Entity<Buffer>> {
+ if let Some(buffer_id) = anchor.buffer_id {
+ self.buffer(buffer_id)
+ } else {
+ let (_, buffer, _) = self.excerpt_containing(anchor, cx)?;
+ Some(buffer)
+ }
+ }
+
// If point is at the end of the buffer, the last excerpt is returned
pub fn point_to_buffer_offset<T: ToOffset>(
&self,
@@ -2316,12 +2318,12 @@ impl MultiBuffer {
// Skip over any subsequent excerpts that are also removed.
if let Some(&next_excerpt_id) = excerpt_ids.peek() {
let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id);
- if let Some(next_excerpt) = cursor.item() {
- if next_excerpt.locator == *next_locator {
- excerpt_ids.next();
- excerpt = next_excerpt;
- continue 'remove_excerpts;
- }
+ if let Some(next_excerpt) = cursor.item()
+ && next_excerpt.locator == *next_locator
+ {
+ excerpt_ids.next();
+ excerpt = next_excerpt;
+ continue 'remove_excerpts;
}
}
@@ -2426,28 +2428,24 @@ impl MultiBuffer {
event: &language::BufferEvent,
cx: &mut Context<Self>,
) {
+ use language::BufferEvent;
cx.emit(match event {
- language::BufferEvent::Edited => Event::Edited {
+ BufferEvent::Edited => Event::Edited {
singleton_buffer_edited: true,
- edited_buffer: Some(buffer.clone()),
+ edited_buffer: Some(buffer),
},
- language::BufferEvent::DirtyChanged => Event::DirtyChanged,
- language::BufferEvent::Saved => Event::Saved,
- language::BufferEvent::FileHandleChanged => Event::FileHandleChanged,
- language::BufferEvent::Reloaded => Event::Reloaded,
- language::BufferEvent::ReloadNeeded => Event::ReloadNeeded,
- language::BufferEvent::LanguageChanged => {
- Event::LanguageChanged(buffer.read(cx).remote_id())
- }
- language::BufferEvent::Reparsed => Event::Reparsed(buffer.read(cx).remote_id()),
- language::BufferEvent::DiagnosticsUpdated => Event::DiagnosticsUpdated,
- language::BufferEvent::Closed => Event::Closed,
- language::BufferEvent::Discarded => Event::Discarded,
- language::BufferEvent::CapabilityChanged => {
+ BufferEvent::DirtyChanged => Event::DirtyChanged,
+ BufferEvent::Saved => Event::Saved,
+ BufferEvent::FileHandleChanged => Event::FileHandleChanged,
+ BufferEvent::Reloaded => Event::Reloaded,
+ BufferEvent::LanguageChanged => Event::LanguageChanged(buffer.read(cx).remote_id()),
+ BufferEvent::Reparsed => Event::Reparsed(buffer.read(cx).remote_id()),
+ BufferEvent::DiagnosticsUpdated => Event::DiagnosticsUpdated,
+ BufferEvent::CapabilityChanged => {
self.capability = buffer.read(cx).capability();
- Event::CapabilityChanged
+ return;
}
- language::BufferEvent::Operation { .. } => return,
+ BufferEvent::Operation { .. } | BufferEvent::ReloadNeeded => return,
});
}
@@ -2484,7 +2482,7 @@ impl MultiBuffer {
let base_text_changed = snapshot
.diffs
.get(&buffer_id)
- .map_or(true, |old_diff| !new_diff.base_texts_eq(old_diff));
+ .is_none_or(|old_diff| !new_diff.base_texts_eq(old_diff));
snapshot.diffs.insert(buffer_id, new_diff);
@@ -2494,33 +2492,33 @@ impl MultiBuffer {
.excerpts
.cursor::<Dimensions<Option<&Locator>, ExcerptOffset>>(&());
cursor.seek_forward(&Some(locator), Bias::Left);
- if let Some(excerpt) = cursor.item() {
- if excerpt.locator == *locator {
- let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer);
- if diff_change_range.end < excerpt_buffer_range.start
- || diff_change_range.start > excerpt_buffer_range.end
- {
- continue;
- }
- let excerpt_start = cursor.start().1;
- let excerpt_len = ExcerptOffset::new(excerpt.text_summary.len);
- let diff_change_start_in_excerpt = ExcerptOffset::new(
- diff_change_range
- .start
- .saturating_sub(excerpt_buffer_range.start),
- );
- let diff_change_end_in_excerpt = ExcerptOffset::new(
- diff_change_range
- .end
- .saturating_sub(excerpt_buffer_range.start),
- );
- let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len);
- let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len);
- excerpt_edits.push(Edit {
- old: edit_start..edit_end,
- new: edit_start..edit_end,
- });
+ if let Some(excerpt) = cursor.item()
+ && excerpt.locator == *locator
+ {
+ let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer);
+ if diff_change_range.end < excerpt_buffer_range.start
+ || diff_change_range.start > excerpt_buffer_range.end
+ {
+ continue;
}
+ let excerpt_start = cursor.start().1;
+ let excerpt_len = ExcerptOffset::new(excerpt.text_summary.len);
+ let diff_change_start_in_excerpt = ExcerptOffset::new(
+ diff_change_range
+ .start
+ .saturating_sub(excerpt_buffer_range.start),
+ );
+ let diff_change_end_in_excerpt = ExcerptOffset::new(
+ diff_change_range
+ .end
+ .saturating_sub(excerpt_buffer_range.start),
+ );
+ let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len);
+ let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len);
+ excerpt_edits.push(Edit {
+ old: edit_start..edit_end,
+ new: edit_start..edit_end,
+ });
}
}
@@ -2545,6 +2543,10 @@ impl MultiBuffer {
.collect()
}
+ pub fn all_buffer_ids(&self) -> Vec<BufferId> {
+ self.buffers.borrow().keys().copied().collect()
+ }
+
pub fn buffer(&self, buffer_id: BufferId) -> Option<Entity<Buffer>> {
self.buffers
.borrow()
@@ -2778,7 +2780,7 @@ impl MultiBuffer {
if diff_hunk.excerpt_id.cmp(&end_excerpt_id, &snapshot).is_gt() {
continue;
}
- if last_hunk_row.map_or(false, |row| row >= diff_hunk.row_range.start) {
+ if last_hunk_row.is_some_and(|row| row >= diff_hunk.row_range.start) {
continue;
}
let start = Anchor::in_buffer(
@@ -3042,7 +3044,7 @@ impl MultiBuffer {
is_dirty |= buffer.is_dirty();
has_deleted_file |= buffer
.file()
- .map_or(false, |file| file.disk_state() == DiskState::Deleted);
+ .is_some_and(|file| file.disk_state() == DiskState::Deleted);
has_conflict |= buffer.has_conflict();
}
if edited {
@@ -3056,7 +3058,7 @@ impl MultiBuffer {
snapshot.has_conflict = has_conflict;
for (id, diff) in self.diffs.iter() {
- if snapshot.diffs.get(&id).is_none() {
+ if snapshot.diffs.get(id).is_none() {
snapshot.diffs.insert(*id, diff.diff.read(cx).snapshot(cx));
}
}
@@ -3155,13 +3157,12 @@ impl MultiBuffer {
at_transform_boundary = false;
let transforms_before_edit = old_diff_transforms.slice(&edit.old.start, Bias::Left);
self.append_diff_transforms(&mut new_diff_transforms, transforms_before_edit);
- if let Some(transform) = old_diff_transforms.item() {
- if old_diff_transforms.end().0 == edit.old.start
- && old_diff_transforms.start().0 < edit.old.start
- {
- self.push_diff_transform(&mut new_diff_transforms, transform.clone());
- old_diff_transforms.next();
- }
+ if let Some(transform) = old_diff_transforms.item()
+ && old_diff_transforms.end().0 == edit.old.start
+ && old_diff_transforms.start().0 < edit.old.start
+ {
+ self.push_diff_transform(&mut new_diff_transforms, transform.clone());
+ old_diff_transforms.next();
}
}
@@ -3177,7 +3178,7 @@ impl MultiBuffer {
&mut new_diff_transforms,
&mut end_of_current_insert,
&mut old_expanded_hunks,
- &snapshot,
+ snapshot,
change_kind,
);
@@ -3201,9 +3202,10 @@ impl MultiBuffer {
// If this is the last edit that intersects the current diff transform,
// then recreate the content up to the end of this transform, to prepare
// for reusing additional slices of the old transforms.
- if excerpt_edits.peek().map_or(true, |next_edit| {
- next_edit.old.start >= old_diff_transforms.end().0
- }) {
+ if excerpt_edits
+ .peek()
+ .is_none_or(|next_edit| next_edit.old.start >= old_diff_transforms.end().0)
+ {
let keep_next_old_transform = (old_diff_transforms.start().0 >= edit.old.end)
&& match old_diff_transforms.item() {
Some(DiffTransform::BufferContent {
@@ -3223,7 +3225,7 @@ impl MultiBuffer {
old_expanded_hunks.clear();
self.push_buffer_content_transform(
- &snapshot,
+ snapshot,
&mut new_diff_transforms,
excerpt_offset,
end_of_current_insert,
@@ -3431,18 +3433,17 @@ impl MultiBuffer {
inserted_hunk_info,
summary,
}) = subtree.first()
- {
- if self.extend_last_buffer_content_transform(
+ && self.extend_last_buffer_content_transform(
new_transforms,
*inserted_hunk_info,
*summary,
- ) {
- let mut cursor = subtree.cursor::<()>(&());
- cursor.next();
- cursor.next();
- new_transforms.append(cursor.suffix(), &());
- return;
- }
+ )
+ {
+ let mut cursor = subtree.cursor::<()>(&());
+ cursor.next();
+ cursor.next();
+ new_transforms.append(cursor.suffix(), &());
+ return;
}
new_transforms.append(subtree, &());
}
@@ -3456,14 +3457,13 @@ impl MultiBuffer {
inserted_hunk_info: inserted_hunk_anchor,
summary,
} = transform
- {
- if self.extend_last_buffer_content_transform(
+ && self.extend_last_buffer_content_transform(
new_transforms,
inserted_hunk_anchor,
summary,
- ) {
- return;
- }
+ )
+ {
+ return;
}
new_transforms.push(transform, &());
}
@@ -3518,11 +3518,10 @@ impl MultiBuffer {
summary,
inserted_hunk_info: inserted_hunk_anchor,
} = last_transform
+ && *inserted_hunk_anchor == new_inserted_hunk_info
{
- if *inserted_hunk_anchor == new_inserted_hunk_info {
- *summary += summary_to_add;
- did_extend = true;
- }
+ *summary += summary_to_add;
+ did_extend = true;
}
},
&(),
@@ -3565,9 +3564,7 @@ impl MultiBuffer {
let multi = cx.new(|_| Self::new(Capability::ReadWrite));
for (text, ranges) in excerpts {
let buffer = cx.new(|cx| Buffer::local(text, cx));
- let excerpt_ranges = ranges
- .into_iter()
- .map(|range| ExcerptRange::new(range.clone()));
+ let excerpt_ranges = ranges.into_iter().map(ExcerptRange::new);
multi.update(cx, |multi, cx| {
multi.push_excerpts(buffer, excerpt_ranges, cx)
});
@@ -3583,7 +3580,7 @@ impl MultiBuffer {
pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::App) -> Entity<Self> {
cx.new(|cx| {
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
- let mutation_count = rng.gen_range(1..=5);
+ let mutation_count = rng.random_range(1..=5);
multibuffer.randomly_edit_excerpts(rng, mutation_count, cx);
multibuffer
})
@@ -3601,21 +3598,22 @@ impl MultiBuffer {
let mut edits: Vec<(Range<usize>, Arc<str>)> = Vec::new();
let mut last_end = None;
for _ in 0..edit_count {
- if last_end.map_or(false, |last_end| last_end >= snapshot.len()) {
+ if last_end.is_some_and(|last_end| last_end >= snapshot.len()) {
break;
}
let new_start = last_end.map_or(0, |last_end| last_end + 1);
- let end = snapshot.clip_offset(rng.gen_range(new_start..=snapshot.len()), Bias::Right);
- let start = snapshot.clip_offset(rng.gen_range(new_start..=end), Bias::Right);
+ let end =
+ snapshot.clip_offset(rng.random_range(new_start..=snapshot.len()), Bias::Right);
+ let start = snapshot.clip_offset(rng.random_range(new_start..=end), Bias::Right);
last_end = Some(end);
let mut range = start..end;
- if rng.gen_bool(0.2) {
+ if rng.random_bool(0.2) {
mem::swap(&mut range.start, &mut range.end);
}
- let new_text_len = rng.gen_range(0..10);
+ let new_text_len = rng.random_range(0..10);
let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect();
edits.push((range, new_text.into()));
@@ -3642,18 +3640,18 @@ impl MultiBuffer {
let mut buffers = Vec::new();
for _ in 0..mutation_count {
- if rng.gen_bool(0.05) {
+ if rng.random_bool(0.05) {
log::info!("Clearing multi-buffer");
self.clear(cx);
continue;
- } else if rng.gen_bool(0.1) && !self.excerpt_ids().is_empty() {
+ } else if rng.random_bool(0.1) && !self.excerpt_ids().is_empty() {
let ids = self.excerpt_ids();
let mut excerpts = HashSet::default();
- for _ in 0..rng.gen_range(0..ids.len()) {
+ for _ in 0..rng.random_range(0..ids.len()) {
excerpts.extend(ids.choose(rng).copied());
}
- let line_count = rng.gen_range(0..5);
+ let line_count = rng.random_range(0..5);
log::info!("Expanding excerpts {excerpts:?} by {line_count} lines");
@@ -3667,8 +3665,8 @@ impl MultiBuffer {
}
let excerpt_ids = self.excerpt_ids();
- if excerpt_ids.is_empty() || (rng.r#gen() && excerpt_ids.len() < max_excerpts) {
- let buffer_handle = if rng.r#gen() || self.buffers.borrow().is_empty() {
+ if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) {
+ let buffer_handle = if rng.random() || self.buffers.borrow().is_empty() {
let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
buffers.push(cx.new(|cx| Buffer::local(text, cx)));
let buffer = buffers.last().unwrap().read(cx);
@@ -3690,11 +3688,11 @@ impl MultiBuffer {
let buffer = buffer_handle.read(cx);
let buffer_text = buffer.text();
- let ranges = (0..rng.gen_range(0..5))
+ let ranges = (0..rng.random_range(0..5))
.map(|_| {
let end_ix =
- buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right);
- let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
+ buffer.clip_offset(rng.random_range(0..=buffer.len()), Bias::Right);
+ let start_ix = buffer.clip_offset(rng.random_range(0..=end_ix), Bias::Left);
ExcerptRange::new(start_ix..end_ix)
})
.collect::<Vec<_>>();
@@ -3711,7 +3709,7 @@ impl MultiBuffer {
let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx);
log::info!("Inserted with ids: {:?}", excerpt_id);
} else {
- let remove_count = rng.gen_range(1..=excerpt_ids.len());
+ let remove_count = rng.random_range(1..=excerpt_ids.len());
let mut excerpts_to_remove = excerpt_ids
.choose_multiple(rng, remove_count)
.cloned()
@@ -3733,7 +3731,7 @@ impl MultiBuffer {
) {
use rand::prelude::*;
- if rng.gen_bool(0.7) || self.singleton {
+ if rng.random_bool(0.7) || self.singleton {
let buffer = self
.buffers
.borrow()
@@ -3743,7 +3741,7 @@ impl MultiBuffer {
if let Some(buffer) = buffer {
buffer.update(cx, |buffer, cx| {
- if rng.r#gen() {
+ if rng.random() {
buffer.randomly_edit(rng, mutation_count, cx);
} else {
buffer.randomly_undo_redo(rng, cx);
@@ -3916,8 +3914,8 @@ impl MultiBufferSnapshot {
&self,
range: Range<T>,
) -> Vec<(&BufferSnapshot, Range<usize>, ExcerptId)> {
- let start = range.start.to_offset(&self);
- let end = range.end.to_offset(&self);
+ let start = range.start.to_offset(self);
+ let end = range.end.to_offset(self);
let mut cursor = self.cursor::<usize>();
cursor.seek(&start);
@@ -3955,8 +3953,8 @@ impl MultiBufferSnapshot {
&self,
range: Range<T>,
) -> impl Iterator<Item = (&BufferSnapshot, Range<usize>, ExcerptId, Option<Anchor>)> + '_ {
- let start = range.start.to_offset(&self);
- let end = range.end.to_offset(&self);
+ let start = range.start.to_offset(self);
+ let end = range.end.to_offset(self);
let mut cursor = self.cursor::<usize>();
cursor.seek(&start);
@@ -4037,10 +4035,10 @@ impl MultiBufferSnapshot {
cursor.seek(&query_range.start);
- if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer) {
- if region.range.start > D::zero(&()) {
- cursor.prev()
- }
+ if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer)
+ && region.range.start > D::zero(&())
+ {
+ cursor.prev()
}
iter::from_fn(move || {
@@ -4070,19 +4068,15 @@ impl MultiBufferSnapshot {
buffer_start = cursor.main_buffer_position()?;
};
let mut buffer_end = excerpt.range.context.end.summary::<D>(&excerpt.buffer);
- if let Some((end_excerpt_id, end_buffer_offset)) = range_end {
- if excerpt.id == end_excerpt_id {
- buffer_end = buffer_end.min(end_buffer_offset);
- }
- }
-
- if let Some(iterator) =
- get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end)
+ if let Some((end_excerpt_id, end_buffer_offset)) = range_end
+ && excerpt.id == end_excerpt_id
{
- Some(&mut current_excerpt_metadata.insert((excerpt.id, iterator)).1)
- } else {
- None
+ buffer_end = buffer_end.min(end_buffer_offset);
}
+
+ get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end).map(|iterator| {
+ &mut current_excerpt_metadata.insert((excerpt.id, iterator)).1
+ })
};
// Visit each metadata item.
@@ -4144,10 +4138,10 @@ impl MultiBufferSnapshot {
// When there are no more metadata items for this excerpt, move to the next excerpt.
else {
current_excerpt_metadata.take();
- if let Some((end_excerpt_id, _)) = range_end {
- if excerpt.id == end_excerpt_id {
- return None;
- }
+ if let Some((end_excerpt_id, _)) = range_end
+ && excerpt.id == end_excerpt_id
+ {
+ return None;
}
cursor.next_excerpt();
}
@@ -4186,7 +4180,7 @@ impl MultiBufferSnapshot {
}
let start =
Anchor::in_buffer(excerpt.id, excerpt.buffer_id, hunk.buffer_range.start)
- .to_point(&self);
+ .to_point(self);
return Some(MultiBufferRow(start.row));
}
}
@@ -4204,7 +4198,7 @@ impl MultiBufferSnapshot {
continue;
};
let start = Anchor::in_buffer(excerpt.id, excerpt.buffer_id, hunk.buffer_range.start)
- .to_point(&self);
+ .to_point(self);
return Some(MultiBufferRow(start.row));
}
}
@@ -4455,7 +4449,7 @@ impl MultiBufferSnapshot {
let mut buffer_position = region.buffer_range.start;
buffer_position.add_assign(&overshoot);
let clipped_buffer_position =
- clip_buffer_position(®ion.buffer, buffer_position, bias);
+ clip_buffer_position(region.buffer, buffer_position, bias);
let mut position = region.range.start;
position.add_assign(&(clipped_buffer_position - region.buffer_range.start));
position
@@ -4485,7 +4479,7 @@ impl MultiBufferSnapshot {
let buffer_start_value = region.buffer_range.start.value.unwrap();
let mut buffer_key = buffer_start_key;
buffer_key.add_assign(&(key - start_key));
- let buffer_value = convert_buffer_dimension(®ion.buffer, buffer_key);
+ let buffer_value = convert_buffer_dimension(region.buffer, buffer_key);
let mut result = start_value;
result.add_assign(&(buffer_value - buffer_start_value));
result
@@ -4622,20 +4616,20 @@ impl MultiBufferSnapshot {
pub fn indent_and_comment_for_line(&self, row: MultiBufferRow, cx: &App) -> String {
let mut indent = self.indent_size_for_line(row).chars().collect::<String>();
- if self.language_settings(cx).extend_comment_on_newline {
- if let Some(language_scope) = self.language_scope_at(Point::new(row.0, 0)) {
- let delimiters = language_scope.line_comment_prefixes();
- for delimiter in delimiters {
- if *self
- .chars_at(Point::new(row.0, indent.len() as u32))
- .take(delimiter.chars().count())
- .collect::<String>()
- .as_str()
- == **delimiter
- {
- indent.push_str(&delimiter);
- break;
- }
+ if self.language_settings(cx).extend_comment_on_newline
+ && let Some(language_scope) = self.language_scope_at(Point::new(row.0, 0))
+ {
+ let delimiters = language_scope.line_comment_prefixes();
+ for delimiter in delimiters {
+ if *self
+ .chars_at(Point::new(row.0, indent.len() as u32))
+ .take(delimiter.chars().count())
+ .collect::<String>()
+ .as_str()
+ == **delimiter
+ {
+ indent.push_str(delimiter);
+ break;
}
}
}
@@ -4655,7 +4649,7 @@ impl MultiBufferSnapshot {
return true;
}
}
- return true;
+ true
}
pub fn prev_non_blank_row(&self, mut row: MultiBufferRow) -> Option<MultiBufferRow> {
@@ -4893,25 +4887,22 @@ impl MultiBufferSnapshot {
base_text_byte_range,
..
}) => {
- if let Some(diff_base_anchor) = &anchor.diff_base_anchor {
- if let Some(base_text) =
+ if let Some(diff_base_anchor) = &anchor.diff_base_anchor
+ && let Some(base_text) =
self.diffs.get(buffer_id).map(|diff| diff.base_text())
+ && base_text.can_resolve(diff_base_anchor)
+ {
+ let base_text_offset = diff_base_anchor.to_offset(base_text);
+ if base_text_offset >= base_text_byte_range.start
+ && base_text_offset <= base_text_byte_range.end
{
- if base_text.can_resolve(&diff_base_anchor) {
- let base_text_offset = diff_base_anchor.to_offset(&base_text);
- if base_text_offset >= base_text_byte_range.start
- && base_text_offset <= base_text_byte_range.end
- {
- let position_in_hunk = base_text
- .text_summary_for_range::<D, _>(
- base_text_byte_range.start..base_text_offset,
- );
- position.add_assign(&position_in_hunk);
- } else if at_transform_end {
- diff_transforms.next();
- continue;
- }
- }
+ let position_in_hunk = base_text.text_summary_for_range::<D, _>(
+ base_text_byte_range.start..base_text_offset,
+ );
+ position.add_assign(&position_in_hunk);
+ } else if at_transform_end {
+ diff_transforms.next();
+ continue;
}
}
}
@@ -4941,20 +4932,19 @@ impl MultiBufferSnapshot {
}
let mut position = cursor.start().1;
- if let Some(excerpt) = cursor.item() {
- if excerpt.id == anchor.excerpt_id {
- let excerpt_buffer_start = excerpt
- .buffer
- .offset_for_anchor(&excerpt.range.context.start);
- let excerpt_buffer_end =
- excerpt.buffer.offset_for_anchor(&excerpt.range.context.end);
- let buffer_position = cmp::min(
- excerpt_buffer_end,
- excerpt.buffer.offset_for_anchor(&anchor.text_anchor),
- );
- if buffer_position > excerpt_buffer_start {
- position.value += buffer_position - excerpt_buffer_start;
- }
+ if let Some(excerpt) = cursor.item()
+ && excerpt.id == anchor.excerpt_id
+ {
+ let excerpt_buffer_start = excerpt
+ .buffer
+ .offset_for_anchor(&excerpt.range.context.start);
+ let excerpt_buffer_end = excerpt.buffer.offset_for_anchor(&excerpt.range.context.end);
+ let buffer_position = cmp::min(
+ excerpt_buffer_end,
+ excerpt.buffer.offset_for_anchor(&anchor.text_anchor),
+ );
+ if buffer_position > excerpt_buffer_start {
+ position.value += buffer_position - excerpt_buffer_start;
}
}
position
@@ -4964,7 +4954,7 @@ impl MultiBufferSnapshot {
while let Some(replacement) = self.replaced_excerpts.get(&excerpt_id) {
excerpt_id = *replacement;
}
- return excerpt_id;
+ excerpt_id
}
pub fn summaries_for_anchors<'a, D, I>(&'a self, anchors: I) -> Vec<D>
@@ -5082,9 +5072,9 @@ impl MultiBufferSnapshot {
if point == region.range.end.key && region.has_trailing_newline {
position.add_assign(&D::from_text_summary(&TextSummary::newline()));
}
- return Some(position);
+ Some(position)
} else {
- return Some(D::from_text_summary(&self.text_summary()));
+ Some(D::from_text_summary(&self.text_summary()))
}
})
}
@@ -5124,7 +5114,7 @@ impl MultiBufferSnapshot {
// Leave min and max anchors unchanged if invalid or
// if the old excerpt still exists at this location
let mut kept_position = next_excerpt
- .map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor))
+ .is_some_and(|e| e.id == old_excerpt_id && e.contains(&anchor))
|| old_excerpt_id == ExcerptId::max()
|| old_excerpt_id == ExcerptId::min();
@@ -5211,15 +5201,12 @@ impl MultiBufferSnapshot {
.cursor::<Dimensions<usize, ExcerptOffset>>(&());
diff_transforms.seek(&offset, Bias::Right);
- if offset == diff_transforms.start().0 && bias == Bias::Left {
- if let Some(prev_item) = diff_transforms.prev_item() {
- match prev_item {
- DiffTransform::DeletedHunk { .. } => {
- diff_transforms.prev();
- }
- _ => {}
- }
- }
+ if offset == diff_transforms.start().0
+ && bias == Bias::Left
+ && let Some(prev_item) = diff_transforms.prev_item()
+ && let DiffTransform::DeletedHunk { .. } = prev_item
+ {
+ diff_transforms.prev();
}
let offset_in_transform = offset - diff_transforms.start().0;
let mut excerpt_offset = diff_transforms.start().1;
@@ -7,6 +7,7 @@ use parking_lot::RwLock;
use rand::prelude::*;
use settings::SettingsStore;
use std::env;
+use util::RandomCharIter;
use util::test::sample_text;
#[ctor::ctor]
@@ -473,7 +474,7 @@ fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) {
let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n";
let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n";
let buffer = cx.new(|cx| Buffer::local(text, cx));
- let diff = cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer, cx));
+ let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| {
@@ -2250,11 +2251,11 @@ impl ReferenceMultibuffer {
let base_buffer = diff.base_text();
let mut offset = buffer_range.start;
- let mut hunks = diff
+ let hunks = diff
.hunks_intersecting_range(excerpt.range.clone(), buffer, cx)
.peekable();
- while let Some(hunk) = hunks.next() {
+ for hunk in hunks {
// Ignore hunks that are outside the excerpt range.
let mut hunk_range = hunk.buffer_range.to_offset(buffer);
@@ -2265,14 +2266,14 @@ impl ReferenceMultibuffer {
}
if !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| {
- expanded_anchor.to_offset(&buffer).max(buffer_range.start)
+ expanded_anchor.to_offset(buffer).max(buffer_range.start)
== hunk_range.start.max(buffer_range.start)
}) {
log::trace!("skipping a hunk that's not marked as expanded");
continue;
}
- if !hunk.buffer_range.start.is_valid(&buffer) {
+ if !hunk.buffer_range.start.is_valid(buffer) {
log::trace!("skipping hunk with deleted start: {:?}", hunk.range);
continue;
}
@@ -2449,7 +2450,7 @@ impl ReferenceMultibuffer {
return false;
}
while let Some(hunk) = hunks.peek() {
- match hunk.buffer_range.start.cmp(&hunk_anchor, &buffer) {
+ match hunk.buffer_range.start.cmp(hunk_anchor, &buffer) {
cmp::Ordering::Less => {
hunks.next();
}
@@ -2491,12 +2492,12 @@ async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) {
for _ in 0..operations {
let snapshot = buf.update(cx, |buf, _| buf.snapshot());
- let num_ranges = rng.gen_range(0..=10);
+ let num_ranges = rng.random_range(0..=10);
let max_row = snapshot.max_point().row;
let mut ranges = (0..num_ranges)
.map(|_| {
- let start = rng.gen_range(0..max_row);
- let end = rng.gen_range(start + 1..max_row + 1);
+ let start = rng.random_range(0..max_row);
+ let end = rng.random_range(start + 1..max_row + 1);
Point::row_range(start..end)
})
.collect::<Vec<_>>();
@@ -2519,8 +2520,8 @@ async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) {
let mut seen_ranges = Vec::default();
for (_, buf, range) in snapshot.excerpts() {
- let start = range.context.start.to_point(&buf);
- let end = range.context.end.to_point(&buf);
+ let start = range.context.start.to_point(buf);
+ let end = range.context.end.to_point(buf);
seen_ranges.push(start..end);
if let Some(last_end) = last_end.take() {
@@ -2562,11 +2563,11 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
let mut needs_diff_calculation = false;
for _ in 0..operations {
- match rng.gen_range(0..100) {
+ match rng.random_range(0..100) {
0..=14 if !buffers.is_empty() => {
let buffer = buffers.choose(&mut rng).unwrap();
buffer.update(cx, |buf, cx| {
- let edit_count = rng.gen_range(1..5);
+ let edit_count = rng.random_range(1..5);
buf.randomly_edit(&mut rng, edit_count, cx);
log::info!("buffer text:\n{}", buf.text());
needs_diff_calculation = true;
@@ -2577,11 +2578,11 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
multibuffer.update(cx, |multibuffer, cx| {
let ids = multibuffer.excerpt_ids();
let mut excerpts = HashSet::default();
- for _ in 0..rng.gen_range(0..ids.len()) {
+ for _ in 0..rng.random_range(0..ids.len()) {
excerpts.extend(ids.choose(&mut rng).copied());
}
- let line_count = rng.gen_range(0..5);
+ let line_count = rng.random_range(0..5);
let excerpt_ixs = excerpts
.iter()
@@ -2600,7 +2601,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
}
20..=29 if !reference.excerpts.is_empty() => {
let mut ids_to_remove = vec![];
- for _ in 0..rng.gen_range(1..=3) {
+ for _ in 0..rng.random_range(1..=3) {
let Some(excerpt) = reference.excerpts.choose(&mut rng) else {
break;
};
@@ -2620,8 +2621,12 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
let multibuffer =
multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
let offset =
- multibuffer.clip_offset(rng.gen_range(0..=multibuffer.len()), Bias::Left);
- let bias = if rng.r#gen() { Bias::Left } else { Bias::Right };
+ multibuffer.clip_offset(rng.random_range(0..=multibuffer.len()), Bias::Left);
+ let bias = if rng.random() {
+ Bias::Left
+ } else {
+ Bias::Right
+ };
log::info!("Creating anchor at {} with bias {:?}", offset, bias);
anchors.push(multibuffer.anchor_at(offset, bias));
anchors.sort_by(|a, b| a.cmp(b, &multibuffer));
@@ -2654,7 +2659,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
45..=55 if !reference.excerpts.is_empty() => {
multibuffer.update(cx, |multibuffer, cx| {
let snapshot = multibuffer.snapshot(cx);
- let excerpt_ix = rng.gen_range(0..reference.excerpts.len());
+ let excerpt_ix = rng.random_range(0..reference.excerpts.len());
let excerpt = &reference.excerpts[excerpt_ix];
let start = excerpt.range.start;
let end = excerpt.range.end;
@@ -2691,7 +2696,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
});
}
_ => {
- let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) {
+ let buffer_handle = if buffers.is_empty() || rng.random_bool(0.4) {
let mut base_text = util::RandomCharIter::new(&mut rng)
.take(256)
.collect::<String>();
@@ -2708,7 +2713,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
buffers.choose(&mut rng).unwrap()
};
- let prev_excerpt_ix = rng.gen_range(0..=reference.excerpts.len());
+ let prev_excerpt_ix = rng.random_range(0..=reference.excerpts.len());
let prev_excerpt_id = reference
.excerpts
.get(prev_excerpt_ix)
@@ -2716,8 +2721,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len());
let (range, anchor_range) = buffer_handle.read_with(cx, |buffer, _| {
- let end_row = rng.gen_range(0..=buffer.max_point().row);
- let start_row = rng.gen_range(0..=end_row);
+ let end_row = rng.random_range(0..=buffer.max_point().row);
+ let start_row = rng.random_range(0..=end_row);
let end_ix = buffer.point_to_offset(Point::new(end_row, 0));
let start_ix = buffer.point_to_offset(Point::new(start_row, 0));
let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix);
@@ -2739,9 +2744,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
let id = buffer_handle.read(cx).remote_id();
if multibuffer.diff_for(id).is_none() {
let base_text = base_texts.get(&id).unwrap();
- let diff = cx.new(|cx| {
- BufferDiff::new_with_base_text(base_text, &buffer_handle, cx)
- });
+ let diff = cx
+ .new(|cx| BufferDiff::new_with_base_text(base_text, buffer_handle, cx));
reference.add_diff(diff.clone(), cx);
multibuffer.add_diff(diff, cx)
}
@@ -2767,7 +2771,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
}
}
- if rng.gen_bool(0.3) {
+ if rng.random_bool(0.3) {
multibuffer.update(cx, |multibuffer, cx| {
old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe()));
})
@@ -2816,7 +2820,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
pretty_assertions::assert_eq!(actual_row_infos, expected_row_infos);
for _ in 0..5 {
- let start_row = rng.gen_range(0..=expected_row_infos.len());
+ let start_row = rng.random_range(0..=expected_row_infos.len());
assert_eq!(
snapshot
.row_infos(MultiBufferRow(start_row as u32))
@@ -2873,8 +2877,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
let text_rope = Rope::from(expected_text.as_str());
for _ in 0..10 {
- let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right);
- let start_ix = text_rope.clip_offset(rng.gen_range(0..=end_ix), Bias::Left);
+ let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right);
+ let start_ix = text_rope.clip_offset(rng.random_range(0..=end_ix), Bias::Left);
let text_for_range = snapshot
.text_for_range(start_ix..end_ix)
@@ -2909,7 +2913,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
}
for _ in 0..10 {
- let end_ix = text_rope.clip_offset(rng.gen_range(0..=text_rope.len()), Bias::Right);
+ let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right);
assert_eq!(
snapshot.reversed_chars_at(end_ix).collect::<String>(),
expected_text[..end_ix].chars().rev().collect::<String>(),
@@ -2917,8 +2921,8 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) {
}
for _ in 0..10 {
- let end_ix = rng.gen_range(0..=text_rope.len());
- let start_ix = rng.gen_range(0..=end_ix);
+ let end_ix = rng.random_range(0..=text_rope.len());
+ let start_ix = rng.random_range(0..=end_ix);
assert_eq!(
snapshot
.bytes_in_range(start_ix..end_ix)
@@ -3593,24 +3597,20 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) {
for (anchors, bias) in [(&left_anchors, Bias::Left), (&right_anchors, Bias::Right)] {
for (ix, (offset, anchor)) in offsets.iter().zip(anchors).enumerate() {
- if ix > 0 {
- if *offset == 252 {
- if offset > &offsets[ix - 1] {
- let prev_anchor = left_anchors[ix - 1];
- assert!(
- anchor.cmp(&prev_anchor, snapshot).is_gt(),
- "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()",
- offsets[ix],
- offsets[ix - 1],
- );
- assert!(
- prev_anchor.cmp(&anchor, snapshot).is_lt(),
- "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()",
- offsets[ix - 1],
- offsets[ix],
- );
- }
- }
+ if ix > 0 && *offset == 252 && offset > &offsets[ix - 1] {
+ let prev_anchor = left_anchors[ix - 1];
+ assert!(
+ anchor.cmp(&prev_anchor, snapshot).is_gt(),
+ "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()",
+ offsets[ix],
+ offsets[ix - 1],
+ );
+ assert!(
+ prev_anchor.cmp(anchor, snapshot).is_lt(),
+ "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()",
+ offsets[ix - 1],
+ offsets[ix],
+ );
}
}
}
@@ -3717,3 +3717,235 @@ fn test_new_empty_buffers_title_can_be_set(cx: &mut App) {
});
assert_eq!(multibuffer.read(cx).title(cx), "Hey");
}
+
+#[gpui::test(iterations = 100)]
+fn test_random_chunk_bitmaps(cx: &mut App, mut rng: StdRng) {
+ let multibuffer = if rng.random() {
+ let len = rng.random_range(0..10000);
+ let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+ let buffer = cx.new(|cx| Buffer::local(text, cx));
+ cx.new(|cx| MultiBuffer::singleton(buffer, cx))
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ };
+
+ let snapshot = multibuffer.read(cx).snapshot(cx);
+
+ let chunks = snapshot.chunks(0..snapshot.len(), false);
+
+ for chunk in chunks {
+ let chunk_text = chunk.text;
+ let chars_bitmap = chunk.chars;
+ let tabs_bitmap = chunk.tabs;
+
+ if chunk_text.is_empty() {
+ assert_eq!(
+ chars_bitmap, 0,
+ "Empty chunk should have empty chars bitmap"
+ );
+ assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
+ continue;
+ }
+
+ assert!(
+ chunk_text.len() <= 128,
+ "Chunk text length {} exceeds 128 bytes",
+ chunk_text.len()
+ );
+
+ // Verify chars bitmap
+ let char_indices = chunk_text
+ .char_indices()
+ .map(|(i, _)| i)
+ .collect::<Vec<_>>();
+
+ for byte_idx in 0..chunk_text.len() {
+ let should_have_bit = char_indices.contains(&byte_idx);
+ let has_bit = chars_bitmap & (1 << byte_idx) != 0;
+
+ if has_bit != should_have_bit {
+ eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
+ eprintln!("Char indices: {:?}", char_indices);
+ eprintln!("Chars bitmap: {:#b}", chars_bitmap);
+ }
+
+ assert_eq!(
+ has_bit, should_have_bit,
+ "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, should_have_bit, has_bit
+ );
+ }
+
+ for (byte_idx, byte) in chunk_text.bytes().enumerate() {
+ let is_tab = byte == b'\t';
+ let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
+
+ if has_bit != is_tab {
+ eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
+ eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
+ assert_eq!(
+ has_bit, is_tab,
+ "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, byte as char, is_tab, has_bit
+ );
+ }
+ }
+ }
+}
+
+#[gpui::test(iterations = 100)]
+fn test_random_chunk_bitmaps_with_diffs(cx: &mut App, mut rng: StdRng) {
+ use buffer_diff::BufferDiff;
+ use util::RandomCharIter;
+
+ let multibuffer = if rng.random() {
+ let len = rng.random_range(100..10000);
+ let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+ let buffer = cx.new(|cx| Buffer::local(text, cx));
+ cx.new(|cx| MultiBuffer::singleton(buffer, cx))
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ };
+
+ let _diff_count = rng.random_range(1..5);
+ let mut diffs = Vec::new();
+
+ multibuffer.update(cx, |multibuffer, cx| {
+ for buffer_id in multibuffer.excerpt_buffer_ids() {
+ if rng.random_bool(0.7) {
+ if let Some(buffer_handle) = multibuffer.buffer(buffer_id) {
+ let buffer_text = buffer_handle.read(cx).text();
+ let mut base_text = String::new();
+
+ for line in buffer_text.lines() {
+ if rng.random_bool(0.3) {
+ continue;
+ } else if rng.random_bool(0.3) {
+ let line_len = rng.random_range(0..50);
+ let modified_line = RandomCharIter::new(&mut rng)
+ .take(line_len)
+ .collect::<String>();
+ base_text.push_str(&modified_line);
+ base_text.push('\n');
+ } else {
+ base_text.push_str(line);
+ base_text.push('\n');
+ }
+ }
+
+ if rng.random_bool(0.5) {
+ let extra_lines = rng.random_range(1..5);
+ for _ in 0..extra_lines {
+ let line_len = rng.random_range(0..50);
+ let extra_line = RandomCharIter::new(&mut rng)
+ .take(line_len)
+ .collect::<String>();
+ base_text.push_str(&extra_line);
+ base_text.push('\n');
+ }
+ }
+
+ let diff =
+ cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_handle, cx));
+ diffs.push(diff.clone());
+ multibuffer.add_diff(diff, cx);
+ }
+ }
+ }
+ });
+
+ multibuffer.update(cx, |multibuffer, cx| {
+ if rng.random_bool(0.5) {
+ multibuffer.set_all_diff_hunks_expanded(cx);
+ } else {
+ let snapshot = multibuffer.snapshot(cx);
+ let text = snapshot.text();
+
+ let mut ranges = Vec::new();
+ for _ in 0..rng.random_range(1..5) {
+ if snapshot.len() == 0 {
+ break;
+ }
+
+ let diff_size = rng.random_range(5..1000);
+ let mut start = rng.random_range(0..snapshot.len());
+
+ while !text.is_char_boundary(start) {
+ start = start.saturating_sub(1);
+ }
+
+ let mut end = rng.random_range(start..snapshot.len().min(start + diff_size));
+
+ while !text.is_char_boundary(end) {
+ end = end.saturating_add(1);
+ }
+ let start_anchor = snapshot.anchor_after(start);
+ let end_anchor = snapshot.anchor_before(end);
+ ranges.push(start_anchor..end_anchor);
+ }
+ multibuffer.expand_diff_hunks(ranges, cx);
+ }
+ });
+
+ let snapshot = multibuffer.read(cx).snapshot(cx);
+
+ let chunks = snapshot.chunks(0..snapshot.len(), false);
+
+ for chunk in chunks {
+ let chunk_text = chunk.text;
+ let chars_bitmap = chunk.chars;
+ let tabs_bitmap = chunk.tabs;
+
+ if chunk_text.is_empty() {
+ assert_eq!(
+ chars_bitmap, 0,
+ "Empty chunk should have empty chars bitmap"
+ );
+ assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap");
+ continue;
+ }
+
+ assert!(
+ chunk_text.len() <= 128,
+ "Chunk text length {} exceeds 128 bytes",
+ chunk_text.len()
+ );
+
+ let char_indices = chunk_text
+ .char_indices()
+ .map(|(i, _)| i)
+ .collect::<Vec<_>>();
+
+ for byte_idx in 0..chunk_text.len() {
+ let should_have_bit = char_indices.contains(&byte_idx);
+ let has_bit = chars_bitmap & (1 << byte_idx) != 0;
+
+ if has_bit != should_have_bit {
+ eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
+ eprintln!("Char indices: {:?}", char_indices);
+ eprintln!("Chars bitmap: {:#b}", chars_bitmap);
+ }
+
+ assert_eq!(
+ has_bit, should_have_bit,
+ "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, should_have_bit, has_bit
+ );
+ }
+
+ for (byte_idx, byte) in chunk_text.bytes().enumerate() {
+ let is_tab = byte == b'\t';
+ let has_bit = tabs_bitmap & (1 << byte_idx) != 0;
+
+ if has_bit != is_tab {
+ eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes());
+ eprintln!("Tabs bitmap: {:#b}", tabs_bitmap);
+ assert_eq!(
+ has_bit, is_tab,
+ "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}",
+ byte_idx, chunk_text, byte as char, is_tab, has_bit
+ );
+ }
+ }
+ }
+}
@@ -126,17 +126,17 @@ impl<T> Default for TypedRow<T> {
impl<T> PartialOrd for TypedOffset<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
- Some(self.cmp(&other))
+ Some(self.cmp(other))
}
}
impl<T> PartialOrd for TypedPoint<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
- Some(self.cmp(&other))
+ Some(self.cmp(other))
}
}
impl<T> PartialOrd for TypedRow<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
- Some(self.cmp(&other))
+ Some(self.cmp(other))
}
}